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