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