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