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