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.RepoCacher
23 allIds []string
24 bugs []cache.BugCacher
25 pageCursor int
26 selectCursor int
27}
28
29func newBugTable(cache cache.RepoCacher) *bugTable {
30 return &bugTable{
31 repo: cache,
32 pageCursor: 0,
33 selectCursor: 0,
34 }
35}
36
37func (bt *bugTable) layout(g *gocui.Gui) error {
38 maxX, maxY := g.Size()
39
40 if maxY < 4 {
41 // window too small !
42 return nil
43 }
44
45 v, err := g.SetView(bugTableHeaderView, -1, -1, maxX, 3)
46
47 if err != nil {
48 if err != gocui.ErrUnknownView {
49 return err
50 }
51
52 v.Frame = false
53 }
54
55 v.Clear()
56 bt.renderHeader(v, maxX)
57
58 v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-3)
59
60 if err != nil {
61 if err != gocui.ErrUnknownView {
62 return err
63 }
64
65 v.Frame = false
66 v.Highlight = true
67 v.SelBgColor = gocui.ColorWhite
68 v.SelFgColor = gocui.ColorBlack
69
70 // restore the cursor
71 // window is too small to set the cursor properly, ignoring the error
72 _ = v.SetCursor(0, bt.selectCursor)
73 }
74
75 _, viewHeight := v.Size()
76 err = bt.paginate(viewHeight)
77 if err != nil {
78 return err
79 }
80
81 err = bt.cursorClamp(v)
82 if err != nil {
83 return err
84 }
85
86 v.Clear()
87 bt.render(v, maxX)
88
89 v, err = g.SetView(bugTableFooterView, -1, maxY-4, maxX, maxY)
90
91 if err != nil {
92 if err != gocui.ErrUnknownView {
93 return err
94 }
95
96 v.Frame = false
97 }
98
99 v.Clear()
100 bt.renderFooter(v, maxX)
101
102 v, err = g.SetView(bugTableInstructionView, -1, maxY-2, maxX, maxY)
103
104 if err != nil {
105 if err != gocui.ErrUnknownView {
106 return err
107 }
108
109 v.Frame = false
110 v.BgColor = gocui.ColorBlue
111
112 fmt.Fprintf(v, "[q] Quit [←↓↑→,hjkl] Navigation [enter] Open bug [n] New bug [i] Pull [o] Push")
113 }
114
115 _, err = g.SetCurrentView(bugTableView)
116 return err
117}
118
119func (bt *bugTable) keybindings(g *gocui.Gui) error {
120 // Quit
121 if err := g.SetKeybinding(bugTableView, 'q', gocui.ModNone, quit); err != nil {
122 return err
123 }
124
125 // Down
126 if err := g.SetKeybinding(bugTableView, 'j', gocui.ModNone,
127 bt.cursorDown); err != nil {
128 return err
129 }
130 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowDown, gocui.ModNone,
131 bt.cursorDown); err != nil {
132 return err
133 }
134 // Up
135 if err := g.SetKeybinding(bugTableView, 'k', gocui.ModNone,
136 bt.cursorUp); err != nil {
137 return err
138 }
139 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowUp, gocui.ModNone,
140 bt.cursorUp); err != nil {
141 return err
142 }
143
144 // Previous page
145 if err := g.SetKeybinding(bugTableView, 'h', gocui.ModNone,
146 bt.previousPage); err != nil {
147 return err
148 }
149 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowLeft, gocui.ModNone,
150 bt.previousPage); err != nil {
151 return err
152 }
153 if err := g.SetKeybinding(bugTableView, gocui.KeyPgup, gocui.ModNone,
154 bt.previousPage); err != nil {
155 return err
156 }
157 // Next page
158 if err := g.SetKeybinding(bugTableView, 'l', gocui.ModNone,
159 bt.nextPage); err != nil {
160 return err
161 }
162 if err := g.SetKeybinding(bugTableView, gocui.KeyArrowRight, gocui.ModNone,
163 bt.nextPage); err != nil {
164 return err
165 }
166 if err := g.SetKeybinding(bugTableView, gocui.KeyPgdn, gocui.ModNone,
167 bt.nextPage); err != nil {
168 return err
169 }
170
171 // New bug
172 if err := g.SetKeybinding(bugTableView, 'n', gocui.ModNone,
173 bt.newBug); err != nil {
174 return err
175 }
176
177 // Open bug
178 if err := g.SetKeybinding(bugTableView, gocui.KeyEnter, gocui.ModNone,
179 bt.openBug); err != nil {
180 return err
181 }
182
183 // Pull
184 if err := g.SetKeybinding(bugTableView, 'i', gocui.ModNone,
185 bt.pull); err != nil {
186 return err
187 }
188
189 // Push
190 if err := g.SetKeybinding(bugTableView, 'o', gocui.ModNone,
191 bt.push); err != nil {
192 return err
193 }
194
195 return nil
196}
197
198func (bt *bugTable) disable(g *gocui.Gui) error {
199 if err := g.DeleteView(bugTableView); err != nil && err != gocui.ErrUnknownView {
200 return err
201 }
202 if err := g.DeleteView(bugTableHeaderView); err != nil && err != gocui.ErrUnknownView {
203 return err
204 }
205 if err := g.DeleteView(bugTableFooterView); err != nil && err != gocui.ErrUnknownView {
206 return err
207 }
208 if err := g.DeleteView(bugTableInstructionView); err != nil && err != gocui.ErrUnknownView {
209 return err
210 }
211 return nil
212}
213
214func (bt *bugTable) paginate(max int) error {
215 allIds, err := bt.repo.AllBugIds()
216 if err != nil {
217 return err
218 }
219
220 bt.allIds = allIds
221
222 return bt.doPaginate(allIds, max)
223}
224
225func (bt *bugTable) doPaginate(allIds []string, max int) error {
226 // clamp the cursor
227 bt.pageCursor = maxInt(bt.pageCursor, 0)
228 bt.pageCursor = minInt(bt.pageCursor, len(allIds))
229
230 nb := minInt(len(allIds)-bt.pageCursor, max)
231
232 if nb < 0 {
233 bt.bugs = []cache.BugCacher{}
234 return nil
235 }
236
237 // slice the data
238 ids := allIds[bt.pageCursor : bt.pageCursor+nb]
239
240 bt.bugs = make([]cache.BugCacher, 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 allIds, err := bt.repo.AllBugIds()
364 if err != nil {
365 return err
366 }
367
368 bt.allIds = allIds
369
370 if bt.pageCursor+max >= len(allIds) {
371 return nil
372 }
373
374 bt.pageCursor += max
375
376 return bt.doPaginate(allIds, max)
377}
378
379func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
380 _, max := v.Size()
381 allIds, err := bt.repo.AllBugIds()
382 if err != nil {
383 return err
384 }
385
386 bt.allIds = allIds
387
388 bt.pageCursor = maxInt(0, bt.pageCursor-max)
389
390 return bt.doPaginate(allIds, max)
391}
392
393func (bt *bugTable) newBug(g *gocui.Gui, v *gocui.View) error {
394 return newBugWithEditor(bt.repo)
395}
396
397func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
398 _, y := v.Cursor()
399 ui.showBug.SetBug(bt.bugs[y])
400 return ui.activateWindow(ui.showBug)
401}
402
403func (bt *bugTable) pull(g *gocui.Gui, v *gocui.View) error {
404 // Note: this is very hacky
405
406 ui.msgPopup.Activate("Pull from remote "+remote, "...")
407
408 go func() {
409 stdout, err := bt.repo.Fetch(remote)
410
411 if err != nil {
412 g.Update(func(gui *gocui.Gui) error {
413 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
414 return nil
415 })
416 } else {
417 g.Update(func(gui *gocui.Gui) error {
418 ui.msgPopup.UpdateMessage(stdout)
419 return nil
420 })
421 }
422
423 var buffer bytes.Buffer
424 beginLine := ""
425
426 for merge := range bt.repo.MergeAll(remote) {
427 if merge.Status == bug.MsgMergeNothing {
428 continue
429 }
430
431 if merge.Err != nil {
432 g.Update(func(gui *gocui.Gui) error {
433 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
434 return nil
435 })
436 } else {
437 fmt.Fprintf(&buffer, "%s%s: %s",
438 beginLine, util.Cyan(merge.HumanId), merge.Status,
439 )
440
441 beginLine = "\n"
442
443 g.Update(func(gui *gocui.Gui) error {
444 ui.msgPopup.UpdateMessage(buffer.String())
445 return nil
446 })
447 }
448 }
449
450 fmt.Fprintf(&buffer, "%sdone", beginLine)
451
452 g.Update(func(gui *gocui.Gui) error {
453 ui.msgPopup.UpdateMessage(buffer.String())
454 return nil
455 })
456
457 }()
458
459 return nil
460}
461
462func (bt *bugTable) push(g *gocui.Gui, v *gocui.View) error {
463 ui.msgPopup.Activate("Push to remote "+remote, "...")
464
465 go func() {
466 // TODO: make the remote configurable
467 stdout, err := bt.repo.Push(remote)
468
469 if err != nil {
470 g.Update(func(gui *gocui.Gui) error {
471 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
472 return nil
473 })
474 } else {
475 g.Update(func(gui *gocui.Gui) error {
476 ui.msgPopup.UpdateMessage(stdout)
477 return nil
478 })
479 }
480 }()
481
482 return nil
483}