diff options
author | Michael Muré <batolettre@gmail.com> | 2018-07-31 16:43:43 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2018-07-31 16:44:23 +0200 |
commit | 87669e0f18f282854d340a676834b939e34e5ed3 (patch) | |
tree | c78eaa155d2939d6647ab814c6710d0d3ed69a6e | |
parent | eb39c5c29bc0e9b5e15a940a1b71bdac688b6535 (diff) | |
download | git-bug-87669e0f18f282854d340a676834b939e34e5ed3.tar.gz |
termui: use the editor to create a new bug
-rw-r--r-- | cache/cache.go | 19 | ||||
-rw-r--r-- | commands/comment.go | 2 | ||||
-rw-r--r-- | commands/new.go | 2 | ||||
-rw-r--r-- | commands/root.go | 1 | ||||
-rw-r--r-- | input/input.go | 10 | ||||
-rw-r--r-- | termui/bug_table.go | 257 | ||||
-rw-r--r-- | termui/termui.go | 153 |
7 files changed, 271 insertions, 173 deletions
diff --git a/cache/cache.go b/cache/cache.go index 2813b150..b46ea83f 100644 --- a/cache/cache.go +++ b/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 } diff --git a/commands/comment.go b/commands/comment.go index cebf729c..0a76e4ce 100644 --- a/commands/comment.go +++ b/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 diff --git a/commands/new.go b/commands/new.go index 88e7cd81..a13e36bf 100644 --- a/commands/new.go +++ b/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.") diff --git a/commands/root.go b/commands/root.go index 54368fc7..cee39083 100644 --- a/commands/root.go +++ b/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 diff --git a/input/input.go b/input/input.go index 49d3501d..a1a2e885 100644 --- a/input/input.go +++ b/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 diff --git a/termui/bug_table.go b/termui/bug_table.go index 89f43c87..264dff2d 100644 --- a/termui/bug_table.go +++ b/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) } diff --git a/termui/termui.go b/termui/termui.go index e2e5ae24..9ac82fd3 100644 --- a/termui/termui.go +++ b/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 } |