termui: add a reusable error popup, use it for badly formated bug creation

Michael Muré created

Change summary

termui/bug_table.go   | 40 ++++++++++--------
termui/error_popup.go | 93 ++++++++++++++++++++++++++++++++++++++++++++
termui/termui.go      | 42 +++++++++++++-------
3 files changed, 142 insertions(+), 33 deletions(-)

Detailed changes

termui/bug_table.go 🔗

@@ -27,6 +27,11 @@ func newBugTable(cache cache.RepoCacher) *bugTable {
 func (bt *bugTable) layout(g *gocui.Gui) error {
 	maxX, maxY := g.Size()
 
+	if maxY < 4 {
+		// window too small !
+		return nil
+	}
+
 	v, err := g.SetView("header", -1, -1, maxX, 3)
 
 	if err != nil {
@@ -51,12 +56,6 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 		v.Highlight = true
 		v.SelBgColor = gocui.ColorWhite
 		v.SelFgColor = gocui.ColorBlack
-
-		_, err = g.SetCurrentView(bugTableView)
-
-		if err != nil {
-			return err
-		}
 	}
 
 	_, viewHeight := v.Size()
@@ -99,6 +98,12 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
 		fmt.Fprintf(v, "[q] Quit [←,h] Previous page [↓,j] Down [↑,k] Up [→,l] Next page [enter] Open bug [n] New bug")
 	}
 
+	_, err = g.SetCurrentView(bugTableView)
+
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -181,6 +186,11 @@ func (bt *bugTable) doPaginate(allIds []string, max int) error {
 
 	nb := minInt(len(allIds)-bt.cursor, max)
 
+	if nb < 0 {
+		bt.bugs = []*bug.Snapshot{}
+		return nil
+	}
+
 	// slice the data
 	ids := allIds[bt.cursor : bt.cursor+nb]
 
@@ -260,10 +270,8 @@ 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
-	}
+	// window is too small to set the cursor properly, ignoring the error
+	_ = v.SetCursor(0, y)
 
 	return nil
 }
@@ -272,10 +280,8 @@ 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
-	}
+	// window is too small to set the cursor properly, ignoring the error
+	_ = v.SetCursor(0, y)
 
 	return nil
 }
@@ -286,10 +292,8 @@ func (bt *bugTable) cursorClamp(v *gocui.View) error {
 	y = minInt(y, bt.getTableLength()-1)
 	y = maxInt(y, 0)
 
-	err := v.SetCursor(0, y)
-	if err != nil {
-		return err
-	}
+	// window is too small to set the cursor properly, ignoring the error
+	_ = v.SetCursor(0, y)
 
 	return nil
 }

termui/error_popup.go 🔗

@@ -0,0 +1,93 @@
+package termui
+
+import (
+	"fmt"
+	"github.com/jroimartin/gocui"
+	"strings"
+)
+
+const errorPopupView = "errorPopupView"
+
+type errorPopup struct {
+	err string
+}
+
+func newErrorPopup() *errorPopup {
+	return &errorPopup{
+		err: "",
+	}
+}
+
+func (ep *errorPopup) keybindings(g *gocui.Gui) error {
+	if err := g.SetKeybinding(errorPopupView, gocui.KeySpace, gocui.ModNone, ep.close); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(errorPopupView, gocui.KeyEnter, gocui.ModNone, ep.close); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ep *errorPopup) layout(g *gocui.Gui) error {
+	if ep.err == "" {
+		return nil
+	}
+
+	maxX, maxY := g.Size()
+
+	width := minInt(30, maxX)
+	wrapped, nblines := word_wrap(ep.err, width-2)
+	height := minInt(nblines+2, maxY)
+	x0 := (maxX - width) / 2
+	y0 := (maxY - height) / 2
+
+	v, err := g.SetView(errorPopupView, x0, y0, x0+width, y0+height)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = true
+
+		fmt.Fprintf(v, wrapped)
+	}
+
+	if _, err := g.SetCurrentView(errorPopupView); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ep *errorPopup) close(g *gocui.Gui, v *gocui.View) error {
+	ep.err = ""
+	g.DeleteView(errorPopupView)
+	return nil
+}
+
+func (ep *errorPopup) isActive() bool {
+	return ep.err != ""
+}
+
+func word_wrap(text string, lineWidth int) (string, int) {
+	words := strings.Fields(strings.TrimSpace(text))
+	if len(words) == 0 {
+		return text, 1
+	}
+	lines := 1
+	wrapped := words[0]
+	spaceLeft := lineWidth - len(wrapped)
+	for _, word := range words[1:] {
+		if len(word)+1 > spaceLeft {
+			wrapped += "\n" + word
+			spaceLeft = lineWidth - len(word)
+			lines++
+		} else {
+			wrapped += " " + word
+			spaceLeft -= 1 + len(word)
+		}
+	}
+
+	return wrapped, lines
+}

termui/termui.go 🔗

@@ -16,7 +16,8 @@ type termUI struct {
 	cache        cache.RepoCacher
 	activeWindow window
 
-	bugTable *bugTable
+	bugTable   *bugTable
+	errorPopup *errorPopup
 }
 
 var ui *termUI
@@ -30,9 +31,10 @@ func Run(repo repository.Repo) error {
 	c := cache.NewRepoCache(repo)
 
 	ui = &termUI{
-		gError:   make(chan error, 1),
-		cache:    c,
-		bugTable: newBugTable(c),
+		gError:     make(chan error, 1),
+		cache:      c,
+		bugTable:   newBugTable(c),
+		errorPopup: newErrorPopup(),
 	}
 
 	ui.activeWindow = ui.bugTable
@@ -64,6 +66,7 @@ func initGui() {
 
 	if err != nil {
 		ui.g.Close()
+		ui.g = nil
 		ui.gError <- err
 		return
 	}
@@ -71,7 +74,9 @@ func initGui() {
 	err = g.MainLoop()
 
 	if err != nil && err != errTerminateMainloop {
-		ui.g.Close()
+		if ui.g != nil {
+			ui.g.Close()
+		}
 		ui.gError <- err
 	}
 
@@ -79,14 +84,16 @@ func initGui() {
 }
 
 func layout(g *gocui.Gui) error {
-	//maxX, maxY := g.Size()
-
 	g.Cursor = false
 
 	if err := ui.activeWindow.layout(g); err != nil {
 		return err
 	}
 
+	if err := ui.errorPopup.layout(g); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -100,6 +107,10 @@ func keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	if err := ui.errorPopup.keybindings(g); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -118,20 +129,21 @@ func newBugWithEditor(g *gocui.Gui, v *gocui.View) error {
 	//		instance's mainLoop. This error is then filtered.
 
 	ui.g.Close()
+	ui.g = nil
 
 	title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
 
-	if err == input.ErrEmptyTitle {
-		// TODO: display proper error
-		return err
-	}
-	if err != nil {
+	if err != nil && err != input.ErrEmptyTitle {
 		return err
 	}
 
-	_, err = ui.cache.NewBug(title, message)
-	if err != nil {
-		return err
+	if err == input.ErrEmptyTitle {
+		ui.errorPopup.err = err.Error()
+	} else {
+		_, err = ui.cache.NewBug(title, message)
+		if err != nil {
+			return err
+		}
 	}
 
 	initGui()