1// Package termui contains the interactive terminal UI
2package termui
3
4import (
5 "fmt"
6
7 "github.com/awesome-gocui/gocui"
8 "github.com/pkg/errors"
9
10 errors2 "github.com/go-errors/errors"
11
12 "github.com/MichaelMure/git-bug/cache"
13 "github.com/MichaelMure/git-bug/entity"
14 "github.com/MichaelMure/git-bug/input"
15 "github.com/MichaelMure/git-bug/query"
16)
17
18var errTerminateMainloop = errors.New("terminate gocui mainloop")
19
20type termUI struct {
21 g *gocui.Gui
22 gError chan error
23 cache *cache.RepoCache
24
25 activeWindow window
26
27 bugTable *bugTable
28 showBug *showBug
29 labelSelect *labelSelect
30 msgPopup *msgPopup
31 inputPopup *inputPopup
32}
33
34func (tui *termUI) activateWindow(window window) error {
35 if err := tui.activeWindow.disable(tui.g); err != nil {
36 return err
37 }
38
39 tui.activeWindow = window
40
41 return nil
42}
43
44var ui *termUI
45
46type window interface {
47 keybindings(g *gocui.Gui) error
48 layout(g *gocui.Gui) error
49 disable(g *gocui.Gui) error
50}
51
52// Run will launch the termUI in the terminal
53func Run(cache *cache.RepoCache) error {
54 ui = &termUI{
55 gError: make(chan error, 1),
56 cache: cache,
57 bugTable: newBugTable(cache),
58 showBug: newShowBug(cache),
59 labelSelect: newLabelSelect(),
60 msgPopup: newMsgPopup(),
61 inputPopup: newInputPopup(),
62 }
63
64 ui.activeWindow = ui.bugTable
65
66 initGui(nil)
67
68 err := <-ui.gError
69
70 if err != nil && err != gocui.ErrQuit {
71 if e, ok := err.(*errors2.Error); ok {
72 fmt.Println(e.ErrorStack())
73 }
74 return err
75 }
76
77 return nil
78}
79
80func initGui(action func(ui *termUI) error) {
81 g, err := gocui.NewGui(gocui.Output256, false)
82
83 if err != nil {
84 ui.gError <- err
85 return
86 }
87
88 ui.g = g
89
90 ui.g.SetManagerFunc(layout)
91
92 ui.g.InputEsc = true
93
94 err = keybindings(ui.g)
95
96 if err != nil {
97 ui.g.Close()
98 ui.g = nil
99 ui.gError <- err
100 return
101 }
102
103 if action != nil {
104 err = action(ui)
105 if err != nil {
106 ui.g.Close()
107 ui.g = nil
108 ui.gError <- err
109 return
110 }
111 }
112
113 err = g.MainLoop()
114
115 if err != nil && err != errTerminateMainloop {
116 if ui.g != nil {
117 ui.g.Close()
118 }
119 ui.gError <- err
120 }
121}
122
123func layout(g *gocui.Gui) error {
124 g.Cursor = false
125
126 if err := ui.activeWindow.layout(g); err != nil {
127 return err
128 }
129
130 if err := ui.msgPopup.layout(g); err != nil {
131 return err
132 }
133
134 if err := ui.inputPopup.layout(g); err != nil {
135 return err
136 }
137
138 return nil
139}
140
141func keybindings(g *gocui.Gui) error {
142 // Quit
143 if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
144 return err
145 }
146
147 if err := ui.bugTable.keybindings(g); err != nil {
148 return err
149 }
150
151 if err := ui.showBug.keybindings(g); err != nil {
152 return err
153 }
154
155 if err := ui.labelSelect.keybindings(g); err != nil {
156 return err
157 }
158
159 if err := ui.msgPopup.keybindings(g); err != nil {
160 return err
161 }
162
163 if err := ui.inputPopup.keybindings(g); err != nil {
164 return err
165 }
166
167 return nil
168}
169
170func quit(g *gocui.Gui, v *gocui.View) error {
171 return gocui.ErrQuit
172}
173
174func newBugWithEditor(repo *cache.RepoCache) error {
175 // This is somewhat hacky.
176 // As there is no way to pause gocui, run the editor and restart gocui,
177 // we have to stop it entirely and start a new one later.
178 //
179 // - an error channel is used to route the returned error of this new
180 // instance into the original launch function
181 // - a custom error (errTerminateMainloop) is used to terminate the original
182 // instance's mainLoop. This error is then filtered.
183
184 ui.g.Close()
185 ui.g = nil
186
187 title, message, err := input.BugCreateEditorInput(ui.cache, "", "")
188
189 if err != nil && err != input.ErrEmptyTitle {
190 return err
191 }
192
193 var b *cache.BugCache
194 if err == input.ErrEmptyTitle {
195 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
196 initGui(nil)
197
198 return errTerminateMainloop
199 } else {
200 b, _, err = repo.NewBug(title, message)
201 if err != nil {
202 return err
203 }
204
205 initGui(func(ui *termUI) error {
206 ui.showBug.SetBug(b)
207 return ui.activateWindow(ui.showBug)
208 })
209
210 return errTerminateMainloop
211 }
212}
213
214func addCommentWithEditor(bug *cache.BugCache) error {
215 // This is somewhat hacky.
216 // As there is no way to pause gocui, run the editor and restart gocui,
217 // we have to stop it entirely and start a new one later.
218 //
219 // - an error channel is used to route the returned error of this new
220 // instance into the original launch function
221 // - a custom error (errTerminateMainloop) is used to terminate the original
222 // instance's mainLoop. This error is then filtered.
223
224 ui.g.Close()
225 ui.g = nil
226
227 message, err := input.BugCommentEditorInput(ui.cache, "")
228
229 if err != nil && err != input.ErrEmptyMessage {
230 return err
231 }
232
233 if err == input.ErrEmptyMessage {
234 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
235 } else {
236 _, err := bug.AddComment(message)
237 if err != nil {
238 return err
239 }
240 }
241
242 initGui(nil)
243
244 return errTerminateMainloop
245}
246
247func editCommentWithEditor(bug *cache.BugCache, target entity.Id, preMessage string) error {
248 // This is somewhat hacky.
249 // As there is no way to pause gocui, run the editor and restart gocui,
250 // we have to stop it entirely and start a new one later.
251 //
252 // - an error channel is used to route the returned error of this new
253 // instance into the original launch function
254 // - a custom error (errTerminateMainloop) is used to terminate the original
255 // instance's mainLoop. This error is then filtered.
256
257 ui.g.Close()
258 ui.g = nil
259
260 message, err := input.BugCommentEditorInput(ui.cache, preMessage)
261 if err != nil && err != input.ErrEmptyMessage {
262 return err
263 }
264
265 if err == input.ErrEmptyMessage {
266 // TODO: Allow comments to be deleted?
267 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
268 } else if message == preMessage {
269 ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
270 } else {
271 _, err := bug.EditComment(target, message)
272 if err != nil {
273 return err
274 }
275 }
276
277 initGui(nil)
278
279 return errTerminateMainloop
280}
281
282func setTitleWithEditor(bug *cache.BugCache) error {
283 // This is somewhat hacky.
284 // As there is no way to pause gocui, run the editor and restart gocui,
285 // we have to stop it entirely and start a new one later.
286 //
287 // - an error channel is used to route the returned error of this new
288 // instance into the original launch function
289 // - a custom error (errTerminateMainloop) is used to terminate the original
290 // instance's mainLoop. This error is then filtered.
291
292 ui.g.Close()
293 ui.g = nil
294
295 snap := bug.Snapshot()
296
297 title, err := input.BugTitleEditorInput(ui.cache, snap.Title)
298
299 if err != nil && err != input.ErrEmptyTitle {
300 return err
301 }
302
303 if err == input.ErrEmptyTitle {
304 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
305 } else if title == snap.Title {
306 ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
307 } else {
308 _, err := bug.SetTitle(title)
309 if err != nil {
310 return err
311 }
312 }
313
314 initGui(nil)
315
316 return errTerminateMainloop
317}
318
319func editQueryWithEditor(bt *bugTable) error {
320 // This is somewhat hacky.
321 // As there is no way to pause gocui, run the editor and restart gocui,
322 // we have to stop it entirely and start a new one later.
323 //
324 // - an error channel is used to route the returned error of this new
325 // instance into the original launch function
326 // - a custom error (errTerminateMainloop) is used to terminate the original
327 // instance's mainLoop. This error is then filtered.
328
329 ui.g.Close()
330 ui.g = nil
331
332 queryStr, err := input.QueryEditorInput(bt.repo, bt.queryStr)
333
334 if err != nil {
335 return err
336 }
337
338 bt.queryStr = queryStr
339
340 q, err := query.Parse(queryStr)
341
342 if err != nil {
343 ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
344 } else {
345 bt.query = q
346 }
347
348 initGui(nil)
349
350 return errTerminateMainloop
351}
352
353func maxInt(a, b int) int {
354 if a > b {
355 return a
356 }
357 return b
358}
359
360func minInt(a, b int) int {
361 if a > b {
362 return b
363 }
364 return a
365}