bug_table.go

  1package termui
  2
  3import (
  4	"fmt"
  5	"github.com/MichaelMure/git-bug/bug"
  6	"github.com/MichaelMure/git-bug/cache"
  7	"github.com/MichaelMure/git-bug/util"
  8	"github.com/dustin/go-humanize"
  9	"github.com/jroimartin/gocui"
 10)
 11
 12const bugTableView = "bugTableView"
 13const bugTableHeaderView = "bugTableHeaderView"
 14const bugTableFooterView = "bugTableFooterView"
 15const bugTableInstructionView = "bugTableInstructionView"
 16
 17type bugTable struct {
 18	repo         cache.RepoCacher
 19	allIds       []string
 20	bugs         []cache.BugCacher
 21	pageCursor   int
 22	selectCursor int
 23}
 24
 25func newBugTable(cache cache.RepoCacher) *bugTable {
 26	return &bugTable{
 27		repo:         cache,
 28		pageCursor:   0,
 29		selectCursor: 0,
 30	}
 31}
 32
 33func (bt *bugTable) layout(g *gocui.Gui) error {
 34	maxX, maxY := g.Size()
 35
 36	if maxY < 4 {
 37		// window too small !
 38		return nil
 39	}
 40
 41	v, err := g.SetView(bugTableHeaderView, -1, -1, maxX, 3)
 42
 43	if err != nil {
 44		if err != gocui.ErrUnknownView {
 45			return err
 46		}
 47
 48		v.Frame = false
 49	}
 50
 51	v.Clear()
 52	bt.renderHeader(v, maxX)
 53
 54	v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-3)
 55
 56	if err != nil {
 57		if err != gocui.ErrUnknownView {
 58			return err
 59		}
 60
 61		v.Frame = false
 62		v.Highlight = true
 63		v.SelBgColor = gocui.ColorWhite
 64		v.SelFgColor = gocui.ColorBlack
 65
 66		// restore the cursor
 67		// window is too small to set the cursor properly, ignoring the error
 68		_ = v.SetCursor(0, bt.selectCursor)
 69	}
 70
 71	_, viewHeight := v.Size()
 72	err = bt.paginate(viewHeight)
 73	if err != nil {
 74		return err
 75	}
 76
 77	err = bt.cursorClamp(v)
 78	if err != nil {
 79		return err
 80	}
 81
 82	v.Clear()
 83	bt.render(v, maxX)
 84
 85	v, err = g.SetView(bugTableFooterView, -1, maxY-4, maxX, maxY)
 86
 87	if err != nil {
 88		if err != gocui.ErrUnknownView {
 89			return err
 90		}
 91
 92		v.Frame = false
 93	}
 94
 95	v.Clear()
 96	bt.renderFooter(v, maxX)
 97
 98	v, err = g.SetView(bugTableInstructionView, -1, maxY-2, maxX, maxY)
 99
100	if err != nil {
101		if err != gocui.ErrUnknownView {
102			return err
103		}
104
105		v.Frame = false
106		v.BgColor = gocui.ColorBlue
107
108		fmt.Fprintf(v, "[q] Quit [←,h] Previous page [↓,j] Down [↑,k] Up [→,l] Next page [enter] Open bug [n] New bug")
109	}
110
111	_, err = g.SetCurrentView(bugTableView)
112	return err
113}
114
115func (bt *bugTable) keybindings(g *gocui.Gui) error {
116	// Quit
117	if err := g.SetKeybinding(bugTableView, 'q', gocui.ModNone, quit); err != nil {
118		return err
119	}
120
121	// Down
122	if err := g.SetKeybinding(bugTableView, 'j', gocui.ModNone,
123		bt.cursorDown); err != nil {
124		return err
125	}
126	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowDown, gocui.ModNone,
127		bt.cursorDown); err != nil {
128		return err
129	}
130	// Up
131	if err := g.SetKeybinding(bugTableView, 'k', gocui.ModNone,
132		bt.cursorUp); err != nil {
133		return err
134	}
135	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowUp, gocui.ModNone,
136		bt.cursorUp); err != nil {
137		return err
138	}
139
140	// Previous page
141	if err := g.SetKeybinding(bugTableView, 'h', gocui.ModNone,
142		bt.previousPage); err != nil {
143		return err
144	}
145	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowLeft, gocui.ModNone,
146		bt.previousPage); err != nil {
147		return err
148	}
149	if err := g.SetKeybinding(bugTableView, gocui.KeyPgup, gocui.ModNone,
150		bt.previousPage); err != nil {
151		return err
152	}
153	// Next page
154	if err := g.SetKeybinding(bugTableView, 'l', gocui.ModNone,
155		bt.nextPage); err != nil {
156		return err
157	}
158	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowRight, gocui.ModNone,
159		bt.nextPage); err != nil {
160		return err
161	}
162	if err := g.SetKeybinding(bugTableView, gocui.KeyPgdn, gocui.ModNone,
163		bt.nextPage); err != nil {
164		return err
165	}
166
167	// New bug
168	if err := g.SetKeybinding(bugTableView, 'n', gocui.ModNone,
169		bt.newBug); err != nil {
170		return err
171	}
172
173	// Open bug
174	if err := g.SetKeybinding(bugTableView, gocui.KeyEnter, gocui.ModNone,
175		bt.openBug); err != nil {
176		return err
177	}
178
179	return nil
180}
181
182func (bt *bugTable) disable(g *gocui.Gui) error {
183	if err := g.DeleteView(bugTableView); err != nil {
184		return err
185	}
186	if err := g.DeleteView(bugTableHeaderView); err != nil {
187		return err
188	}
189	if err := g.DeleteView(bugTableFooterView); err != nil {
190		return err
191	}
192	if err := g.DeleteView(bugTableInstructionView); err != nil {
193		return err
194	}
195	return nil
196}
197
198func (bt *bugTable) paginate(max int) error {
199	allIds, err := bt.repo.AllBugIds()
200	if err != nil {
201		return err
202	}
203
204	bt.allIds = allIds
205
206	return bt.doPaginate(allIds, max)
207}
208
209func (bt *bugTable) doPaginate(allIds []string, max int) error {
210	// clamp the cursor
211	bt.pageCursor = maxInt(bt.pageCursor, 0)
212	bt.pageCursor = minInt(bt.pageCursor, len(allIds))
213
214	nb := minInt(len(allIds)-bt.pageCursor, max)
215
216	if nb < 0 {
217		bt.bugs = []cache.BugCacher{}
218		return nil
219	}
220
221	// slice the data
222	ids := allIds[bt.pageCursor : bt.pageCursor+nb]
223
224	bt.bugs = make([]cache.BugCacher, len(ids))
225
226	for i, id := range ids {
227		b, err := bt.repo.ResolveBug(id)
228		if err != nil {
229			return err
230		}
231
232		bt.bugs[i] = b
233	}
234
235	return nil
236}
237
238func (bt *bugTable) getTableLength() int {
239	return len(bt.bugs)
240}
241
242func (bt *bugTable) getColumnWidths(maxX int) map[string]int {
243	m := make(map[string]int)
244	m["id"] = 10
245	m["status"] = 8
246
247	left := maxX - 5 - m["id"] - m["status"]
248
249	m["summary"] = maxInt(11, left/6)
250	left -= m["summary"]
251
252	m["lastEdit"] = maxInt(19, left/6)
253	left -= m["lastEdit"]
254
255	m["author"] = maxInt(left*2/5, 15)
256	m["title"] = maxInt(left-m["author"], 10)
257
258	return m
259}
260
261func (bt *bugTable) render(v *gocui.View, maxX int) {
262	columnWidths := bt.getColumnWidths(maxX)
263
264	for _, b := range bt.bugs {
265		person := bug.Person{}
266		snap := b.Snapshot()
267		if len(snap.Comments) > 0 {
268			create := snap.Comments[0]
269			person = create.Author
270		}
271
272		id := util.LeftPaddedString(snap.HumanId(), columnWidths["id"], 2)
273		status := util.LeftPaddedString(snap.Status.String(), columnWidths["status"], 2)
274		title := util.LeftPaddedString(snap.Title, columnWidths["title"], 2)
275		author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
276		summary := util.LeftPaddedString(snap.Summary(), columnWidths["summary"], 2)
277		lastEdit := util.LeftPaddedString(humanize.Time(snap.LastEdit()), columnWidths["lastEdit"], 2)
278
279		fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
280	}
281}
282
283func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
284	columnWidths := bt.getColumnWidths(maxX)
285
286	id := util.LeftPaddedString("ID", columnWidths["id"], 2)
287	status := util.LeftPaddedString("STATUS", columnWidths["status"], 2)
288	title := util.LeftPaddedString("TITLE", columnWidths["title"], 2)
289	author := util.LeftPaddedString("AUTHOR", columnWidths["author"], 2)
290	summary := util.LeftPaddedString("SUMMARY", columnWidths["summary"], 2)
291	lastEdit := util.LeftPaddedString("LAST EDIT", columnWidths["lastEdit"], 2)
292
293	fmt.Fprintf(v, "\n")
294	fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
295
296}
297
298func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
299	fmt.Fprintf(v, " \nShowing %d of %d bugs", len(bt.bugs), len(bt.allIds))
300}
301
302func (bt *bugTable) cursorDown(g *gocui.Gui, v *gocui.View) error {
303	_, y := v.Cursor()
304	y = minInt(y+1, bt.getTableLength()-1)
305
306	// window is too small to set the cursor properly, ignoring the error
307	_ = v.SetCursor(0, y)
308	bt.selectCursor = y
309
310	return nil
311}
312
313func (bt *bugTable) cursorUp(g *gocui.Gui, v *gocui.View) error {
314	_, y := v.Cursor()
315	y = maxInt(y-1, 0)
316
317	// window is too small to set the cursor properly, ignoring the error
318	_ = v.SetCursor(0, y)
319	bt.selectCursor = y
320
321	return nil
322}
323
324func (bt *bugTable) cursorClamp(v *gocui.View) error {
325	_, y := v.Cursor()
326
327	y = minInt(y, bt.getTableLength()-1)
328	y = maxInt(y, 0)
329
330	// window is too small to set the cursor properly, ignoring the error
331	_ = v.SetCursor(0, y)
332	bt.selectCursor = y
333
334	return nil
335}
336
337func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
338	_, max := v.Size()
339
340	allIds, err := bt.repo.AllBugIds()
341	if err != nil {
342		return err
343	}
344
345	bt.allIds = allIds
346
347	if bt.pageCursor+max >= len(allIds) {
348		return nil
349	}
350
351	bt.pageCursor += max
352
353	return bt.doPaginate(allIds, max)
354}
355
356func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
357	_, max := v.Size()
358	allIds, err := bt.repo.AllBugIds()
359	if err != nil {
360		return err
361	}
362
363	bt.allIds = allIds
364
365	bt.pageCursor = maxInt(0, bt.pageCursor-max)
366
367	return bt.doPaginate(allIds, max)
368}
369
370func (bt *bugTable) newBug(g *gocui.Gui, v *gocui.View) error {
371	return newBugWithEditor(bt.repo)
372}
373
374func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
375	_, y := v.Cursor()
376	ui.showBug.bug = bt.bugs[bt.pageCursor+y]
377	return ui.activateWindow(ui.showBug)
378}