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	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}