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 allIds []string
24 bugs []*cache.BugCache
25 pageCursor int
26 selectCursor int
27}
28
29func newBugTable(cache *cache.RepoCache) *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 bt.allIds = bt.repo.AllBugsId(cache.OrderByCreation, cache.OrderAscending)
216
217 return bt.doPaginate(max)
218}
219
220func (bt *bugTable) doPaginate(max int) error {
221 // clamp the cursor
222 bt.pageCursor = maxInt(bt.pageCursor, 0)
223 bt.pageCursor = minInt(bt.pageCursor, len(bt.allIds))
224
225 nb := minInt(len(bt.allIds)-bt.pageCursor, max)
226
227 if nb < 0 {
228 bt.bugs = []*cache.BugCache{}
229 return nil
230 }
231
232 // slice the data
233 ids := bt.allIds[bt.pageCursor : bt.pageCursor+nb]
234
235 bt.bugs = make([]*cache.BugCache, len(ids))
236
237 for i, id := range ids {
238 b, err := bt.repo.ResolveBug(id)
239 if err != nil {
240 return err
241 }
242
243 bt.bugs[i] = b
244 }
245
246 return nil
247}
248
249func (bt *bugTable) getTableLength() int {
250 return len(bt.bugs)
251}
252
253func (bt *bugTable) getColumnWidths(maxX int) map[string]int {
254 m := make(map[string]int)
255 m["id"] = 10
256 m["status"] = 8
257
258 left := maxX - 5 - m["id"] - m["status"]
259
260 m["summary"] = maxInt(11, left/6)
261 left -= m["summary"]
262
263 m["lastEdit"] = maxInt(19, left/6)
264 left -= m["lastEdit"]
265
266 m["author"] = maxInt(left*2/5, 15)
267 m["title"] = maxInt(left-m["author"], 10)
268
269 return m
270}
271
272func (bt *bugTable) render(v *gocui.View, maxX int) {
273 columnWidths := bt.getColumnWidths(maxX)
274
275 for _, b := range bt.bugs {
276 person := bug.Person{}
277 snap := b.Snapshot()
278 if len(snap.Comments) > 0 {
279 create := snap.Comments[0]
280 person = create.Author
281 }
282
283 id := util.LeftPaddedString(snap.HumanId(), columnWidths["id"], 2)
284 status := util.LeftPaddedString(snap.Status.String(), columnWidths["status"], 2)
285 title := util.LeftPaddedString(snap.Title, columnWidths["title"], 2)
286 author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
287 summary := util.LeftPaddedString(snap.Summary(), columnWidths["summary"], 2)
288 lastEdit := util.LeftPaddedString(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 2)
289
290 fmt.Fprintf(v, "%s %s %s %s %s %s\n",
291 util.Cyan(id),
292 util.Yellow(status),
293 title,
294 util.Magenta(author),
295 summary,
296 lastEdit,
297 )
298 }
299}
300
301func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
302 columnWidths := bt.getColumnWidths(maxX)
303
304 id := util.LeftPaddedString("ID", columnWidths["id"], 2)
305 status := util.LeftPaddedString("STATUS", columnWidths["status"], 2)
306 title := util.LeftPaddedString("TITLE", columnWidths["title"], 2)
307 author := util.LeftPaddedString("AUTHOR", columnWidths["author"], 2)
308 summary := util.LeftPaddedString("SUMMARY", columnWidths["summary"], 2)
309 lastEdit := util.LeftPaddedString("LAST EDIT", columnWidths["lastEdit"], 2)
310
311 fmt.Fprintf(v, "\n")
312 fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
313
314}
315
316func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
317 fmt.Fprintf(v, " \nShowing %d of %d bugs", len(bt.bugs), len(bt.allIds))
318}
319
320func (bt *bugTable) cursorDown(g *gocui.Gui, v *gocui.View) error {
321 _, y := v.Cursor()
322 y = minInt(y+1, bt.getTableLength()-1)
323
324 // window is too small to set the cursor properly, ignoring the error
325 _ = v.SetCursor(0, y)
326 bt.selectCursor = y
327
328 return nil
329}
330
331func (bt *bugTable) cursorUp(g *gocui.Gui, v *gocui.View) error {
332 _, y := v.Cursor()
333 y = maxInt(y-1, 0)
334
335 // window is too small to set the cursor properly, ignoring the error
336 _ = v.SetCursor(0, y)
337 bt.selectCursor = y
338
339 return nil
340}
341
342func (bt *bugTable) cursorClamp(v *gocui.View) error {
343 _, y := v.Cursor()
344
345 y = minInt(y, bt.getTableLength()-1)
346 y = maxInt(y, 0)
347
348 // window is too small to set the cursor properly, ignoring the error
349 _ = v.SetCursor(0, y)
350 bt.selectCursor = y
351
352 return nil
353}
354
355func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
356 _, max := v.Size()
357
358 if bt.pageCursor+max >= len(bt.allIds) {
359 return nil
360 }
361
362 bt.pageCursor += max
363
364 return bt.doPaginate(max)
365}
366
367func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
368 _, max := v.Size()
369
370 bt.pageCursor = maxInt(0, bt.pageCursor-max)
371
372 return bt.doPaginate(max)
373}
374
375func (bt *bugTable) newBug(g *gocui.Gui, v *gocui.View) error {
376 return newBugWithEditor(bt.repo)
377}
378
379func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
380 _, y := v.Cursor()
381 ui.showBug.SetBug(bt.bugs[y])
382 return ui.activateWindow(ui.showBug)
383}
384
385func (bt *bugTable) pull(g *gocui.Gui, v *gocui.View) error {
386 // Note: this is very hacky
387
388 ui.msgPopup.Activate("Pull from remote "+remote, "...")
389
390 go func() {
391 stdout, err := bt.repo.Fetch(remote)
392
393 if err != nil {
394 g.Update(func(gui *gocui.Gui) error {
395 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
396 return nil
397 })
398 } else {
399 g.Update(func(gui *gocui.Gui) error {
400 ui.msgPopup.UpdateMessage(stdout)
401 return nil
402 })
403 }
404
405 var buffer bytes.Buffer
406 beginLine := ""
407
408 for merge := range bt.repo.MergeAll(remote) {
409 if merge.Status == bug.MsgMergeNothing {
410 continue
411 }
412
413 if merge.Err != nil {
414 g.Update(func(gui *gocui.Gui) error {
415 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
416 return nil
417 })
418 } else {
419 fmt.Fprintf(&buffer, "%s%s: %s",
420 beginLine, util.Cyan(merge.Bug.HumanId()), merge.Status,
421 )
422
423 beginLine = "\n"
424
425 g.Update(func(gui *gocui.Gui) error {
426 ui.msgPopup.UpdateMessage(buffer.String())
427 return nil
428 })
429 }
430 }
431
432 fmt.Fprintf(&buffer, "%sdone", beginLine)
433
434 g.Update(func(gui *gocui.Gui) error {
435 ui.msgPopup.UpdateMessage(buffer.String())
436 return nil
437 })
438
439 }()
440
441 return nil
442}
443
444func (bt *bugTable) push(g *gocui.Gui, v *gocui.View) error {
445 ui.msgPopup.Activate("Push to remote "+remote, "...")
446
447 go func() {
448 // TODO: make the remote configurable
449 stdout, err := bt.repo.Push(remote)
450
451 if err != nil {
452 g.Update(func(gui *gocui.Gui) error {
453 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
454 return nil
455 })
456 } else {
457 g.Update(func(gui *gocui.Gui) error {
458 ui.msgPopup.UpdateMessage(stdout)
459 return nil
460 })
461 }
462 }()
463
464 return nil
465}