termui: use the editor to create a new bug

Michael Muré created

Change summary

cache/cache.go      |  19 ++-
commands/comment.go |   2 
commands/new.go     |   2 
commands/root.go    |   1 
input/input.go      |  10 +
termui/bug_table.go | 257 ++++++++++++++++++++++++++++++++--------------
termui/termui.go    | 153 +++++++++++++--------------
7 files changed, 271 insertions(+), 173 deletions(-)

Detailed changes

cache/cache.go 🔗

@@ -22,6 +22,7 @@ type Cacher interface {
 }
 
 type RepoCacher interface {
+	Repository() repository.Repo
 	ResolveBug(id string) (BugCacher, error)
 	ResolveBugPrefix(prefix string) (BugCacher, error)
 	AllBugIds() ([]string, error)
@@ -111,7 +112,11 @@ func NewRepoCache(r repository.Repo) RepoCacher {
 	}
 }
 
-func (c RepoCache) ResolveBug(id string) (BugCacher, error) {
+func (c *RepoCache) Repository() repository.Repo {
+	return c.repo
+}
+
+func (c *RepoCache) ResolveBug(id string) (BugCacher, error) {
 	cached, ok := c.bugs[id]
 	if ok {
 		return cached, nil
@@ -128,7 +133,7 @@ func (c RepoCache) ResolveBug(id string) (BugCacher, error) {
 	return cached, nil
 }
 
-func (c RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) {
+func (c *RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) {
 	// preallocate but empty
 	matching := make([]string, 0, 5)
 
@@ -161,15 +166,15 @@ func (c RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) {
 	return cached, nil
 }
 
-func (c RepoCache) AllBugIds() ([]string, error) {
+func (c *RepoCache) AllBugIds() ([]string, error) {
 	return bug.ListLocalIds(c.repo)
 }
 
-func (c RepoCache) ClearAllBugs() {
+func (c *RepoCache) ClearAllBugs() {
 	c.bugs = make(map[string]BugCacher)
 }
 
-func (c RepoCache) NewBug(title string, message string) (BugCacher, error) {
+func (c *RepoCache) NewBug(title string, message string) (BugCacher, error) {
 	author, err := bug.GetUser(c.repo)
 	if err != nil {
 		return nil, err
@@ -204,7 +209,7 @@ func NewBugCache(b *bug.Bug) BugCacher {
 	}
 }
 
-func (c BugCache) Snapshot() *bug.Snapshot {
+func (c *BugCache) Snapshot() *bug.Snapshot {
 	if c.snap == nil {
 		snap := c.bug.Compile()
 		c.snap = &snap
@@ -212,6 +217,6 @@ func (c BugCache) Snapshot() *bug.Snapshot {
 	return c.snap
 }
 
-func (c BugCache) ClearSnapshot() {
+func (c *BugCache) ClearSnapshot() {
 	c.snap = nil
 }

commands/comment.go 🔗

@@ -35,7 +35,7 @@ func runComment(cmd *cobra.Command, args []string) error {
 	}
 
 	if commentMessage == "" {
-		commentMessage, err = input.BugCommentEditorInput(repo, messageFilename)
+		commentMessage, err = input.BugCommentEditorInput(repo)
 		if err == input.ErrEmptyMessage {
 			fmt.Println("Empty message, aborting.")
 			return nil

commands/new.go 🔗

@@ -25,7 +25,7 @@ func runNewBug(cmd *cobra.Command, args []string) error {
 	}
 
 	if newMessage == "" || newTitle == "" {
-		newTitle, newMessage, err = input.BugCreateEditorInput(repo, messageFilename, newTitle, newMessage)
+		newTitle, newMessage, err = input.BugCreateEditorInput(repo, newTitle, newMessage)
 
 		if err == input.ErrEmptyTitle {
 			fmt.Println("Empty title, aborting.")

commands/root.go 🔗

@@ -12,7 +12,6 @@ import (
 // It's used to avoid cobra to split the Use string at the first space to get the root command name
 //const rootCommandName = "git\u00A0bug"
 const rootCommandName = "git-bug"
-const messageFilename = "BUG_MESSAGE_EDITMSG"
 
 // package scoped var to hold the repo after the PreRun execution
 var repo repository.Repo

input/input.go 🔗

@@ -14,6 +14,8 @@ import (
 	"strings"
 )
 
+const messageFilename = "BUG_MESSAGE_EDITMSG"
+
 var ErrEmptyMessage = errors.New("empty message")
 var ErrEmptyTitle = errors.New("empty title")
 
@@ -24,14 +26,14 @@ const bugTitleCommentTemplate = `%s%s
 # An empty title aborts the operation.
 `
 
-func BugCreateEditorInput(repo repository.Repo, fileName string, preTitle string, preMessage string) (string, string, error) {
+func BugCreateEditorInput(repo repository.Repo, preTitle string, preMessage string) (string, string, error) {
 	if preMessage != "" {
 		preMessage = "\n\n" + preMessage
 	}
 
 	template := fmt.Sprintf(bugTitleCommentTemplate, preTitle, preMessage)
 
-	raw, err := LaunchEditorWithTemplate(repo, fileName, template)
+	raw, err := LaunchEditorWithTemplate(repo, messageFilename, template)
 
 	if err != nil {
 		return "", "", err
@@ -73,8 +75,8 @@ const bugCommentTemplate = `
 # and an empty message aborts the operation.
 `
 
-func BugCommentEditorInput(repo repository.Repo, fileName string) (string, error) {
-	raw, err := LaunchEditorWithTemplate(repo, fileName, bugCommentTemplate)
+func BugCommentEditorInput(repo repository.Repo) (string, error) {
+	raw, err := LaunchEditorWithTemplate(repo, messageFilename, bugCommentTemplate)
 
 	if err != nil {
 		return "", err

termui/bug_table.go 🔗

@@ -8,6 +8,8 @@ import (
 	"github.com/jroimartin/gocui"
 )
 
+const bugTableView = "bugTableView"
+
 type bugTable struct {
 	cache  cache.RepoCacher
 	allIds []string
@@ -22,71 +24,6 @@ func newBugTable(cache cache.RepoCacher) *bugTable {
 	}
 }
 
-func (bt *bugTable) paginate(max int) error {
-	allIds, err := bt.cache.AllBugIds()
-	if err != nil {
-		return err
-	}
-
-	bt.allIds = allIds
-
-	return bt.doPaginate(allIds, max)
-}
-
-func (bt *bugTable) nextPage(max int) error {
-	allIds, err := bt.cache.AllBugIds()
-	if err != nil {
-		return err
-	}
-
-	bt.allIds = allIds
-
-	if bt.cursor+max >= len(allIds) {
-		return nil
-	}
-
-	bt.cursor += max
-
-	return bt.doPaginate(allIds, max)
-}
-
-func (bt *bugTable) previousPage(max int) error {
-	allIds, err := bt.cache.AllBugIds()
-	if err != nil {
-		return err
-	}
-
-	bt.allIds = allIds
-
-	bt.cursor = maxInt(0, bt.cursor-max)
-
-	return bt.doPaginate(allIds, max)
-}
-
-func (bt *bugTable) doPaginate(allIds []string, max int) error {
-	// clamp the cursor
-	bt.cursor = maxInt(bt.cursor, 0)
-	bt.cursor = minInt(bt.cursor, len(allIds)-1)
-
-	nb := minInt(len(allIds)-bt.cursor, max)
-
-	// slice the data
-	ids := allIds[bt.cursor : bt.cursor+nb]
-
-	bt.bugs = make([]*bug.Snapshot, len(ids))
-
-	for i, id := range ids {
-		b, err := bt.cache.ResolveBug(id)
-		if err != nil {
-			return err
-		}
-
-		bt.bugs[i] = b.Snapshot()
-	}
-
-	return nil
-}
-
 func (bt *bugTable) layout(g *gocui.Gui) error {
 	maxX, maxY := g.Size()
 
@@ -101,9 +38,9 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 	}
 
 	v.Clear()
-	ui.bugTable.renderHeader(v, maxX)
+	bt.renderHeader(v, maxX)
 
-	v, err = g.SetView("bugTable", -1, 1, maxX, maxY-2)
+	v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-2)
 
 	if err != nil {
 		if err != gocui.ErrUnknownView {
@@ -115,21 +52,26 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 		v.SelBgColor = gocui.ColorWhite
 		v.SelFgColor = gocui.ColorBlack
 
-		_, err = g.SetCurrentView("bugTable")
+		_, err = g.SetCurrentView(bugTableView)
 
 		if err != nil {
 			return err
 		}
 	}
 
-	_, tableHeight := v.Size()
-	err = bt.paginate(tableHeight)
+	_, viewHeight := v.Size()
+	err = bt.paginate(viewHeight - 1)
+	if err != nil {
+		return err
+	}
+
+	err = bt.cursorClamp(v)
 	if err != nil {
 		return err
 	}
 
 	v.Clear()
-	ui.bugTable.render(v, maxX)
+	bt.render(v, maxX)
 
 	v, err = g.SetView("footer", -1, maxY-3, maxX, maxY)
 
@@ -142,7 +84,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 	}
 
 	v.Clear()
-	ui.bugTable.renderFooter(v, maxX)
+	bt.renderFooter(v, maxX)
 
 	v, err = g.SetView("instructions", -1, maxY-2, maxX, maxY)
 
@@ -154,7 +96,103 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 		v.Frame = false
 		v.BgColor = gocui.ColorBlue
 
-		fmt.Fprintf(v, "[q] Quit [h] Previous page [j] Down [k] Up [l] Next page [enter] Open bug")
+		fmt.Fprintf(v, "[q] Quit [←,h] Previous page [↓,j] Down [↑,k] Up [→,l] Next page [enter] Open bug [n] New bug")
+	}
+
+	return nil
+}
+
+func (bt *bugTable) keybindings(g *gocui.Gui) error {
+	// Quit
+	if err := g.SetKeybinding(bugTableView, 'q', gocui.ModNone, quit); err != nil {
+		return err
+	}
+
+	// Down
+	if err := g.SetKeybinding(bugTableView, 'j', gocui.ModNone,
+		bt.cursorDown); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowDown, gocui.ModNone,
+		bt.cursorDown); err != nil {
+		return err
+	}
+	// Up
+	if err := g.SetKeybinding(bugTableView, 'k', gocui.ModNone,
+		bt.cursorUp); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowUp, gocui.ModNone,
+		bt.cursorUp); err != nil {
+		return err
+	}
+
+	// Previous page
+	if err := g.SetKeybinding(bugTableView, 'h', gocui.ModNone,
+		bt.previousPage); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowLeft, gocui.ModNone,
+		bt.previousPage); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyPgup, gocui.ModNone,
+		bt.previousPage); err != nil {
+		return err
+	}
+	// Next page
+	if err := g.SetKeybinding(bugTableView, 'l', gocui.ModNone,
+		bt.nextPage); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyArrowRight, gocui.ModNone,
+		bt.nextPage); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(bugTableView, gocui.KeyPgdn, gocui.ModNone,
+		bt.nextPage); err != nil {
+		return err
+	}
+
+	// New bug
+	if err := g.SetKeybinding(bugTableView, 'n', gocui.ModNone,
+		newBugWithEditor); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (bt *bugTable) paginate(max int) error {
+	allIds, err := bt.cache.AllBugIds()
+	if err != nil {
+		return err
+	}
+
+	bt.allIds = allIds
+
+	return bt.doPaginate(allIds, max)
+}
+
+func (bt *bugTable) doPaginate(allIds []string, max int) error {
+	// clamp the cursor
+	bt.cursor = maxInt(bt.cursor, 0)
+	bt.cursor = minInt(bt.cursor, len(allIds)-1)
+
+	nb := minInt(len(allIds)-bt.cursor, max)
+
+	// slice the data
+	ids := allIds[bt.cursor : bt.cursor+nb]
+
+	bt.bugs = make([]*bug.Snapshot, len(ids))
+
+	for i, id := range ids {
+		b, err := bt.cache.ResolveBug(id)
+		if err != nil {
+			return err
+		}
+
+		bt.bugs[i] = b.Snapshot()
 	}
 
 	return nil
@@ -218,16 +256,73 @@ func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
 	fmt.Fprintf(v, "Showing %d of %d bugs", len(bt.bugs), len(bt.allIds))
 }
 
-func maxInt(a, b int) int {
-	if a > b {
-		return a
+func (bt *bugTable) cursorDown(g *gocui.Gui, v *gocui.View) error {
+	_, y := v.Cursor()
+	y = minInt(y+1, bt.getTableLength()-1)
+
+	err := v.SetCursor(0, y)
+	if err != nil {
+		return err
 	}
-	return b
+
+	return nil
+}
+
+func (bt *bugTable) cursorUp(g *gocui.Gui, v *gocui.View) error {
+	_, y := v.Cursor()
+	y = maxInt(y-1, 0)
+
+	err := v.SetCursor(0, y)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
-func minInt(a, b int) int {
-	if a > b {
-		return b
+func (bt *bugTable) cursorClamp(v *gocui.View) error {
+	_, y := v.Cursor()
+
+	y = minInt(y, bt.getTableLength()-1)
+	y = maxInt(y, 0)
+
+	err := v.SetCursor(0, y)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (bt *bugTable) nextPage(g *gocui.Gui, v *gocui.View) error {
+	_, max := v.Size()
+
+	allIds, err := bt.cache.AllBugIds()
+	if err != nil {
+		return err
 	}
-	return a
+
+	bt.allIds = allIds
+
+	if bt.cursor+max >= len(allIds) {
+		return nil
+	}
+
+	bt.cursor += max
+
+	return bt.doPaginate(allIds, max)
+}
+
+func (bt *bugTable) previousPage(g *gocui.Gui, v *gocui.View) error {
+	_, max := v.Size()
+	allIds, err := bt.cache.AllBugIds()
+	if err != nil {
+		return err
+	}
+
+	bt.allIds = allIds
+
+	bt.cursor = maxInt(0, bt.cursor-max)
+
+	return bt.doPaginate(allIds, max)
 }

termui/termui.go 🔗

@@ -2,104 +2,103 @@ package termui
 
 import (
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/jroimartin/gocui"
+	"github.com/pkg/errors"
 )
 
+var errTerminateMainloop = errors.New("terminate gocui mainloop")
+
 type termUI struct {
-	cache    cache.RepoCacher
+	g            *gocui.Gui
+	gError       chan error
+	cache        cache.RepoCacher
+	activeWindow window
+
 	bugTable *bugTable
 }
 
 var ui *termUI
 
+type window interface {
+	keybindings(g *gocui.Gui) error
+	layout(g *gocui.Gui) error
+}
+
 func Run(repo repository.Repo) error {
 	c := cache.NewRepoCache(repo)
 
 	ui = &termUI{
+		gError:   make(chan error, 1),
 		cache:    c,
 		bugTable: newBugTable(c),
 	}
 
+	ui.activeWindow = ui.bugTable
+
+	initGui()
+
+	err := <-ui.gError
+
+	if err != nil && err != gocui.ErrQuit {
+		return err
+	}
+
+	return nil
+}
+
+func initGui() {
 	g, err := gocui.NewGui(gocui.OutputNormal)
 
 	if err != nil {
-		return err
+		ui.gError <- err
+		return
 	}
 
-	defer g.Close()
+	ui.g = g
 
-	g.SetManagerFunc(layout)
+	ui.g.SetManagerFunc(layout)
 
-	err = keybindings(g)
+	err = keybindings(ui.g)
 
 	if err != nil {
-		return err
+		ui.g.Close()
+		ui.gError <- err
+		return
 	}
 
 	err = g.MainLoop()
 
-	if err != nil && err != gocui.ErrQuit {
-		return err
+	if err != nil && err != errTerminateMainloop {
+		ui.g.Close()
+		ui.gError <- err
 	}
 
-	return nil
+	return
 }
 
 func layout(g *gocui.Gui) error {
 	//maxX, maxY := g.Size()
 
-	ui.bugTable.layout(g)
+	g.Cursor = false
 
-	v, err := g.View("bugTable")
-	if err != nil {
+	if err := ui.activeWindow.layout(g); err != nil {
 		return err
 	}
 
-	cursorClamp(v)
-
 	return nil
 }
 
 func keybindings(g *gocui.Gui) error {
-	if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", 'j', gocui.ModNone, cursorDown); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", 'k', gocui.ModNone, cursorUp); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
+	// Quit
+	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
 		return err
 	}
 
-	if err := g.SetKeybinding("bugTable", 'h', gocui.ModNone, previousPage); err != nil {
+	if err := ui.bugTable.keybindings(g); err != nil {
 		return err
 	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyArrowLeft, gocui.ModNone, previousPage); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyPgup, gocui.ModNone, previousPage); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", 'l', gocui.ModNone, nextPage); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyArrowRight, gocui.ModNone, nextPage); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding("bugTable", gocui.KeyPgdn, gocui.ModNone, nextPage); err != nil {
-		return err
-	}
-
-	//err = g.SetKeybinding("bugTable", 'p', gocui.ModNone, playSelected)
-	//err = g.SetKeybinding("bugTable", gocui.KeyEnter, gocui.ModNone, playSelectedAndExit)
-	//err = g.SetKeybinding("bugTable", 'm', gocui.ModNone, loadNextRecords)
 
 	return nil
 }
@@ -108,50 +107,48 @@ func quit(g *gocui.Gui, v *gocui.View) error {
 	return gocui.ErrQuit
 }
 
-func cursorDown(g *gocui.Gui, v *gocui.View) error {
-	_, y := v.Cursor()
-	y = minInt(y+1, ui.bugTable.getTableLength()-1)
-
-	err := v.SetCursor(0, y)
-	if err != nil {
-		return err
-	}
+func newBugWithEditor(g *gocui.Gui, v *gocui.View) error {
+	// This is somewhat hacky.
+	// As there is no way to pause gocui, run the editor, restart gocui,
+	// we have to stop it entirely and start a new one later.
+	//
+	// - an error channel is used to route the returned error of this new
+	// 		instance into the original launch function
+	// - a custom error (errTerminateMainloop) is used to terminate the original
+	//		instance's mainLoop. This error is then filtered.
 
-	return nil
-}
+	ui.g.Close()
 
-func cursorUp(g *gocui.Gui, v *gocui.View) error {
-	_, y := v.Cursor()
-	y = maxInt(y-1, 0)
+	title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
 
-	err := v.SetCursor(0, y)
+	if err == input.ErrEmptyTitle {
+		// TODO: display proper error
+		return err
+	}
 	if err != nil {
 		return err
 	}
 
-	return nil
-}
-
-func cursorClamp(v *gocui.View) error {
-	_, y := v.Cursor()
-
-	y = minInt(y, ui.bugTable.getTableLength()-1)
-	y = maxInt(y, 0)
-
-	err := v.SetCursor(0, y)
+	_, err = ui.cache.NewBug(title, message)
 	if err != nil {
 		return err
 	}
 
-	return nil
+	initGui()
+
+	return errTerminateMainloop
 }
 
-func nextPage(g *gocui.Gui, v *gocui.View) error {
-	_, maxY := v.Size()
-	return ui.bugTable.nextPage(maxY)
+func maxInt(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
 }
 
-func previousPage(g *gocui.Gui, v *gocui.View) error {
-	_, maxY := v.Size()
-	return ui.bugTable.previousPage(maxY)
+func minInt(a, b int) int {
+	if a > b {
+		return b
+	}
+	return a
 }