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 } else {
183 b, err = repo.NewBug(title, message)
184 if err != nil {
185 return err
186 }
187 }
188
189 initGui(func(ui *termUI) error {
190 ui.showBug.SetBug(b)
191 return ui.activateWindow(ui.showBug)
192 })
193
194 return errTerminateMainloop
195}
196
197func addCommentWithEditor(bug cache.BugCacher) 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.BugCacher) 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}