aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cache/cache.go19
-rw-r--r--commands/comment.go2
-rw-r--r--commands/new.go2
-rw-r--r--commands/root.go1
-rw-r--r--input/input.go10
-rw-r--r--termui/bug_table.go257
-rw-r--r--termui/termui.go153
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
}