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