1package termui
2
3import (
4 "bytes"
5 "fmt"
6 "strings"
7
8 "github.com/MichaelMure/git-bug/bug/operations"
9 "github.com/MichaelMure/git-bug/cache"
10 "github.com/MichaelMure/git-bug/util"
11 "github.com/jroimartin/gocui"
12)
13
14const showBugView = "showBugView"
15const showBugSidebarView = "showBugSidebarView"
16const showBugInstructionView = "showBugInstructionView"
17const showBugHeaderView = "showBugHeaderView"
18
19const timeLayout = "Jan 2 2006"
20
21type showBug struct {
22 cache cache.RepoCacher
23 bug cache.BugCacher
24 childViews []string
25 selectableView []string
26 selected string
27 scroll int
28}
29
30func newShowBug(cache cache.RepoCacher) *showBug {
31 return &showBug{
32 cache: cache,
33 }
34}
35
36func (sb *showBug) SetBug(bug cache.BugCacher) {
37 sb.bug = bug
38 sb.scroll = 0
39}
40
41func (sb *showBug) layout(g *gocui.Gui) error {
42 maxX, maxY := g.Size()
43
44 v, err := g.SetView(showBugView, 0, 0, maxX*2/3, maxY-2)
45
46 if err != nil {
47 if err != gocui.ErrUnknownView {
48 return err
49 }
50
51 sb.childViews = append(sb.childViews, showBugView)
52 v.Frame = false
53 }
54
55 v.Clear()
56 err = sb.renderMain(g, v)
57 if err != nil {
58 return err
59 }
60
61 v, err = g.SetView(showBugSidebarView, maxX*2/3+1, 0, maxX-1, maxY-2)
62
63 if err != nil {
64 if err != gocui.ErrUnknownView {
65 return err
66 }
67
68 sb.childViews = append(sb.childViews, showBugSidebarView)
69 v.Frame = true
70 }
71
72 v.Clear()
73 sb.renderSidebar(v)
74
75 v, err = g.SetView(showBugInstructionView, -1, maxY-2, maxX, maxY)
76
77 if err != nil {
78 if err != gocui.ErrUnknownView {
79 return err
80 }
81
82 sb.childViews = append(sb.childViews, showBugInstructionView)
83 v.Frame = false
84 v.BgColor = gocui.ColorBlue
85
86 fmt.Fprintf(v, "[q] Save and return [c] Comment [t] Change title [↓,j] Down [↑,k] Up")
87 }
88
89 _, err = g.SetCurrentView(showBugView)
90 return err
91}
92
93func (sb *showBug) keybindings(g *gocui.Gui) error {
94 // Return
95 if err := g.SetKeybinding(showBugView, 'q', gocui.ModNone, sb.saveAndBack); err != nil {
96 return err
97 }
98
99 // Scrolling
100 if err := g.SetKeybinding(showBugView, gocui.KeyPgup, gocui.ModNone,
101 sb.scrollUp); err != nil {
102 return err
103 }
104 if err := g.SetKeybinding(showBugView, gocui.KeyPgdn, gocui.ModNone,
105 sb.scrollDown); err != nil {
106 return err
107 }
108
109 // Down
110 if err := g.SetKeybinding(showBugView, 'j', gocui.ModNone,
111 sb.selectNext); err != nil {
112 return err
113 }
114 if err := g.SetKeybinding(showBugView, gocui.KeyArrowDown, gocui.ModNone,
115 sb.selectNext); err != nil {
116 return err
117 }
118 // Up
119 if err := g.SetKeybinding(showBugView, 'k', gocui.ModNone,
120 sb.selectPrevious); err != nil {
121 return err
122 }
123 if err := g.SetKeybinding(showBugView, gocui.KeyArrowUp, gocui.ModNone,
124 sb.selectPrevious); err != nil {
125 return err
126 }
127
128 // Comment
129 if err := g.SetKeybinding(showBugView, 'c', gocui.ModNone,
130 sb.comment); err != nil {
131 return err
132 }
133
134 // Title
135 if err := g.SetKeybinding(showBugView, 't', gocui.ModNone,
136 sb.setTitle); err != nil {
137 return err
138 }
139
140 // Labels
141
142 return nil
143}
144
145func (sb *showBug) disable(g *gocui.Gui) error {
146 for _, view := range sb.childViews {
147 if err := g.DeleteView(view); err != nil {
148 return err
149 }
150 }
151 return nil
152}
153
154func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
155 maxX, _ := mainView.Size()
156 x0, y0, _, _, _ := g.ViewPosition(mainView.Name())
157
158 y0 -= sb.scroll
159
160 snap := sb.bug.Snapshot()
161
162 sb.childViews = nil
163 sb.selectableView = nil
164
165 bugHeader := fmt.Sprintf("[ %s ] %s\n\n[ %s ] %s opened this bug on %s",
166 util.Cyan(snap.HumanId()),
167 util.Bold(snap.Title),
168 util.Yellow(snap.Status),
169 util.Magenta(snap.Author.Name),
170 snap.CreatedAt.Format(timeLayout),
171 )
172 bugHeader, lines := util.TextWrap(bugHeader, maxX)
173
174 v, err := sb.createOpView(g, showBugHeaderView, x0, y0, maxX+1, lines, false)
175 if err != nil {
176 return err
177 }
178
179 fmt.Fprint(v, bugHeader)
180 y0 += lines + 1
181
182 for i, op := range snap.Operations {
183 viewName := fmt.Sprintf("op%d", i)
184
185 // TODO: me might skip the rendering of blocks that are outside of the view
186 // but to do that we need to rework how sb.selectableView is maintained
187
188 switch op.(type) {
189
190 case operations.CreateOperation:
191 create := op.(operations.CreateOperation)
192 content, lines := util.TextWrapPadded(create.Message, maxX, 4)
193
194 v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
195 if err != nil {
196 return err
197 }
198 fmt.Fprint(v, content)
199 y0 += lines + 2
200
201 case operations.AddCommentOperation:
202 comment := op.(operations.AddCommentOperation)
203
204 message, _ := util.TextWrapPadded(comment.Message, maxX, 4)
205 content := fmt.Sprintf("%s commented on %s\n\n%s",
206 util.Magenta(comment.Author.Name),
207 comment.Time().Format(timeLayout),
208 message,
209 )
210 content, lines = util.TextWrap(content, maxX)
211
212 v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
213 if err != nil {
214 return err
215 }
216 fmt.Fprint(v, content)
217 y0 += lines + 2
218
219 case operations.SetTitleOperation:
220 setTitle := op.(operations.SetTitleOperation)
221
222 content := fmt.Sprintf("%s changed the title to %s on %s",
223 util.Magenta(setTitle.Author.Name),
224 util.Bold(setTitle.Title),
225 setTitle.Time().Format(timeLayout),
226 )
227 content, lines := util.TextWrap(content, maxX)
228
229 v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
230 if err != nil {
231 return err
232 }
233 fmt.Fprint(v, content)
234 y0 += lines + 2
235
236 case operations.SetStatusOperation:
237 setStatus := op.(operations.SetStatusOperation)
238
239 content := fmt.Sprintf("%s %s the bug on %s",
240 util.Magenta(setStatus.Author.Name),
241 util.Bold(setStatus.Status.Action()),
242 setStatus.Time().Format(timeLayout),
243 )
244 content, lines := util.TextWrap(content, maxX)
245
246 v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
247 if err != nil {
248 return err
249 }
250 fmt.Fprint(v, content)
251 y0 += lines + 2
252
253 case operations.LabelChangeOperation:
254 labelChange := op.(operations.LabelChangeOperation)
255
256 var added []string
257 for _, label := range labelChange.Added {
258 added = append(added, util.Bold("\""+label+"\""))
259 }
260
261 var removed []string
262 for _, label := range labelChange.Removed {
263 removed = append(removed, util.Bold("\""+label+"\""))
264 }
265
266 var action bytes.Buffer
267
268 if len(added) > 0 {
269 action.WriteString("added ")
270 action.WriteString(strings.Join(added, " "))
271
272 if len(removed) > 0 {
273 action.WriteString(" and ")
274 }
275 }
276
277 if len(removed) > 0 {
278 action.WriteString("removed ")
279 action.WriteString(strings.Join(removed, " "))
280 }
281
282 if len(added)+len(removed) > 1 {
283 action.WriteString(" labels")
284 } else {
285 action.WriteString(" label")
286 }
287
288 content := fmt.Sprintf("%s %s on %s",
289 util.Magenta(labelChange.Author.Name),
290 action.String(),
291 labelChange.Time().Format(timeLayout),
292 )
293 content, lines := util.TextWrap(content, maxX)
294
295 v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
296 if err != nil {
297 return err
298 }
299 fmt.Fprint(v, content)
300 y0 += lines + 2
301 }
302 }
303
304 return nil
305}
306
307func (sb *showBug) createOpView(g *gocui.Gui, name string, x0 int, y0 int, maxX int, height int, selectable bool) (*gocui.View, error) {
308 v, err := g.SetView(name, x0, y0, maxX, y0+height+1)
309
310 if err != nil && err != gocui.ErrUnknownView {
311 return nil, err
312 }
313
314 sb.childViews = append(sb.childViews, name)
315
316 if selectable {
317 sb.selectableView = append(sb.selectableView, name)
318 }
319
320 v.Frame = sb.selected == name
321
322 v.Clear()
323
324 return v, nil
325}
326
327func (sb *showBug) renderSidebar(v *gocui.View) {
328 maxX, _ := v.Size()
329 snap := sb.bug.Snapshot()
330
331 title := util.LeftPaddedString("LABEL", maxX, 2)
332 fmt.Fprintf(v, title+"\n\n")
333
334 for _, label := range snap.Labels {
335 fmt.Fprintf(v, util.LeftPaddedString(label.String(), maxX, 2))
336 fmt.Fprintln(v)
337 }
338}
339
340func (sb *showBug) saveAndBack(g *gocui.Gui, v *gocui.View) error {
341 err := sb.bug.CommitAsNeeded()
342 if err != nil {
343 return err
344 }
345 ui.activateWindow(ui.bugTable)
346 return nil
347}
348
349func (sb *showBug) scrollUp(g *gocui.Gui, v *gocui.View) error {
350 mainView, err := g.View(showBugView)
351 if err != nil {
352 return err
353 }
354
355 _, maxY := mainView.Size()
356
357 sb.scroll -= maxY / 2
358
359 sb.scroll = maxInt(sb.scroll, 0)
360
361 return nil
362}
363
364func (sb *showBug) scrollDown(g *gocui.Gui, v *gocui.View) error {
365 _, maxY := v.Size()
366
367 lastViewName := sb.childViews[len(sb.childViews)-1]
368
369 lastView, err := g.View(lastViewName)
370 if err != nil {
371 return err
372 }
373
374 _, vMaxY := lastView.Size()
375
376 _, vy0, _, _, err := g.ViewPosition(lastViewName)
377 if err != nil {
378 return err
379 }
380
381 maxScroll := vy0 + sb.scroll + vMaxY - maxY
382
383 sb.scroll += maxY / 2
384
385 sb.scroll = minInt(sb.scroll, maxScroll)
386
387 return nil
388}
389
390func (sb *showBug) selectPrevious(g *gocui.Gui, v *gocui.View) error {
391 if len(sb.selectableView) == 0 {
392 return nil
393 }
394
395 defer sb.focusView(g)
396
397 for i, name := range sb.selectableView {
398 if name == sb.selected {
399 // special case to scroll up to the top
400 if i == 0 {
401 sb.scroll = 0
402 }
403
404 sb.selected = sb.selectableView[maxInt(i-1, 0)]
405 return nil
406 }
407 }
408
409 if sb.selected == "" {
410 sb.selected = sb.selectableView[0]
411 }
412
413 return nil
414}
415
416func (sb *showBug) selectNext(g *gocui.Gui, v *gocui.View) error {
417 if len(sb.selectableView) == 0 {
418 return nil
419 }
420
421 defer sb.focusView(g)
422
423 for i, name := range sb.selectableView {
424 if name == sb.selected {
425 sb.selected = sb.selectableView[minInt(i+1, len(sb.selectableView)-1)]
426 return nil
427 }
428 }
429
430 if sb.selected == "" {
431 sb.selected = sb.selectableView[0]
432 }
433
434 return nil
435}
436
437func (sb *showBug) focusView(g *gocui.Gui) error {
438 mainView, err := g.View(showBugView)
439 if err != nil {
440 return err
441 }
442
443 _, maxY := mainView.Size()
444
445 _, vy0, _, _, err := g.ViewPosition(sb.selected)
446 if err != nil {
447 return err
448 }
449
450 v, err := g.View(sb.selected)
451 if err != nil {
452 return err
453 }
454
455 _, vMaxY := v.Size()
456
457 vy1 := vy0 + vMaxY
458
459 if vy0 < 0 {
460 sb.scroll += vy0
461 return nil
462 }
463
464 if vy1 > maxY {
465 sb.scroll -= maxY - vy1
466 }
467
468 return nil
469}
470
471func (sb *showBug) comment(g *gocui.Gui, v *gocui.View) error {
472 return addCommentWithEditor(sb.bug)
473}
474
475func (sb *showBug) setTitle(g *gocui.Gui, v *gocui.View) error {
476 return setTitleWithEditor(sb.bug)
477}