diff options
-rw-r--r-- | termui/label_select.go | 304 | ||||
-rw-r--r-- | termui/show_bug.go | 86 | ||||
-rw-r--r-- | termui/termui.go | 26 |
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 } |