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 := 0
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}