1package termui
2
3import (
4 "bytes"
5 "fmt"
6
7 "github.com/MichaelMure/git-bug/bug"
8 "github.com/MichaelMure/git-bug/cache"
9 "github.com/MichaelMure/git-bug/util"
10 "github.com/dustin/go-humanize"
11 "github.com/jroimartin/gocui"
12)
13
14const bugTableView = "bugTableView"
15const bugTableHeaderView = "bugTableHeaderView"
16const bugTableFooterView = "bugTableFooterView"
17const bugTableInstructionView = "bugTableInstructionView"
18
19const remote = "origin"
20
21type bugTable struct {
22 repo *cache.RepoCache
23 query *cache.Query
24 allIds []string
25 bugs []*cache.BugCache
26 pageCursor int
27 selectCursor int
28}
29
30func newBugTable(c *cache.RepoCache) *bugTable {
31 return &bugTable{
32 repo: c,
33 query: &cache.Query{
34 OrderBy: cache.OrderByCreation,
35 OrderDirection: cache.OrderAscending,
36 },
37 pageCursor: 0,
38 selectCursor: 0,
39 }
40}
41
42func (bt *bugTable) layout(g *gocui.Gui) error {
43 maxX, maxY := g.Size()
44
45 if maxY < 4 {
46 // window too small !
47 return nil
48 }
49
50 v, err := g.SetView(bugTableHeaderView, -1, -1, maxX, 3)
51
52 if err != nil {
53 if err != gocui.ErrUnknownView {
54 return err
55 }
56
57 v.Frame = false
58 }
59
60 v.Clear()
61 bt.renderHeader(v, maxX)
62
63 v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-3)
64
65 if err != nil {
66 if err != gocui.ErrUnknownView {
67 return err
68 }
69
70 v.Frame = false
71 v.Highlight = true
72 v.SelBgColor = gocui.ColorWhite
73 v.SelFgColor = gocui.ColorBlack
74
75 // restore the cursor
76 // window is too small to set the cursor properly, ignoring the error
77 _ = v.SetCursor(0, bt.selectCursor)
78 }
79
80 _, viewHeight := v.Size()
81 err = bt.paginate(viewHeight)
82 if err != nil {
83 return err
84 }
85
86 err = bt.cursorClamp(v)
87 if err != nil {
88 return err
89 }
90
91 v.Clear()
92 bt.render(v, maxX)
93
94 v, err = g.SetView(bugTableFooterView, -1, maxY-4, maxX, maxY)
95
96 if err != nil {
97 if err != gocui.ErrUnknownView {
98 return err
99 }
100
101 v.Frame = false
102 }
103
104 v.Clear()
105 bt.renderFooter(v, maxX)
106
107 v, err = g.SetView(bugTableInstructionView, -1, maxY-2, maxX, maxY)
108
109 if err != nil {
110 if err != gocui.ErrUnknownView {
111 return err
112 }
113
114 v.Frame = false
115 v.BgColor = gocui.ColorBlue
116
117 fmt.Fprintf(v, "[Esc] Quit [←↓↑→,hjkl] Navigation [enter] Open bug [n] New bug [i] Pull [o] Push")
118 }
119
120 _, err = g.SetCurrentView(bugTableView)
121 return err
122}
123
124func (bt *bugTable) keybindings(g *gocui.Gui) error {
125 // Quit
126 if err := g.SetKeybinding(bugTableView, gocui.KeyEsc, gocui.ModNone, quit); err != nil {
127 return err
128 }
129
130 // Down
131 if err := g.SetKeybinding(bugTableView, 'j', gocui.ModNone,
132 bt.cursorDown); err != nil {
133 return err
134 }
135 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowDown, gocui.ModNone,
136 bt.cursorDown); err != nil {
137 return err
138 }
139 // Up
140 if err := g.SetKeybinding(bugTableView, 'k', gocui.ModNone,
141 bt.cursorUp); err != nil {
142 return err
143 }
144 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowUp, gocui.ModNone,
145 bt.cursorUp); err != nil {
146 return err
147 }
148
149 // Previous page
150 if err := g.SetKeybinding(bugTableView, 'h', gocui.ModNone,
151 bt.previousPage); err != nil {
152 return err
153 }
154 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowLeft, gocui.ModNone,
155 bt.previousPage); err != nil {
156 return err
157 }
158 if err := g.SetKeybinding(bugTableView, gocui.KeyPgup, gocui.ModNone,
159 bt.previousPage); err != nil {
160 return err
161 }
162 // Next page
163 if err := g.SetKeybinding(bugTableView, 'l', gocui.ModNone,
164 bt.nextPage); err != nil {
165 return err
166 }
167 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowRight, gocui.ModNone,
168 bt.nextPage); err != nil {
169 return err
170 }
171 if err := g.SetKeybinding(bugTableView, gocui.KeyPgdn, gocui.ModNone,
172 bt.nextPage); err != nil {
173 return err
174 }
175
176 // New bug
177 if err := g.SetKeybinding(bugTableView, 'n', gocui.ModNone,
178 bt.newBug); err != nil {
179 return err
180 }
181
182 // Open bug
183 if err := g.SetKeybinding(bugTableView, gocui.KeyEnter, gocui.ModNone,
184 bt.openBug); err != nil {
185 return err
186 }
187
188 // Pull
189 if err := g.SetKeybinding(bugTableView, 'i', gocui.ModNone,
190 bt.pull); err != nil {
191 return err
192 }
193
194 // Push
195 if err := g.SetKeybinding(bugTableView, 'o', gocui.ModNone,
196 bt.push); err != nil {
197 return err
198 }
199
200 return nil
201}
202
203func (bt *bugTable) disable(g *gocui.Gui) error {
204 if err := g.DeleteView(bugTableView); err != nil && err != gocui.ErrUnknownView {
205 return err
206 }
207 if err := g.DeleteView(bugTableHeaderView); err != nil && err != gocui.ErrUnknownView {
208 return err
209 }
210 if err := g.DeleteView(bugTableFooterView); err != nil && err != gocui.ErrUnknownView {
211 return err
212 }
213 if err := g.DeleteView(bugTableInstructionView); err != nil && err != gocui.ErrUnknownView {
214 return err
215 }
216 return nil
217}
218
219func (bt *bugTable) paginate(max int) error {
220 bt.allIds = bt.repo.QueryBugs(bt.query)
221
222 return bt.doPaginate(max)
223}
224
225func (bt *bugTable) doPaginate(max int) error {
226 // clamp the cursor
227 bt.pageCursor = maxInt(bt.pageCursor, 0)
228 bt.pageCursor = minInt(bt.pageCursor, len(bt.allIds))
229
230 nb := minInt(len(bt.allIds)-bt.pageCursor, max)
231
232 if nb < 0 {
233 bt.bugs = []*cache.BugCache{}
234 return nil
235 }
236
237 // slice the data
238 ids := bt.allIds[bt.pageCursor : bt.pageCursor+nb]
239
240 bt.bugs = make([]*cache.BugCache, len(ids))
241
242 for i, id := range ids {
243 b, err := bt.repo.ResolveBug(id)
244 if err != nil {
245 return err
246 }
247
248 bt.bugs[i] = b
249 }
250
251 return nil
252}
253
254func (bt *bugTable) getTableLength() int {
255 return len(bt.bugs)
256}
257
258func (bt *bugTable) getColumnWidths(maxX int) map[string]int {
259 m := make(map[string]int)
260 m["id"] = 10
261 m["status"] = 8
262
263 left := maxX - 5 - m["id"] - m["status"]
264
265 m["summary"] = maxInt(11, left/6)
266 left -= m["summary"]
267
268 m["lastEdit"] = maxInt(19, left/6)
269 left -= m["lastEdit"]
270
271 m["author"] = maxInt(left*2/5, 15)
272 m["title"] = maxInt(left-m["author"], 10)
273
274 return m
275}
276
277func (bt *bugTable) render(v *gocui.View, maxX int) {
278 columnWidths := bt.getColumnWidths(maxX)
279
280 for _, b := range bt.bugs {
281 person := bug.Person{}
282 snap := b.Snapshot()
283 if len(snap.Comments) > 0 {
284 create := snap.Comments[0]
285 person = create.Author
286 }
287
288 id := util.LeftPaddedString(snap.HumanId(), columnWidths["id"], 2)
289 status := util.LeftPaddedString(snap.Status.String(), columnWidths["status"], 2)
290 title := util.LeftPaddedString(snap.Title, columnWidths["title"], 2)
291 author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
292 summary := util.LeftPaddedString(snap.Summary(), columnWidths["summary"], 2)
293 lastEdit := util.LeftPaddedString(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 2)
294
295 fmt.Fprintf(v, "%s %s %s %s %s %s\n",
296 util.Cyan(id),
297 util.Yellow(status),
298 title,
299 util.Magenta(author),
300 summary,
301 lastEdit,
302 )
303 }
304}
305
306func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
307 columnWidths := bt.getColumnWidths(maxX)
308
309 id := util.LeftPaddedString("ID", columnWidths["id"], 2)
310 status := util.LeftPaddedString("STATUS", columnWidths["status"], 2)
311 title := util.LeftPaddedString("TITLE", columnWidths["title"], 2)
312 author := util.LeftPaddedString("AUTHOR", columnWidths["author"], 2)
313 summary := util.LeftPaddedString("SUMMARY", columnWidths["summary"], 2)
314 lastEdit := util.LeftPaddedString("LAST EDIT", columnWidths["lastEdit"], 2)
315
316 fmt.Fprintf(v, "\n")
317 fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
318
319}
320
321func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
322 fmt.Fprintf(v, " \nShowing %d of %d bugs", len(bt.bugs), len(bt.allIds))
323}
324
325func (bt *bugTable) cursorDown(g *gocui.Gui, v *gocui.View) error {
326 _, y := v.Cursor()
327 y = minInt(y+1, bt.getTableLength()-1)
328
329 // window is too small to set the cursor properly, ignoring the error
330 _ = v.SetCursor(0, y)
331 bt.selectCursor = y
332
333 return nil
334}
335
336func (bt *bugTable) cursorUp(g *gocui.Gui, v *gocui.View) error {
337 _, y := v.Cursor()
338 y = maxInt(y-1, 0)
339
340 // window is too small to set the cursor properly, ignoring the error
341 _ = v.SetCursor(0, y)
342 bt.selectCursor = y
343
344 return nil
345}
346
347func (bt *bugTable) cursorClamp(v *gocui.View) error {
348 _, y := v.Cursor()
349
350 y = minInt(y, bt.getTableLength()-1)
351 y = maxInt(y, 0)
352
353 // window is too small to set the cursor properly, ignoring the error
354 _ = v.SetCursor(0, y)
355 bt.selectCursor = y
356
357 return nil
358}
359
360func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
361 _, max := v.Size()
362
363 if bt.pageCursor+max >= len(bt.allIds) {
364 return nil
365 }
366
367 bt.pageCursor += max
368
369 return bt.doPaginate(max)
370}
371
372func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
373 _, max := v.Size()
374
375 bt.pageCursor = maxInt(0, bt.pageCursor-max)
376
377 return bt.doPaginate(max)
378}
379
380func (bt *bugTable) newBug(g *gocui.Gui, v *gocui.View) error {
381 return newBugWithEditor(bt.repo)
382}
383
384func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
385 _, y := v.Cursor()
386 ui.showBug.SetBug(bt.bugs[y])
387 return ui.activateWindow(ui.showBug)
388}
389
390func (bt *bugTable) pull(g *gocui.Gui, v *gocui.View) error {
391 // Note: this is very hacky
392
393 ui.msgPopup.Activate("Pull from remote "+remote, "...")
394
395 go func() {
396 stdout, err := bt.repo.Fetch(remote)
397
398 if err != nil {
399 g.Update(func(gui *gocui.Gui) error {
400 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
401 return nil
402 })
403 } else {
404 g.Update(func(gui *gocui.Gui) error {
405 ui.msgPopup.UpdateMessage(stdout)
406 return nil
407 })
408 }
409
410 var buffer bytes.Buffer
411 beginLine := ""
412
413 for merge := range bt.repo.MergeAll(remote) {
414 if merge.Status == bug.MsgMergeNothing {
415 continue
416 }
417
418 if merge.Err != nil {
419 g.Update(func(gui *gocui.Gui) error {
420 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
421 return nil
422 })
423 } else {
424 fmt.Fprintf(&buffer, "%s%s: %s",
425 beginLine, util.Cyan(merge.Bug.HumanId()), merge.Status,
426 )
427
428 beginLine = "\n"
429
430 g.Update(func(gui *gocui.Gui) error {
431 ui.msgPopup.UpdateMessage(buffer.String())
432 return nil
433 })
434 }
435 }
436
437 fmt.Fprintf(&buffer, "%sdone", beginLine)
438
439 g.Update(func(gui *gocui.Gui) error {
440 ui.msgPopup.UpdateMessage(buffer.String())
441 return nil
442 })
443
444 }()
445
446 return nil
447}
448
449func (bt *bugTable) push(g *gocui.Gui, v *gocui.View) error {
450 ui.msgPopup.Activate("Push to remote "+remote, "...")
451
452 go func() {
453 // TODO: make the remote configurable
454 stdout, err := bt.repo.Push(remote)
455
456 if err != nil {
457 g.Update(func(gui *gocui.Gui) error {
458 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
459 return nil
460 })
461 } else {
462 g.Update(func(gui *gocui.Gui) error {
463 ui.msgPopup.UpdateMessage(stdout)
464 return nil
465 })
466 }
467 }()
468
469 return nil
470}