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