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