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