label_select.go

  1package termui
  2import (
  3	"fmt"
  4	"strings"
  5	"github.com/jroimartin/gocui"
  6	"github.com/MichaelMure/git-bug/bug"
  7	"github.com/MichaelMure/git-bug/cache"
  8)
  9const labelSelectView = "labelSelectView"
 10const labelSelectInstructionsView = "labelSelectInstructionsView"
 11
 12type labelSelect struct {
 13	cache       *cache.RepoCache
 14	bug         *cache.BugCache
 15	labels      []bug.Label
 16	labelSelect []bool
 17	selected    int
 18	scroll      int
 19	childViews  []string
 20}
 21
 22func newLabelSelect() *labelSelect {
 23	return &labelSelect{}
 24}
 25
 26func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) {
 27	ls.cache = cache
 28	ls.bug = bug
 29	ls.labels = cache.ValidLabels()
 30
 31	// Find which labels are currently applied to the bug
 32	bugLabels := bug.Snapshot().Labels
 33	labelSelect := make([]bool, len(ls.labels))
 34	for i, label := range ls.labels {
 35		for _, bugLabel := range bugLabels {
 36			if label == bugLabel {
 37				labelSelect[i] = true
 38				break
 39			}
 40		}
 41	}
 42
 43	ls.labelSelect = labelSelect
 44	ls.selected = 0
 45	ls.scroll = 0
 46}
 47
 48func (ls *labelSelect) keybindings(g *gocui.Gui) error {
 49	// Abort
 50	if err := g.SetKeybinding(labelSelectView, gocui.KeyEsc, gocui.ModNone, ls.abort); err != nil {
 51		return err
 52	}
 53	// Save and return
 54	if err := g.SetKeybinding(labelSelectView, 'q', gocui.ModNone, ls.saveAndReturn); err != nil {
 55		return err
 56	}
 57	// Up
 58	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowUp, gocui.ModNone, ls.selectPrevious); err != nil {
 59		return err
 60	}
 61	if err := g.SetKeybinding(labelSelectView, 'k', gocui.ModNone, ls.selectPrevious); err != nil {
 62		return err
 63	}
 64	// Down
 65	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowDown, gocui.ModNone, ls.selectNext); err != nil {
 66		return err
 67	}
 68	if err := g.SetKeybinding(labelSelectView, 'j', gocui.ModNone, ls.selectNext); err != nil {
 69		return err
 70	}
 71	// Select
 72	if err := g.SetKeybinding(labelSelectView, gocui.KeySpace, gocui.ModNone, ls.selectItem); err != nil {
 73		return err
 74	}
 75	if err := g.SetKeybinding(labelSelectView, 'x', gocui.ModNone, ls.selectItem); err != nil {
 76		return err
 77	}
 78	if err := g.SetKeybinding(labelSelectView, gocui.KeyEnter, gocui.ModNone, ls.selectItem); err != nil {
 79		return err
 80	}
 81	// Add
 82	if err := g.SetKeybinding(labelSelectView, 'a', gocui.ModNone, ls.addItem); err != nil {
 83		return err
 84	}
 85	return nil
 86}
 87
 88func (ls *labelSelect) layout(g *gocui.Gui) error {
 89	maxX, maxY := g.Size()
 90	ls.childViews = nil
 91
 92	// TODO: Make width adaptive
 93	width := 30
 94	x0 := 1
 95
 96	v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2)
 97	if err != nil {
 98		if err != gocui.ErrUnknownView {
 99			return err
100		}
101
102		v.Frame = false
103	}
104	v.Clear()
105	fmt.Fprintln(v, ls.scroll)
106	y0 := 1 - ls.scroll
107
108	for i, label := range ls.labels {
109		viewname := fmt.Sprintf("view%d", i)
110		v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
111		if err != nil && err != gocui.ErrUnknownView {
112			return err
113		}
114		ls.childViews = append(ls.childViews, viewname)
115		v.Frame = i == ls.selected
116		v.Clear()
117		selectBox := " [ ] "
118		if ls.labelSelect[i] {
119			selectBox = " [x] "
120		}
121		fmt.Fprint(v, selectBox, label)
122		y0 += 2
123	}
124
125	v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY)
126	ls.childViews = append(ls.childViews, labelSelectInstructionsView)
127	if err != nil {
128		if err != gocui.ErrUnknownView{
129			return err
130		}
131		v.Frame = false
132		v.BgColor = gocui.ColorBlue
133	}
134	v.Clear()
135	fmt.Fprint(v, "[q] Save and close [↓↑,jk] Nav [a] Add item")
136	if _, err = g.SetViewOnTop(labelSelectInstructionsView); err != nil {
137		return err
138	}
139	if _, err := g.SetCurrentView(labelSelectView); err != nil {
140		return err
141	}
142	return nil
143}
144
145func (ls *labelSelect) disable(g *gocui.Gui) error {
146	for _, view := range ls.childViews {
147		if err := g.DeleteView(view); err != nil && err != gocui.ErrUnknownView {
148			return err
149		}
150	}
151	return nil
152}
153
154func (ls *labelSelect) focusView(g *gocui.Gui) error {
155	mainView, err := g.View(labelSelectView)
156	if err != nil {
157		return err
158	}
159
160	_, maxY := mainView.Size()
161
162	_, vy0, _, _, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
163	if err != nil {
164		return err
165	}
166
167	v, err := g.View(fmt.Sprintf("view%d", ls.selected))
168	if err != nil {
169		return err
170	}
171
172	_, vMaxY := v.Size()
173
174	vy1 := vy0 + vMaxY
175
176	if vy0 < 0 {
177		ls.scroll += vy0
178		return nil
179	}
180
181	if vy1 > maxY {
182		ls.scroll -= maxY - vy1
183	}
184
185	return nil
186}
187
188func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
189	ls.selected = maxInt(0, ls.selected-1)
190	return ls.focusView(g)
191}
192
193func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
194	ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
195	return ls.focusView(g)
196}
197
198func(ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
199	ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
200	return nil
201}
202
203func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
204	c := ui.inputPopup.Activate("Add a new label")
205
206	go func() error {
207		input := <-c
208		
209		// Standardize label format
210		input = strings.TrimSuffix(input, "\n")
211		input = strings.Replace(input, " ", "-", -1)
212
213		// Check if label already exists
214		for i, label := range ls.labels {
215			if input == label.String() {
216				ls.labelSelect[i] = true
217				ls.selected = i
218
219				g.Update(func(gui *gocui.Gui) error {
220					return nil
221				})
222				return ls.focusView(g)
223			}
224		}
225
226		// Add new label, make it selected, and move frame
227		ls.labels = append(ls.labels, bug.Label(input))
228		ls.labelSelect = append(ls.labelSelect, true)
229		ls.selected = len(ls.labels) - 1
230
231		if err := ls.focusView(g); err != nil {
232			return err
233		}
234
235		g.Update(func(gui *gocui.Gui) error {
236			return nil
237		})
238
239		return nil
240	}()
241
242	return nil
243}
244
245func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
246	return ui.activateWindow(ui.showBug)
247}
248
249func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
250	bugLabels := ls.bug.Snapshot().Labels
251	selectedLabels := []bug.Label{}
252	for i, label := range ls.labels {
253		if ls.labelSelect[i] {
254			selectedLabels = append(selectedLabels, label)
255		}
256	}
257
258	// Find the new and removed labels. This makes use of the fact that the first elements
259	// of selectedLabels are the not-removed labels in bugLabels
260	newLabels := []string{}
261	rmLabels := []string{}
262	i := 0	// Index for bugLabels
263	j := 0	// Index for selectedLabels
264	for {
265		if j == len(selectedLabels) {
266			// No more labels to consider
267			break
268		} else if i == len(bugLabels) {
269			// Remaining labels are all new
270			newLabels = append(newLabels, selectedLabels[j].String())
271			j += 1
272		} else if bugLabels[i] == selectedLabels[j] {
273			// Labels match. Move to next pair
274			i += 1
275			j += 1
276		} else {
277			// Labels don't match. Prelabel must have been removed
278			rmLabels = append(rmLabels, bugLabels[i].String())
279			i += 1
280		}
281	}
282
283	if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
284		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
285	}
286
287	return ui.activateWindow(ui.showBug)
288}