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