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.RepoCacher
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 := cache.NewRepoCache(repo)
47
48 ui = &termUI{
49 gError: make(chan error, 1),
50 cache: c,
51 bugTable: newBugTable(c),
52 showBug: newShowBug(c),
53 msgPopup: newMsgPopup(),
54 inputPopup: newInputPopup(),
55 }
56
57 ui.activeWindow = ui.bugTable
58
59 initGui(nil)
60
61 err := <-ui.gError
62
63 if err != nil && err != gocui.ErrQuit {
64 return err
65 }
66
67 return nil
68}
69
70func initGui(action func(ui *termUI) error) {
71 g, err := gocui.NewGui(gocui.OutputNormal)
72
73 if err != nil {
74 ui.gError <- err
75 return
76 }
77
78 ui.g = g
79
80 ui.g.SetManagerFunc(layout)
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.RepoCacher) 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.BugCacher
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.BugCacher) 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.BugCacher) 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 maxInt(a, b int) int {
267 if a > b {
268 return a
269 }
270 return b
271}
272
273func minInt(a, b int) int {
274 if a > b {
275 return b
276 }
277 return a
278}