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 err = keybindings(ui.g)
80
81 if err != nil {
82 ui.g.Close()
83 ui.g = nil
84 ui.gError <- err
85 return
86 }
87
88 if action != nil {
89 err = action(ui)
90 if err != nil {
91 ui.g.Close()
92 ui.g = nil
93 ui.gError <- err
94 return
95 }
96 }
97
98 err = g.MainLoop()
99
100 if err != nil && err != errTerminateMainloop {
101 if ui.g != nil {
102 ui.g.Close()
103 }
104 ui.gError <- err
105 }
106
107 return
108}
109
110func layout(g *gocui.Gui) error {
111 g.Cursor = false
112
113 if err := ui.activeWindow.layout(g); err != nil {
114 return err
115 }
116
117 if err := ui.msgPopup.layout(g); err != nil {
118 return err
119 }
120
121 if err := ui.inputPopup.layout(g); err != nil {
122 return err
123 }
124
125 return nil
126}
127
128func keybindings(g *gocui.Gui) error {
129 // Quit
130 if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
131 return err
132 }
133
134 if err := ui.bugTable.keybindings(g); err != nil {
135 return err
136 }
137
138 if err := ui.showBug.keybindings(g); err != nil {
139 return err
140 }
141
142 if err := ui.msgPopup.keybindings(g); err != nil {
143 return err
144 }
145
146 if err := ui.inputPopup.keybindings(g); err != nil {
147 return err
148 }
149
150 return nil
151}
152
153func quit(g *gocui.Gui, v *gocui.View) error {
154 return gocui.ErrQuit
155}
156
157func newBugWithEditor(repo *cache.RepoCache) error {
158 // This is somewhat hacky.
159 // As there is no way to pause gocui, run the editor and restart gocui,
160 // we have to stop it entirely and start a new one later.
161 //
162 // - an error channel is used to route the returned error of this new
163 // instance into the original launch function
164 // - a custom error (errTerminateMainloop) is used to terminate the original
165 // instance's mainLoop. This error is then filtered.
166
167 ui.g.Close()
168 ui.g = nil
169
170 title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
171
172 if err != nil && err != input.ErrEmptyTitle {
173 return err
174 }
175
176 var b *cache.BugCache
177 if err == input.ErrEmptyTitle {
178 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
179 initGui(nil)
180
181 return errTerminateMainloop
182 } else {
183 b, err = repo.NewBug(title, message)
184 if err != nil {
185 return err
186 }
187
188 initGui(func(ui *termUI) error {
189 ui.showBug.SetBug(b)
190 return ui.activateWindow(ui.showBug)
191 })
192
193 return errTerminateMainloop
194 }
195}
196
197func addCommentWithEditor(bug *cache.BugCache) error {
198 // This is somewhat hacky.
199 // As there is no way to pause gocui, run the editor and restart gocui,
200 // we have to stop it entirely and start a new one later.
201 //
202 // - an error channel is used to route the returned error of this new
203 // instance into the original launch function
204 // - a custom error (errTerminateMainloop) is used to terminate the original
205 // instance's mainLoop. This error is then filtered.
206
207 ui.g.Close()
208 ui.g = nil
209
210 message, err := input.BugCommentEditorInput(ui.cache.Repository())
211
212 if err != nil && err != input.ErrEmptyMessage {
213 return err
214 }
215
216 if err == input.ErrEmptyMessage {
217 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
218 } else {
219 err := bug.AddComment(message)
220 if err != nil {
221 return err
222 }
223 }
224
225 initGui(nil)
226
227 return errTerminateMainloop
228}
229
230func setTitleWithEditor(bug *cache.BugCache) error {
231 // This is somewhat hacky.
232 // As there is no way to pause gocui, run the editor and restart gocui,
233 // we have to stop it entirely and start a new one later.
234 //
235 // - an error channel is used to route the returned error of this new
236 // instance into the original launch function
237 // - a custom error (errTerminateMainloop) is used to terminate the original
238 // instance's mainLoop. This error is then filtered.
239
240 ui.g.Close()
241 ui.g = nil
242
243 title, err := input.BugTitleEditorInput(ui.cache.Repository(), bug.Snapshot().Title)
244
245 if err != nil && err != input.ErrEmptyTitle {
246 return err
247 }
248
249 if err == input.ErrEmptyTitle {
250 ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
251 } else {
252 err := bug.SetTitle(title)
253 if err != nil {
254 return err
255 }
256 }
257
258 initGui(nil)
259
260 return errTerminateMainloop
261}
262
263func maxInt(a, b int) int {
264 if a > b {
265 return a
266 }
267 return b
268}
269
270func minInt(a, b int) int {
271 if a > b {
272 return b
273 }
274 return a
275}