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 // lsx0, lsy0, lsx1, lsy1, err := g.ViewPosition(labelSelectView)
170 _, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
171
172 // vx0, vy0, vx1, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
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 ls.selected = maxInt(0, ls.selected-1)
194 return ls.focusView(g)
195}
196
197func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
198 ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
199 return ls.focusView(g)
200}
201
202func(ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
203 ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
204 return nil
205}
206
207func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
208 c := ui.inputPopup.Activate("Add a new label")
209
210 go func() error {
211 input := <-c
212
213 // Standardize label format
214 input = strings.TrimSuffix(input, "\n")
215 input = strings.Replace(input, " ", "-", -1)
216
217 // Check if label already exists
218 for i, label := range ls.labels {
219 if input == label.String() {
220 ls.labelSelect[i] = true
221 ls.selected = i
222
223 g.Update(func(gui *gocui.Gui) error {
224 return nil
225 })
226 return ls.focusView(g)
227 }
228 }
229
230 // Add new label, make it selected, and move frame
231 ls.labels = append(ls.labels, bug.Label(input))
232 ls.labelSelect = append(ls.labelSelect, true)
233 ls.selected = len(ls.labels) - 1
234
235 ls.layout(g)
236
237 if err := ls.focusView(g); err != nil {
238 panic(err)
239 }
240
241 g.Update(func(gui *gocui.Gui) error {
242 return nil
243 })
244
245 return nil
246 }()
247
248 return nil
249}
250
251func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
252 return ui.activateWindow(ui.showBug)
253}
254
255func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
256 bugLabels := ls.bug.Snapshot().Labels
257 selectedLabels := []bug.Label{}
258 for i, label := range ls.labels {
259 if ls.labelSelect[i] {
260 selectedLabels = append(selectedLabels, label)
261 }
262 }
263
264 // Find the new and removed labels. This makes use of the fact that the first elements
265 // of selectedLabels are the not-removed labels in bugLabels
266 newLabels := []string{}
267 rmLabels := []string{}
268 i := 0 // Index for bugLabels
269 j := 0 // Index for selectedLabels
270 for {
271 if j == len(selectedLabels) {
272 // No more labels to consider
273 break
274 } else if i == len(bugLabels) {
275 // Remaining labels are all new
276 newLabels = append(newLabels, selectedLabels[j].String())
277 j += 1
278 } else if bugLabels[i] == selectedLabels[j] {
279 // Labels match. Move to next pair
280 i += 1
281 j += 1
282 } else {
283 // Labels don't match. Prelabel must have been removed
284 rmLabels = append(rmLabels, bugLabels[i].String())
285 i += 1
286 }
287 }
288
289 if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
290 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
291 }
292
293 return ui.activateWindow(ui.showBug)
294}