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