termui: add and remove labels

Michael Muré created

Change summary

termui/error_popup.go |  5 +
termui/input_popup.go | 96 +++++++++++++++++++++++++++++++++++++++++++++
termui/show_bug.go    | 75 ++++++++++++++++++++++++++++++++++
termui/termui.go      | 16 ++++++-
4 files changed, 186 insertions(+), 6 deletions(-)

Detailed changes

termui/error_popup.go 🔗

@@ -2,6 +2,7 @@ package termui
 
 import (
 	"fmt"
+
 	"github.com/MichaelMure/git-bug/util"
 	"github.com/jroimartin/gocui"
 )
@@ -38,7 +39,7 @@ func (ep *errorPopup) layout(g *gocui.Gui) error {
 
 	width := minInt(30, maxX)
 	wrapped, nblines := util.WordWrap(ep.message, width-2)
-	height := minInt(nblines+2, maxY)
+	height := minInt(nblines+1, maxY)
 	x0 := (maxX - width) / 2
 	y0 := (maxY - height) / 2
 
@@ -66,6 +67,6 @@ func (ep *errorPopup) close(g *gocui.Gui, v *gocui.View) error {
 	return g.DeleteView(errorPopupView)
 }
 
-func (ep *errorPopup) activate(message string) {
+func (ep *errorPopup) Activate(message string) {
 	ep.message = message
 }

termui/input_popup.go 🔗

@@ -0,0 +1,96 @@
+package termui
+
+import (
+	"io/ioutil"
+
+	"github.com/jroimartin/gocui"
+)
+
+const inputPopupView = "inputPopupView"
+
+type inputPopup struct {
+	active bool
+	title  string
+	c      chan string
+}
+
+func newInputPopup() *inputPopup {
+	return &inputPopup{}
+}
+
+func (ip *inputPopup) keybindings(g *gocui.Gui) error {
+	// Close
+	if err := g.SetKeybinding(inputPopupView, gocui.KeyEsc, gocui.ModNone, ip.close); err != nil {
+		return err
+	}
+
+	// Validate
+	if err := g.SetKeybinding(inputPopupView, gocui.KeyEnter, gocui.ModNone, ip.validate); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ip *inputPopup) layout(g *gocui.Gui) error {
+	if !ip.active {
+		return nil
+	}
+
+	maxX, maxY := g.Size()
+
+	width := minInt(30, maxX)
+	height := 2
+	x0 := (maxX - width) / 2
+	y0 := (maxY - height) / 2
+
+	v, err := g.SetView(inputPopupView, x0, y0, x0+width, y0+height)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = true
+		v.Title = ip.title
+		v.Editable = true
+	}
+
+	if _, err := g.SetCurrentView(inputPopupView); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ip *inputPopup) close(g *gocui.Gui, v *gocui.View) error {
+	ip.title = ""
+	ip.active = false
+	return g.DeleteView(inputPopupView)
+}
+
+func (ip *inputPopup) validate(g *gocui.Gui, v *gocui.View) error {
+	ip.title = ""
+
+	content, err := ioutil.ReadAll(v)
+	if err != nil {
+		return err
+	}
+
+	ip.title = ""
+	ip.active = false
+	err = g.DeleteView(inputPopupView)
+	if err != nil {
+		return err
+	}
+
+	ip.c <- string(content)
+
+	return nil
+}
+
+func (ip *inputPopup) Activate(title string) <-chan string {
+	ip.title = title
+	ip.active = true
+	ip.c = make(chan string)
+	return ip.c
+}

termui/show_bug.go 🔗

@@ -90,8 +90,15 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 		sb.childViews = append(sb.childViews, showBugInstructionView)
 		v.Frame = false
 		v.BgColor = gocui.ColorBlue
+	}
+
+	v.Clear()
+	fmt.Fprintf(v, "[q] Save and return [←,h] Left [↓,j] Down [↑,k] Up [→,l] Right ")
 
-		fmt.Fprintf(v, "[q] Save and return [c] Comment [t] Change title [↓,j] Down [↑,k] Up")
+	if sb.isOnSide {
+		fmt.Fprint(v, "[a] Add label [r] Remove label")
+	} else {
+		fmt.Fprint(v, "[c] Comment [t] Change title")
 	}
 
 	_, err = g.SetViewOnTop(showBugInstructionView)
@@ -170,6 +177,14 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
 	}
 
 	// Labels
+	if err := g.SetKeybinding(showBugView, 'a', gocui.ModNone,
+		sb.addLabel); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(showBugView, 'r', gocui.ModNone,
+		sb.removeLabel); err != nil {
+		return err
+	}
 
 	return nil
 }
@@ -563,3 +578,61 @@ func (sb *showBug) comment(g *gocui.Gui, v *gocui.View) error {
 func (sb *showBug) setTitle(g *gocui.Gui, v *gocui.View) error {
 	return setTitleWithEditor(sb.bug)
 }
+
+func (sb *showBug) addLabel(g *gocui.Gui, v *gocui.View) error {
+	c := ui.inputPopup.Activate("Add labels")
+
+	go func() {
+		input := <-c
+
+		labels := strings.FieldsFunc(input, func(r rune) bool {
+			return r == ' ' || r == ','
+		})
+
+		err := sb.bug.ChangeLabels(trimLabels(labels), nil)
+		if err != nil {
+			ui.errorPopup.Activate(err.Error())
+		}
+
+		g.Update(func(gui *gocui.Gui) error {
+			return nil
+		})
+	}()
+
+	return nil
+}
+
+func (sb *showBug) removeLabel(g *gocui.Gui, v *gocui.View) error {
+	c := ui.inputPopup.Activate("Remove labels")
+
+	go func() {
+		input := <-c
+
+		labels := strings.FieldsFunc(input, func(r rune) bool {
+			return r == ' ' || r == ','
+		})
+
+		err := sb.bug.ChangeLabels(nil, trimLabels(labels))
+		if err != nil {
+			ui.errorPopup.Activate(err.Error())
+		}
+
+		g.Update(func(gui *gocui.Gui) error {
+			return nil
+		})
+	}()
+
+	return nil
+}
+
+func trimLabels(labels []string) []string {
+	var result []string
+
+	for _, label := range labels {
+		trimmed := strings.TrimSpace(label)
+		if len(trimmed) > 0 {
+			result = append(result, trimmed)
+		}
+	}
+	return result
+}

termui/termui.go 🔗

@@ -20,6 +20,7 @@ type termUI struct {
 	bugTable   *bugTable
 	showBug    *showBug
 	errorPopup *errorPopup
+	inputPopup *inputPopup
 }
 
 func (tui *termUI) activateWindow(window window) error {
@@ -49,6 +50,7 @@ func Run(repo repository.Repo) error {
 		bugTable:   newBugTable(c),
 		showBug:    newShowBug(c),
 		errorPopup: newErrorPopup(),
+		inputPopup: newInputPopup(),
 	}
 
 	ui.activeWindow = ui.bugTable
@@ -108,6 +110,10 @@ func layout(g *gocui.Gui) error {
 		return err
 	}
 
+	if err := ui.inputPopup.layout(g); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -129,6 +135,10 @@ func keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	if err := ui.inputPopup.keybindings(g); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -156,7 +166,7 @@ func newBugWithEditor(repo cache.RepoCacher) error {
 	}
 
 	if err == input.ErrEmptyTitle {
-		ui.errorPopup.activate("Empty title, aborting.")
+		ui.errorPopup.Activate("Empty title, aborting.")
 	} else {
 		_, err := repo.NewBug(title, message)
 		if err != nil {
@@ -189,7 +199,7 @@ func addCommentWithEditor(bug cache.BugCacher) error {
 	}
 
 	if err == input.ErrEmptyMessage {
-		ui.errorPopup.activate("Empty message, aborting.")
+		ui.errorPopup.Activate("Empty message, aborting.")
 	} else {
 		err := bug.AddComment(message)
 		if err != nil {
@@ -222,7 +232,7 @@ func setTitleWithEditor(bug cache.BugCacher) error {
 	}
 
 	if err == input.ErrEmptyTitle {
-		ui.errorPopup.activate("Empty title, aborting.")
+		ui.errorPopup.Activate("Empty title, aborting.")
 	} else {
 		err := bug.SetTitle(title)
 		if err != nil {