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