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