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.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}