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 := 5
 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	_, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
170	if err != nil {
171		return err
172	}
173
174	_, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
175	if err != nil {
176		return err
177	}
178
179	// Below bottom of frame
180	if vy1 > lsy1 {
181		ls.scroll += vy1 - lsy1
182		return nil
183	}
184
185	// Above top of frame
186	if vy0 < lsy0 {
187		ls.scroll -= lsy0 - vy0
188	}
189
190	return nil
191}
192
193func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
194	ls.selected = maxInt(0, ls.selected-1)
195	return ls.focusView(g)
196}
197
198func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
199	ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
200	return ls.focusView(g)
201}
202
203func(ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
204	ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
205	return nil
206}
207
208func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
209	c := ui.inputPopup.Activate("Add a new label")
210
211	go func() {
212		input := <-c
213		
214		// Standardize label format
215		input = strings.TrimSuffix(input, "\n")
216		input = strings.Replace(input, " ", "-", -1)
217
218		// Check if label already exists
219		for i, label := range ls.labels {
220			if input == label.String() {
221				ls.labelSelect[i] = true
222				ls.selected = i
223
224				if err := ls.focusView(g); err != nil {
225					panic(err)
226				}
227
228				g.Update(func(gui *gocui.Gui) error {
229					return nil
230				})
231
232				return
233			}
234		}
235
236		// Add new label, make it selected, and move frame
237		ls.labels = append(ls.labels, bug.Label(input))
238		ls.labelSelect = append(ls.labelSelect, true)
239		ls.selected = len(ls.labels) - 1
240
241		if err := ls.layout(g); err != nil {
242			panic(err)
243		}
244
245		if err := ls.focusView(g); err != nil {
246			panic(err)
247		}
248
249		g.Update(func(gui *gocui.Gui) error {
250			return nil
251		})
252	}()
253
254	return nil
255}
256
257func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
258	return ui.activateWindow(ui.showBug)
259}
260
261func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
262	bugLabels := ls.bug.Snapshot().Labels
263	selectedLabels := []bug.Label{}
264	for i, label := range ls.labels {
265		if ls.labelSelect[i] {
266			selectedLabels = append(selectedLabels, label)
267		}
268	}
269
270	// Find the new and removed labels. This makes use of the fact that the first elements
271	// of selectedLabels are the not-removed labels in bugLabels
272	newLabels := []string{}
273	rmLabels := []string{}
274	i := 0	// Index for bugLabels
275	j := 0	// Index for selectedLabels
276	for {
277		if j == len(selectedLabels) {
278			// No more labels to consider
279			break
280		} else if i == len(bugLabels) {
281			// Remaining labels are all new
282			newLabels = append(newLabels, selectedLabels[j].String())
283			j += 1
284		} else if bugLabels[i] == selectedLabels[j] {
285			// Labels match. Move to next pair
286			i += 1
287			j += 1
288		} else {
289			// Labels don't match. Prelabel must have been removed
290			rmLabels = append(rmLabels, bugLabels[i].String())
291			i += 1
292		}
293	}
294
295	if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
296		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
297	}
298
299	return ui.activateWindow(ui.showBug)
300}