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/jesseduffield/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, 0)
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, 0)
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 fmt.Fprint(v, selectBox, label)
131 y0 += 2
132 }
133
134 v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY, 0)
135 ls.childViews = append(ls.childViews, labelSelectInstructionsView)
136 if err != nil {
137 if err != gocui.ErrUnknownView {
138 return err
139 }
140 v.Frame = false
141 v.BgColor = gocui.ColorBlue
142 }
143 v.Clear()
144 fmt.Fprint(v, "[q] Save and close [↓↑,jk] Nav [a] Add item")
145 if _, err = g.SetViewOnTop(labelSelectInstructionsView); err != nil {
146 return err
147 }
148 if _, err := g.SetCurrentView(labelSelectView); err != nil {
149 return err
150 }
151 return nil
152}
153
154func (ls *labelSelect) disable(g *gocui.Gui) error {
155 for _, view := range ls.childViews {
156 if err := g.DeleteView(view); err != nil && err != gocui.ErrUnknownView {
157 return err
158 }
159 }
160 return nil
161}
162
163func (ls *labelSelect) focusView(g *gocui.Gui) error {
164 if ls.selected < 0 {
165 return nil
166 }
167
168 _, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
169 if err != nil {
170 return err
171 }
172
173 _, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
174 if err != nil {
175 return err
176 }
177
178 // Below bottom of frame
179 if vy1 > lsy1 {
180 ls.scroll += vy1 - lsy1
181 return nil
182 }
183
184 // Above top of frame
185 if vy0 < lsy0 {
186 ls.scroll -= lsy0 - vy0
187 }
188
189 return nil
190}
191
192func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
193 if ls.selected < 0 {
194 return nil
195 }
196
197 ls.selected = maxInt(0, ls.selected-1)
198 return ls.focusView(g)
199}
200
201func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
202 if ls.selected < 0 {
203 return nil
204 }
205
206 ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
207 return ls.focusView(g)
208}
209
210func (ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
211 if ls.selected < 0 {
212 return nil
213 }
214
215 ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
216 return nil
217}
218
219func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
220 c := ui.inputPopup.Activate("Add a new label")
221
222 go func() {
223 input := <-c
224
225 // Standardize label format
226 input = strings.TrimSuffix(input, "\n")
227 input = strings.Replace(input, " ", "-", -1)
228
229 // Check if label already exists
230 for i, label := range ls.labels {
231 if input == label.String() {
232 ls.labelSelect[i] = true
233 ls.selected = i
234
235 g.Update(func(gui *gocui.Gui) error {
236 return ls.focusView(g)
237 })
238
239 return
240 }
241 }
242
243 // Add new label, make it selected, and focus
244 ls.labels = append(ls.labels, bug.Label(input))
245 ls.labelSelect = append(ls.labelSelect, true)
246 ls.selected = len(ls.labels) - 1
247
248 g.Update(func(g *gocui.Gui) error {
249 return nil
250 })
251 }()
252
253 return nil
254}
255
256func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
257 return ui.activateWindow(ui.showBug)
258}
259
260func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
261 bugLabels := ls.bug.Snapshot().Labels
262 var selectedLabels []bug.Label
263 for i, label := range ls.labels {
264 if ls.labelSelect[i] {
265 selectedLabels = append(selectedLabels, label)
266 }
267 }
268
269 // Find the new and removed labels. This could be implemented more efficiently...
270 var newLabels []string
271 var rmLabels []string
272
273 for _, selectedLabel := range selectedLabels {
274 found := false
275 for _, bugLabel := range bugLabels {
276 if selectedLabel == bugLabel {
277 found = true
278 }
279 }
280
281 if !found {
282 newLabels = append(newLabels, string(selectedLabel))
283 }
284 }
285
286 for _, bugLabel := range bugLabels {
287 found := false
288 for _, selectedLabel := range selectedLabels {
289 if bugLabel == selectedLabel {
290 found = true
291 }
292 }
293
294 if !found {
295 rmLabels = append(rmLabels, string(bugLabel))
296 }
297 }
298
299 if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
300 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
301 }
302
303 return ui.activateWindow(ui.showBug)
304}