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