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 cache cache.RepoCacher
18 allIds []string
19 bugs []*bug.Snapshot
20 pageCursor int
21 selectCursor int
22}
23
24func newBugTable(cache cache.RepoCacher) *bugTable {
25 return &bugTable{
26 cache: 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 newBugWithEditor); 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.cache.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 = []*bug.Snapshot{}
217 return nil
218 }
219
220 // slice the data
221 ids := allIds[bt.pageCursor : bt.pageCursor+nb]
222
223 bt.bugs = make([]*bug.Snapshot, len(ids))
224
225 for i, id := range ids {
226 b, err := bt.cache.ResolveBug(id)
227 if err != nil {
228 return err
229 }
230
231 bt.bugs[i] = b.Snapshot()
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 if len(b.Comments) > 0 {
263 create := b.Comments[0]
264 person = create.Author
265 }
266
267 id := util.LeftPaddedString(b.HumanId(), columnWidths["id"], 2)
268 status := util.LeftPaddedString(b.Status.String(), columnWidths["status"], 2)
269 title := util.LeftPaddedString(b.Title, columnWidths["title"], 2)
270 author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
271 summary := util.LeftPaddedString(b.Summary(), columnWidths["summary"], 2)
272
273 fmt.Fprintf(v, "%s %s %s %s %s\n", id, status, title, author, summary)
274 }
275}
276
277func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
278 columnWidths := bt.getColumnWidths(maxX)
279
280 id := util.LeftPaddedString("ID", columnWidths["id"], 2)
281 status := util.LeftPaddedString("STATUS", columnWidths["status"], 2)
282 title := util.LeftPaddedString("TITLE", columnWidths["title"], 2)
283 author := util.LeftPaddedString("AUTHOR", columnWidths["author"], 2)
284 summary := util.LeftPaddedString("SUMMARY", columnWidths["summary"], 2)
285
286 fmt.Fprintf(v, "\n")
287 fmt.Fprintf(v, "%s %s %s %s %s\n", id, status, title, author, summary)
288
289}
290
291func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
292 fmt.Fprintf(v, " \nShowing %d of %d bugs", len(bt.bugs), len(bt.allIds))
293}
294
295func (bt *bugTable) cursorDown(g *gocui.Gui, v *gocui.View) error {
296 _, y := v.Cursor()
297 y = minInt(y+1, bt.getTableLength()-1)
298
299 // window is too small to set the cursor properly, ignoring the error
300 _ = v.SetCursor(0, y)
301 bt.selectCursor = y
302
303 return nil
304}
305
306func (bt *bugTable) cursorUp(g *gocui.Gui, v *gocui.View) error {
307 _, y := v.Cursor()
308 y = maxInt(y-1, 0)
309
310 // window is too small to set the cursor properly, ignoring the error
311 _ = v.SetCursor(0, y)
312 bt.selectCursor = y
313
314 return nil
315}
316
317func (bt *bugTable) cursorClamp(v *gocui.View) error {
318 _, y := v.Cursor()
319
320 y = minInt(y, bt.getTableLength()-1)
321 y = maxInt(y, 0)
322
323 // window is too small to set the cursor properly, ignoring the error
324 _ = v.SetCursor(0, y)
325 bt.selectCursor = y
326
327 return nil
328}
329
330func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
331 _, max := v.Size()
332
333 allIds, err := bt.cache.AllBugIds()
334 if err != nil {
335 return err
336 }
337
338 bt.allIds = allIds
339
340 if bt.pageCursor+max >= len(allIds) {
341 return nil
342 }
343
344 bt.pageCursor += max
345
346 return bt.doPaginate(allIds, max)
347}
348
349func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
350 _, max := v.Size()
351 allIds, err := bt.cache.AllBugIds()
352 if err != nil {
353 return err
354 }
355
356 bt.allIds = allIds
357
358 bt.pageCursor = maxInt(0, bt.pageCursor-max)
359
360 return bt.doPaginate(allIds, max)
361}
362
363func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error {
364 _, y := v.Cursor()
365 ui.showBug.bug = bt.bugs[bt.pageCursor+y]
366 return ui.activateWindow(ui.showBug)
367}