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