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 makes use of the fact that the first elements
292 // of selectedLabels are the not-removed labels in bugLabels
293 newLabels := []string{}
294 rmLabels := []string{}
295 i := 0 // Index for bugLabels
296 j := 0 // Index for selectedLabels
297 for {
298 if j == len(selectedLabels) {
299 // No more labels to consider
300 break
301 } else if i == len(bugLabels) {
302 // Remaining labels are all new
303 newLabels = append(newLabels, selectedLabels[j].String())
304 j += 1
305 } else if bugLabels[i] == selectedLabels[j] {
306 // Labels match. Move to next pair
307 i += 1
308 j += 1
309 } else {
310 // Labels don't match. Prelabel must have been removed
311 rmLabels = append(rmLabels, bugLabels[i].String())
312 i += 1
313 }
314 }
315
316 if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
317 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
318 }
319
320 return ui.activateWindow(ui.showBug)
321}