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