bug_table.go

  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}