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	if len(labelSelect) > 0 {
 46		ls.selected = 0
 47	} else {
 48		ls.selected = -1
 49	}
 50
 51	ls.scroll = 0
 52}
 53
 54func (ls *labelSelect) keybindings(g *gocui.Gui) error {
 55	// Abort
 56	if err := g.SetKeybinding(labelSelectView, gocui.KeyEsc, gocui.ModNone, ls.abort); err != nil {
 57		return err
 58	}
 59	// Save and return
 60	if err := g.SetKeybinding(labelSelectView, 'q', gocui.ModNone, ls.saveAndReturn); err != nil {
 61		return err
 62	}
 63	// Up
 64	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowUp, gocui.ModNone, ls.selectPrevious); err != nil {
 65		return err
 66	}
 67	if err := g.SetKeybinding(labelSelectView, 'k', gocui.ModNone, ls.selectPrevious); err != nil {
 68		return err
 69	}
 70	// Down
 71	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowDown, gocui.ModNone, ls.selectNext); err != nil {
 72		return err
 73	}
 74	if err := g.SetKeybinding(labelSelectView, 'j', gocui.ModNone, ls.selectNext); err != nil {
 75		return err
 76	}
 77	// Select
 78	if err := g.SetKeybinding(labelSelectView, gocui.KeySpace, gocui.ModNone, ls.selectItem); err != nil {
 79		return err
 80	}
 81	if err := g.SetKeybinding(labelSelectView, 'x', gocui.ModNone, ls.selectItem); err != nil {
 82		return err
 83	}
 84	if err := g.SetKeybinding(labelSelectView, gocui.KeyEnter, gocui.ModNone, ls.selectItem); err != nil {
 85		return err
 86	}
 87	// Add
 88	if err := g.SetKeybinding(labelSelectView, 'a', gocui.ModNone, ls.addItem); err != nil {
 89		return err
 90	}
 91	return nil
 92}
 93
 94func (ls *labelSelect) layout(g *gocui.Gui) error {
 95	maxX, maxY := g.Size()
 96	ls.childViews = nil
 97
 98	width := 5
 99	for _, label := range ls.labels {
100		width = maxInt(width, len(label))
101	}
102	width += 10
103	x0 := 1
104	y0 := 2 - ls.scroll
105
106	v, err := g.SetView("labelTitleView", x0, 0, x0 + width, 2)
107	if err != nil {
108		if err != gocui.ErrUnknownView {
109			return err
110		}
111
112		v.Frame = false
113	}
114	ls.childViews = append(ls.childViews, "labelTitleView")
115	v.Clear()
116	fmt.Fprint(v, "   ", colors.Bold("Add Labels"))
117
118	v, err = g.SetView(labelSelectView, x0, 2, x0+width, maxY-2)
119	if err != nil {
120		if err != gocui.ErrUnknownView {
121			return err
122		}
123
124		v.Frame = false
125	}
126
127	for i, label := range ls.labels {
128		viewname := fmt.Sprintf("view%d", i)
129		v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
130		if err != nil && err != gocui.ErrUnknownView {
131			return err
132		}
133		ls.childViews = append(ls.childViews, viewname)
134		v.Frame = i == ls.selected
135		v.Clear()
136		selectBox := " [ ] "
137		if ls.labelSelect[i] {
138			selectBox = " [x] "
139		}
140		fmt.Fprint(v, selectBox, label)
141		y0 += 2
142	}
143
144	v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY)
145	ls.childViews = append(ls.childViews, labelSelectInstructionsView)
146	if err != nil {
147		if err != gocui.ErrUnknownView{
148			return err
149		}
150		v.Frame = false
151		v.BgColor = gocui.ColorBlue
152	}
153	v.Clear()
154	fmt.Fprint(v, "[q] Save and close [↓↑,jk] Nav [a] Add item")
155	if _, err = g.SetViewOnTop(labelSelectInstructionsView); err != nil {
156		return err
157	}
158	if _, err := g.SetCurrentView(labelSelectView); err != nil {
159		return err
160	}
161	return nil
162}
163
164func (ls *labelSelect) disable(g *gocui.Gui) error {
165	for _, view := range ls.childViews {
166		if err := g.DeleteView(view); err != nil && err != gocui.ErrUnknownView {
167			return err
168		}
169	}
170	return nil
171}
172
173func (ls *labelSelect) focusView(g *gocui.Gui) error {
174	if ls.selected < 0 {
175		return nil
176	}
177
178	_, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
179	if err != nil {
180		return err
181	}
182
183	_, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
184	if err != nil {
185		return err
186	}
187
188	// Below bottom of frame
189	if vy1 > lsy1 {
190		ls.scroll += vy1 - lsy1
191		return nil
192	}
193
194	// Above top of frame
195	if vy0 < lsy0 {
196		ls.scroll -= lsy0 - vy0
197	}
198
199	return nil
200}
201
202func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
203	if ls.selected < 0 {
204		return nil
205	}
206
207	ls.selected = maxInt(0, ls.selected-1)
208	return ls.focusView(g)
209}
210
211func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
212	if ls.selected < 0 {
213		return nil
214	}
215
216	ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
217	return ls.focusView(g)
218}
219
220func(ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
221	if ls.selected < 0 {
222		return nil
223	}
224
225	ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
226	return nil
227}
228
229func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
230	c := ui.inputPopup.Activate("Add a new label")
231
232	go func() {
233		input := <-c
234		
235		// Standardize label format
236		input = strings.TrimSuffix(input, "\n")
237		input = strings.Replace(input, " ", "-", -1)
238
239		// Check if label already exists
240		for i, label := range ls.labels {
241			if input == label.String() {
242				ls.labelSelect[i] = true
243				ls.selected = i
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				return
254			}
255		}
256
257		// Add new label, make it selected, and move frame
258		ls.labels = append(ls.labels, bug.Label(input))
259		ls.labelSelect = append(ls.labelSelect, true)
260		ls.selected = len(ls.labels) - 1
261
262		if err := ls.layout(g); err != nil {
263			panic(err)
264		}
265
266		if err := ls.focusView(g); err != nil {
267			panic(err)
268		}
269
270		g.Update(func(gui *gocui.Gui) error {
271			return nil
272		})
273	}()
274
275	return nil
276}
277
278func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
279	return ui.activateWindow(ui.showBug)
280}
281
282func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
283	bugLabels := ls.bug.Snapshot().Labels
284	selectedLabels := []bug.Label{}
285	for i, label := range ls.labels {
286		if ls.labelSelect[i] {
287			selectedLabels = append(selectedLabels, label)
288		}
289	}
290
291	// Find the new and removed labels. This could be implemented more efficiently...
292	newLabels := []string{}
293	rmLabels := []string{}
294
295	for _, selectedLabel := range selectedLabels {
296		found := false
297		for _, bugLabel := range bugLabels {
298			if selectedLabel == bugLabel {
299				found = true
300			}
301		}
302
303		if !found {
304			newLabels = append(newLabels, string(selectedLabel))
305		}
306	}
307
308	for _, bugLabel := range bugLabels {
309		found := false
310		for _, selectedLabel := range selectedLabels {
311			if bugLabel == selectedLabel {
312				found = true
313			}
314		}
315
316		if !found {
317			rmLabels = append(rmLabels, string(bugLabel))
318		}
319	}
320
321	if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
322		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
323	}
324
325	return ui.activateWindow(ui.showBug)
326}