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