wip terminal ui

Michael Muré created

Change summary

commands/termui.go          |  21 ++++
doc/bash_completion/git-bug |  21 ++++
doc/man/git-bug-termui.3    |  29 ++++++
doc/man/git-bug.3           |   2 
doc/md/git-bug.md           |   1 
doc/md/git-bug_termui.md    |  22 ++++
doc/zsh_completion/git-bug  |   2 
termui/bug_table.go         | 186 +++++++++++++++++++++++++++++++++++++++
termui/termui.go            | 132 +++++++++++++++++++++++++++
9 files changed, 414 insertions(+), 2 deletions(-)

Detailed changes

commands/termui.go 🔗

@@ -0,0 +1,21 @@
+package commands
+
+import (
+	"github.com/MichaelMure/git-bug/termui"
+	"github.com/spf13/cobra"
+)
+
+func runTermUI(cmd *cobra.Command, args []string) error {
+	//time.Sleep(10 * time.Second)
+	return termui.Run(repo)
+}
+
+var termUICmd = &cobra.Command{
+	Use:   "termui",
+	Short: "Launch the terminal UI",
+	RunE:  runTermUI,
+}
+
+func init() {
+	RootCmd.AddCommand(termUICmd)
+}

doc/bash_completion/git-bug 🔗

@@ -461,6 +461,26 @@ _git-bug_show()
     noun_aliases=()
 }
 
+_git-bug_termui()
+{
+    last_command="git-bug_termui"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
 _git-bug_webui()
 {
     last_command="git-bug_webui"
@@ -501,6 +521,7 @@ _git-bug_root_command()
     commands+=("pull")
     commands+=("push")
     commands+=("show")
+    commands+=("termui")
     commands+=("webui")
 
     flags=()

doc/man/git-bug-termui.3 🔗

@@ -0,0 +1,29 @@
+.TH "MINE" "3" "Jul 2018" "Auto generated by spf13/cobra" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-termui \- Launch the terminal UI
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug termui [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Launch the terminal UI
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for termui
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug(3)\fP

doc/man/git-bug.3 🔗

@@ -29,4 +29,4 @@ It use the same internal storage so it doesn't pollute your project. As you woul
 
 .SH SEE ALSO
 .PP
-\fBgit\-bug\-close(3)\fP, \fBgit\-bug\-commands(3)\fP, \fBgit\-bug\-comment(3)\fP, \fBgit\-bug\-label(3)\fP, \fBgit\-bug\-ls(3)\fP, \fBgit\-bug\-new(3)\fP, \fBgit\-bug\-open(3)\fP, \fBgit\-bug\-pull(3)\fP, \fBgit\-bug\-push(3)\fP, \fBgit\-bug\-show(3)\fP, \fBgit\-bug\-webui(3)\fP
+\fBgit\-bug\-close(3)\fP, \fBgit\-bug\-commands(3)\fP, \fBgit\-bug\-comment(3)\fP, \fBgit\-bug\-label(3)\fP, \fBgit\-bug\-ls(3)\fP, \fBgit\-bug\-new(3)\fP, \fBgit\-bug\-open(3)\fP, \fBgit\-bug\-pull(3)\fP, \fBgit\-bug\-push(3)\fP, \fBgit\-bug\-show(3)\fP, \fBgit\-bug\-termui(3)\fP, \fBgit\-bug\-webui(3)\fP

doc/md/git-bug.md 🔗

@@ -30,5 +30,6 @@ git-bug [flags]
 * [git-bug pull](git-bug_pull.md)	 - Pull bugs update from a git remote
 * [git-bug push](git-bug_push.md)	 - Push bugs update to a git remote
 * [git-bug show](git-bug_show.md)	 - Display the details of a bug
+* [git-bug termui](git-bug_termui.md)	 - Launch the terminal UI
 * [git-bug webui](git-bug_webui.md)	 - Launch the web UI
 

doc/md/git-bug_termui.md 🔗

@@ -0,0 +1,22 @@
+## git-bug termui
+
+Launch the terminal UI
+
+### Synopsis
+
+Launch the terminal UI
+
+```
+git-bug termui [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for termui
+```
+
+### SEE ALSO
+
+* [git-bug](git-bug.md)	 - A bugtracker embedded in Git
+

doc/zsh_completion/git-bug 🔗

@@ -7,7 +7,7 @@ case $state in
   level1)
     case $words[1] in
       git-bug)
-        _arguments '1: :(close commands comment label ls new open pull push show webui)'
+        _arguments '1: :(close commands comment label ls new open pull push show termui webui)'
       ;;
       *)
         _arguments '*: :_files'

termui/bug_table.go 🔗

@@ -0,0 +1,186 @@
+package termui
+
+import (
+	"fmt"
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/jroimartin/gocui"
+)
+
+type bugTable struct {
+	cache  cache.RepoCacher
+	bugs   []*bug.Snapshot
+	cursor int
+}
+
+func newBugTable(cache cache.RepoCacher) *bugTable {
+	return &bugTable{
+		cache:  cache,
+		cursor: 0,
+	}
+}
+
+func (bt *bugTable) paginate(max int) error {
+	allIds, err := bt.cache.AllBugIds()
+	if err != nil {
+		return err
+	}
+
+	return bt.doPaginate(allIds, max)
+}
+
+func (bt *bugTable) nextPage(max int) error {
+	allIds, err := bt.cache.AllBugIds()
+	if err != nil {
+		return err
+	}
+
+	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.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)
+
+	// slice the data
+	nb := minInt(len(allIds)-bt.cursor, max)
+
+	ids := allIds[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()
+
+	v, err := g.SetView("header", -1, -1, maxX, 3)
+
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = false
+	}
+
+	v.Clear()
+	ui.bugTable.renderHeader(v, maxX)
+
+	v, err = g.SetView("table", -1, 1, maxX, maxY-2)
+
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = false
+		v.Highlight = true
+		v.SelBgColor = gocui.ColorWhite
+		v.SelFgColor = gocui.ColorBlack
+
+		_, err = g.SetCurrentView("table")
+
+		if err != nil {
+			return err
+		}
+	}
+
+	_, tableHeight := v.Size()
+	err = bt.paginate(tableHeight)
+	if err != nil {
+		return err
+	}
+
+	v.Clear()
+	ui.bugTable.render(v, maxX)
+
+	v, err = g.SetView("footer", -1, maxY-3, maxX, maxY)
+
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = false
+	}
+
+	v.Clear()
+	ui.bugTable.renderFooter(v, maxX)
+
+	v, err = g.SetView("instructions", -1, maxY-2, maxX, maxY)
+
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = false
+		v.BgColor = gocui.ColorBlue
+
+		fmt.Fprintf(v, "[q] Quit [h] Go back [j] Down [k] Up [l] Go forward [m] Load Additional [p] Play [enter] Play and Exit")
+	}
+
+	return nil
+}
+
+func (bt *bugTable) getTableLength() int {
+	return len(bt.bugs)
+}
+
+func (bt *bugTable) render(v *gocui.View, maxX int) {
+	for _, b := range bt.bugs {
+		fmt.Fprintln(v, b.Title)
+	}
+}
+
+func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
+	fmt.Fprintf(v, "header")
+
+}
+
+func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
+	fmt.Fprintf(v, "footer")
+}
+
+func maxInt(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+func minInt(a, b int) int {
+	if a > b {
+		return b
+	}
+	return a
+}

termui/termui.go 🔗

@@ -0,0 +1,132 @@
+package termui
+
+import (
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/jroimartin/gocui"
+)
+
+type termUI struct {
+	cache    cache.RepoCacher
+	bugTable *bugTable
+}
+
+var ui *termUI
+
+func Run(repo repository.Repo) error {
+	c := cache.NewRepoCache(repo)
+
+	ui = &termUI{
+		cache:    c,
+		bugTable: newBugTable(c),
+	}
+
+	g, err := gocui.NewGui(gocui.OutputNormal)
+
+	if err != nil {
+		return err
+	}
+
+	defer g.Close()
+
+	g.SetManagerFunc(layout)
+
+	err = keybindings(g)
+
+	if err != nil {
+		return err
+	}
+
+	err = g.MainLoop()
+
+	if err != nil && err != gocui.ErrQuit {
+		return err
+	}
+
+	return nil
+}
+
+func layout(g *gocui.Gui) error {
+	//maxX, maxY := g.Size()
+
+	ui.bugTable.layout(g)
+
+	v, err := g.View("table")
+	if 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("table", 'j', gocui.ModNone, cursorDown); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding("table", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding("table", 'k', gocui.ModNone, cursorUp); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding("table", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
+		return err
+	}
+
+	//err = g.SetKeybinding("table", 'h', gocui.ModNone, popTable)
+	//err = g.SetKeybinding("table", gocui.KeyArrowLeft, gocui.ModNone, popTable)
+	//err = g.SetKeybinding("table", 'l', gocui.ModNone, pushTable)
+	//err = g.SetKeybinding("table", gocui.KeyArrowRight, gocui.ModNone, pushTable)
+	//err = g.SetKeybinding("table", 'p', gocui.ModNone, playSelected)
+	//err = g.SetKeybinding("table", gocui.KeyEnter, gocui.ModNone, playSelectedAndExit)
+	//err = g.SetKeybinding("table", 'm', gocui.ModNone, loadNextRecords)
+
+	return nil
+}
+
+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
+	}
+
+	return nil
+}
+
+func 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 cursorClamp(v *gocui.View) error {
+	_, y := v.Cursor()
+
+	y = minInt(y, ui.bugTable.getTableLength()-1)
+	y = maxInt(y, 0)
+
+	err := v.SetCursor(0, y)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}