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}