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