show_bug.go

  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}