aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--termui/label_select.go304
-rw-r--r--termui/show_bug.go86
-rw-r--r--termui/termui.go26
3 files changed, 329 insertions, 87 deletions
diff --git a/termui/label_select.go b/termui/label_select.go
new file mode 100644
index 00000000..244001df
--- /dev/null
+++ b/termui/label_select.go
@@ -0,0 +1,304 @@
+package termui
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/MichaelMure/git-bug/bug"
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/jroimartin/gocui"
+)
+
+const labelSelectView = "labelSelectView"
+const labelSelectInstructionsView = "labelSelectInstructionsView"
+
+type labelSelect struct {
+ cache *cache.RepoCache
+ bug *cache.BugCache
+ labels []bug.Label
+ labelSelect []bool
+ selected int
+ scroll int
+ childViews []string
+}
+
+func newLabelSelect() *labelSelect {
+ return &labelSelect{}
+}
+
+func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) {
+ ls.cache = cache
+ ls.bug = bug
+ ls.labels = cache.ValidLabels()
+
+ // Find which labels are currently applied to the bug
+ bugLabels := bug.Snapshot().Labels
+ labelSelect := make([]bool, len(ls.labels))
+ for i, label := range ls.labels {
+ for _, bugLabel := range bugLabels {
+ if label == bugLabel {
+ labelSelect[i] = true
+ break
+ }
+ }
+ }
+
+ ls.labelSelect = labelSelect
+ if len(labelSelect) > 0 {
+ ls.selected = 0
+ } else {
+ ls.selected = -1
+ }
+
+ ls.scroll = 0
+}
+
+func (ls *labelSelect) keybindings(g *gocui.Gui) error {
+ // Abort
+ if err := g.SetKeybinding(labelSelectView, gocui.KeyEsc, gocui.ModNone, ls.abort); err != nil {
+ return err
+ }
+ // Save and return
+ if err := g.SetKeybinding(labelSelectView, 'q', gocui.ModNone, ls.saveAndReturn); err != nil {
+ return err
+ }
+ // Up
+ if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowUp, gocui.ModNone, ls.selectPrevious); err != nil {
+ return err
+ }
+ if err := g.SetKeybinding(labelSelectView, 'k', gocui.ModNone, ls.selectPrevious); err != nil {
+ return err
+ }
+ // Down
+ if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowDown, gocui.ModNone, ls.selectNext); err != nil {
+ return err
+ }
+ if err := g.SetKeybinding(labelSelectView, 'j', gocui.ModNone, ls.selectNext); err != nil {
+ return err
+ }
+ // Select
+ if err := g.SetKeybinding(labelSelectView, gocui.KeySpace, gocui.ModNone, ls.selectItem); err != nil {
+ return err
+ }
+ if err := g.SetKeybinding(labelSelectView, 'x', gocui.ModNone, ls.selectItem); err != nil {
+ return err
+ }
+ if err := g.SetKeybinding(labelSelectView, gocui.KeyEnter, gocui.ModNone, ls.selectItem); err != nil {
+ return err
+ }
+ // Add
+ if err := g.SetKeybinding(labelSelectView, 'a', gocui.ModNone, ls.addItem); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ls *labelSelect) layout(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ ls.childViews = nil
+
+ width := 5
+ for _, label := range ls.labels {
+ width = maxInt(width, len(label))
+ }
+ width += 10
+ x0 := 1
+ y0 := 0 - ls.scroll
+
+ v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+
+ v.Frame = false
+ }
+
+ for i, label := range ls.labels {
+ viewname := fmt.Sprintf("view%d", i)
+ v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
+ if err != nil && err != gocui.ErrUnknownView {
+ return err
+ }
+ ls.childViews = append(ls.childViews, viewname)
+ v.Frame = i == ls.selected
+ v.Clear()
+ selectBox := " [ ] "
+ if ls.labelSelect[i] {
+ selectBox = " [x] "
+ }
+ fmt.Fprint(v, selectBox, label)
+ y0 += 2
+ }
+
+ v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY)
+ ls.childViews = append(ls.childViews, labelSelectInstructionsView)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Frame = false
+ v.BgColor = gocui.ColorBlue
+ }
+ v.Clear()
+ fmt.Fprint(v, "[q] Save and close [↓↑,jk] Nav [a] Add item")
+ if _, err = g.SetViewOnTop(labelSelectInstructionsView); err != nil {
+ return err
+ }
+ if _, err := g.SetCurrentView(labelSelectView); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ls *labelSelect) disable(g *gocui.Gui) error {
+ for _, view := range ls.childViews {
+ if err := g.DeleteView(view); err != nil && err != gocui.ErrUnknownView {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ls *labelSelect) focusView(g *gocui.Gui) error {
+ if ls.selected < 0 {
+ return nil
+ }
+
+ _, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
+ if err != nil {
+ return err
+ }
+
+ _, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
+ if err != nil {
+ return err
+ }
+
+ // Below bottom of frame
+ if vy1 > lsy1 {
+ ls.scroll += vy1 - lsy1
+ return nil
+ }
+
+ // Above top of frame
+ if vy0 < lsy0 {
+ ls.scroll -= lsy0 - vy0
+ }
+
+ return nil
+}
+
+func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
+ if ls.selected < 0 {
+ return nil
+ }
+
+ ls.selected = maxInt(0, ls.selected-1)
+ return ls.focusView(g)
+}
+
+func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
+ if ls.selected < 0 {
+ return nil
+ }
+
+ ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
+ return ls.focusView(g)
+}
+
+func (ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
+ if ls.selected < 0 {
+ return nil
+ }
+
+ ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
+ return nil
+}
+
+func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
+ c := ui.inputPopup.Activate("Add a new label")
+
+ go func() {
+ input := <-c
+
+ // Standardize label format
+ input = strings.TrimSuffix(input, "\n")
+ input = strings.Replace(input, " ", "-", -1)
+
+ // Check if label already exists
+ for i, label := range ls.labels {
+ if input == label.String() {
+ ls.labelSelect[i] = true
+ ls.selected = i
+
+ g.Update(func(gui *gocui.Gui) error {
+ return ls.focusView(g)
+ })
+
+ return
+ }
+ }
+
+ // Add new label, make it selected, and focus
+ ls.labels = append(ls.labels, bug.Label(input))
+ ls.labelSelect = append(ls.labelSelect, true)
+ ls.selected = len(ls.labels) - 1
+
+ g.Update(func(g *gocui.Gui) error {
+ return nil
+ })
+ }()
+
+ return nil
+}
+
+func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
+ return ui.activateWindow(ui.showBug)
+}
+
+func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
+ bugLabels := ls.bug.Snapshot().Labels
+ var selectedLabels []bug.Label
+ for i, label := range ls.labels {
+ if ls.labelSelect[i] {
+ selectedLabels = append(selectedLabels, label)
+ }
+ }
+
+ // Find the new and removed labels. This could be implemented more efficiently...
+ var newLabels []string
+ var rmLabels []string
+
+ for _, selectedLabel := range selectedLabels {
+ found := false
+ for _, bugLabel := range bugLabels {
+ if selectedLabel == bugLabel {
+ found = true
+ }
+ }
+
+ if !found {
+ newLabels = append(newLabels, string(selectedLabel))
+ }
+ }
+
+ for _, bugLabel := range bugLabels {
+ found := false
+ for _, selectedLabel := range selectedLabels {
+ if bugLabel == selectedLabel {
+ found = true
+ }
+ }
+
+ if !found {
+ rmLabels = append(rmLabels, string(bugLabel))
+ }
+ }
+
+ if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
+ ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
+ }
+
+ return ui.activateWindow(ui.showBug)
+}
diff --git a/termui/show_bug.go b/termui/show_bug.go
index 395d0cd2..a16626a4 100644
--- a/termui/show_bug.go
+++ b/termui/show_bug.go
@@ -95,13 +95,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
}
v.Clear()
- fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation ")
-
- if sb.isOnSide {
- fmt.Fprint(v, "[a] Add label [r] Remove label")
- } else {
- fmt.Fprint(v, "[o] Toggle open/close [e] Edit [c] Comment [t] Change title")
- }
+ fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation [o] Toggle open/close [e] Edit [c] Comment [t] Change title")
_, err = g.SetViewOnTop(showBugInstructionView)
if err != nil {
@@ -190,16 +184,6 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
return err
}
- // 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
}
@@ -628,13 +612,12 @@ func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error {
}
func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
+ snap := sb.bug.Snapshot()
+
if sb.isOnSide {
- ui.msgPopup.Activate(msgPopupErrorTitle, "Selected field is not editable.")
- return nil
+ return sb.editLabels(g, snap)
}
- snap := sb.bug.Snapshot()
-
op, err := snap.SearchTimelineItem(git.Hash(sb.selected))
if err != nil {
return err
@@ -647,66 +630,15 @@ func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
case *bug.CreateTimelineItem:
preMessage := op.(*bug.CreateTimelineItem).Message
return editCommentWithEditor(sb.bug, op.Hash(), preMessage)
+ case *bug.LabelChangeTimelineItem:
+ return sb.editLabels(g, snap)
}
ui.msgPopup.Activate(msgPopupErrorTitle, "Selected field is not editable.")
return nil
}
-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.msgPopup.Activate(msgPopupErrorTitle, 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.msgPopup.Activate(msgPopupErrorTitle, 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
+func (sb *showBug) editLabels(g *gocui.Gui, snap *bug.Snapshot) error {
+ ui.labelSelect.SetBug(sb.cache, sb.bug)
+ return ui.activateWindow(ui.labelSelect)
}
diff --git a/termui/termui.go b/termui/termui.go
index 5c39869b..9f68efcc 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -18,10 +18,11 @@ type termUI struct {
activeWindow window
- bugTable *bugTable
- showBug *showBug
- msgPopup *msgPopup
- inputPopup *inputPopup
+ bugTable *bugTable
+ showBug *showBug
+ labelSelect *labelSelect
+ msgPopup *msgPopup
+ inputPopup *inputPopup
}
func (tui *termUI) activateWindow(window window) error {
@@ -45,12 +46,13 @@ type window interface {
// Run will launch the termUI in the terminal
func Run(cache *cache.RepoCache) error {
ui = &termUI{
- gError: make(chan error, 1),
- cache: cache,
- bugTable: newBugTable(cache),
- showBug: newShowBug(cache),
- msgPopup: newMsgPopup(),
- inputPopup: newInputPopup(),
+ gError: make(chan error, 1),
+ cache: cache,
+ bugTable: newBugTable(cache),
+ showBug: newShowBug(cache),
+ labelSelect: newLabelSelect(),
+ msgPopup: newMsgPopup(),
+ inputPopup: newInputPopup(),
}
ui.activeWindow = ui.bugTable
@@ -143,6 +145,10 @@ func keybindings(g *gocui.Gui) error {
return err
}
+ if err := ui.labelSelect.keybindings(g); err != nil {
+ return err
+ }
+
if err := ui.msgPopup.keybindings(g); err != nil {
return err
}