label_select.go

  1package termui
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/MichaelMure/git-bug/bug"
  8	"github.com/MichaelMure/git-bug/cache"
  9	"github.com/MichaelMure/gocui"
 10)
 11
 12const labelSelectView = "labelSelectView"
 13const labelSelectInstructionsView = "labelSelectInstructionsView"
 14
 15type labelSelect struct {
 16	cache       *cache.RepoCache
 17	bug         *cache.BugCache
 18	labels      []bug.Label
 19	labelSelect []bool
 20	selected    int
 21	scroll      int
 22	childViews  []string
 23}
 24
 25func newLabelSelect() *labelSelect {
 26	return &labelSelect{}
 27}
 28
 29func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) {
 30	ls.cache = cache
 31	ls.bug = bug
 32	ls.labels = cache.ValidLabels()
 33
 34	// Find which labels are currently applied to the bug
 35	bugLabels := bug.Snapshot().Labels
 36	labelSelect := make([]bool, len(ls.labels))
 37	for i, label := range ls.labels {
 38		for _, bugLabel := range bugLabels {
 39			if label == bugLabel {
 40				labelSelect[i] = true
 41				break
 42			}
 43		}
 44	}
 45
 46	ls.labelSelect = labelSelect
 47	if len(labelSelect) > 0 {
 48		ls.selected = 0
 49	} else {
 50		ls.selected = -1
 51	}
 52
 53	ls.scroll = 0
 54}
 55
 56func (ls *labelSelect) keybindings(g *gocui.Gui) error {
 57	// Abort
 58	if err := g.SetKeybinding(labelSelectView, gocui.KeyEsc, gocui.ModNone, ls.abort); err != nil {
 59		return err
 60	}
 61	// Save and return
 62	if err := g.SetKeybinding(labelSelectView, 'q', gocui.ModNone, ls.saveAndReturn); err != nil {
 63		return err
 64	}
 65	// Up
 66	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowUp, gocui.ModNone, ls.selectPrevious); err != nil {
 67		return err
 68	}
 69	if err := g.SetKeybinding(labelSelectView, 'k', gocui.ModNone, ls.selectPrevious); err != nil {
 70		return err
 71	}
 72	// Down
 73	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowDown, gocui.ModNone, ls.selectNext); err != nil {
 74		return err
 75	}
 76	if err := g.SetKeybinding(labelSelectView, 'j', gocui.ModNone, ls.selectNext); err != nil {
 77		return err
 78	}
 79	// Select
 80	if err := g.SetKeybinding(labelSelectView, gocui.KeySpace, gocui.ModNone, ls.selectItem); err != nil {
 81		return err
 82	}
 83	if err := g.SetKeybinding(labelSelectView, 'x', gocui.ModNone, ls.selectItem); err != nil {
 84		return err
 85	}
 86	if err := g.SetKeybinding(labelSelectView, gocui.KeyEnter, gocui.ModNone, ls.selectItem); err != nil {
 87		return err
 88	}
 89	// Add
 90	if err := g.SetKeybinding(labelSelectView, 'a', gocui.ModNone, ls.addItem); err != nil {
 91		return err
 92	}
 93	return nil
 94}
 95
 96func (ls *labelSelect) layout(g *gocui.Gui) error {
 97	maxX, maxY := g.Size()
 98	ls.childViews = nil
 99
100	width := 5
101	for _, label := range ls.labels {
102		width = maxInt(width, len(label))
103	}
104	width += 10
105	x0 := 1
106	y0 := 0 - ls.scroll
107
108	v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2)
109	if err != nil {
110		if err != gocui.ErrUnknownView {
111			return err
112		}
113
114		v.Frame = false
115	}
116
117	for i, label := range ls.labels {
118		viewname := fmt.Sprintf("view%d", i)
119		v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
120		if err != nil && err != gocui.ErrUnknownView {
121			return err
122		}
123		ls.childViews = append(ls.childViews, viewname)
124		v.Frame = i == ls.selected
125		v.Clear()
126		selectBox := " [ ] "
127		if ls.labelSelect[i] {
128			selectBox = " [x] "
129		}
130
131		lc := label.Color()
132		lc256 := lc.Term256()
133		labelStr := lc256.Escape() + "◼ " + lc256.Unescape() + label.String()
134		fmt.Fprint(v, selectBox, labelStr)
135
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	if ls.selected < 0 {
170		return nil
171	}
172
173	_, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
174	if err != nil {
175		return err
176	}
177
178	_, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
179	if err != nil {
180		return err
181	}
182
183	// Below bottom of frame
184	if vy1 > lsy1 {
185		ls.scroll += vy1 - lsy1
186		return nil
187	}
188
189	// Above top of frame
190	if vy0 < lsy0 {
191		ls.scroll -= lsy0 - vy0
192	}
193
194	return nil
195}
196
197func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
198	if ls.selected < 0 {
199		return nil
200	}
201
202	ls.selected = maxInt(0, ls.selected-1)
203	return ls.focusView(g)
204}
205
206func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
207	if ls.selected < 0 {
208		return nil
209	}
210
211	ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
212	return ls.focusView(g)
213}
214
215func (ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
216	if ls.selected < 0 {
217		return nil
218	}
219
220	ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
221	return nil
222}
223
224func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
225	c := ui.inputPopup.Activate("Add a new label")
226
227	go func() {
228		input := <-c
229
230		// Standardize label format
231		input = strings.TrimSuffix(input, "\n")
232		input = strings.Replace(input, " ", "-", -1)
233
234		// Check if label already exists
235		for i, label := range ls.labels {
236			if input == label.String() {
237				ls.labelSelect[i] = true
238				ls.selected = i
239
240				g.Update(func(gui *gocui.Gui) error {
241					return ls.focusView(g)
242				})
243
244				return
245			}
246		}
247
248		// Add new label, make it selected, and focus
249		ls.labels = append(ls.labels, bug.Label(input))
250		ls.labelSelect = append(ls.labelSelect, true)
251		ls.selected = len(ls.labels) - 1
252
253		g.Update(func(g *gocui.Gui) error {
254			return nil
255		})
256	}()
257
258	return nil
259}
260
261func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
262	return ui.activateWindow(ui.showBug)
263}
264
265func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
266	bugLabels := ls.bug.Snapshot().Labels
267	var selectedLabels []bug.Label
268	for i, label := range ls.labels {
269		if ls.labelSelect[i] {
270			selectedLabels = append(selectedLabels, label)
271		}
272	}
273
274	// Find the new and removed labels. This could be implemented more efficiently...
275	var newLabels []string
276	var rmLabels []string
277
278	for _, selectedLabel := range selectedLabels {
279		found := false
280		for _, bugLabel := range bugLabels {
281			if selectedLabel == bugLabel {
282				found = true
283			}
284		}
285
286		if !found {
287			newLabels = append(newLabels, string(selectedLabel))
288		}
289	}
290
291	for _, bugLabel := range bugLabels {
292		found := false
293		for _, selectedLabel := range selectedLabels {
294			if bugLabel == selectedLabel {
295				found = true
296			}
297		}
298
299		if !found {
300			rmLabels = append(rmLabels, string(bugLabel))
301		}
302	}
303
304	if _, _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
305		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
306	}
307
308	return ui.activateWindow(ui.showBug)
309}