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