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