show_bug.go

  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}