termui.go

  1package termui
  2
  3import (
  4	"github.com/MichaelMure/git-bug/cache"
  5	"github.com/MichaelMure/git-bug/input"
  6	"github.com/jroimartin/gocui"
  7	"github.com/pkg/errors"
  8)
  9
 10var errTerminateMainloop = errors.New("terminate gocui mainloop")
 11
 12type termUI struct {
 13	g      *gocui.Gui
 14	gError chan error
 15	cache  *cache.RepoCache
 16
 17	activeWindow window
 18
 19	bugTable   *bugTable
 20	showBug    *showBug
 21	msgPopup   *msgPopup
 22	inputPopup *inputPopup
 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
 43// Run will launch the termUI in the terminal
 44func Run(cache *cache.RepoCache) error {
 45	ui = &termUI{
 46		gError:     make(chan error, 1),
 47		cache:      cache,
 48		bugTable:   newBugTable(cache),
 49		showBug:    newShowBug(cache),
 50		msgPopup:   newMsgPopup(),
 51		inputPopup: newInputPopup(),
 52	}
 53
 54	ui.activeWindow = ui.bugTable
 55
 56	initGui(nil)
 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(action func(ui *termUI) error) {
 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	ui.g.InputEsc = true
 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	if action != nil {
 91		err = action(ui)
 92		if err != nil {
 93			ui.g.Close()
 94			ui.g = nil
 95			ui.gError <- err
 96			return
 97		}
 98	}
 99
100	err = g.MainLoop()
101
102	if err != nil && err != errTerminateMainloop {
103		if ui.g != nil {
104			ui.g.Close()
105		}
106		ui.gError <- err
107	}
108
109	return
110}
111
112func layout(g *gocui.Gui) error {
113	g.Cursor = false
114
115	if err := ui.activeWindow.layout(g); err != nil {
116		return err
117	}
118
119	if err := ui.msgPopup.layout(g); err != nil {
120		return err
121	}
122
123	if err := ui.inputPopup.layout(g); err != nil {
124		return err
125	}
126
127	return nil
128}
129
130func keybindings(g *gocui.Gui) error {
131	// Quit
132	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
133		return err
134	}
135
136	if err := ui.bugTable.keybindings(g); err != nil {
137		return err
138	}
139
140	if err := ui.showBug.keybindings(g); err != nil {
141		return err
142	}
143
144	if err := ui.msgPopup.keybindings(g); err != nil {
145		return err
146	}
147
148	if err := ui.inputPopup.keybindings(g); err != nil {
149		return err
150	}
151
152	return nil
153}
154
155func quit(g *gocui.Gui, v *gocui.View) error {
156	return gocui.ErrQuit
157}
158
159func newBugWithEditor(repo *cache.RepoCache) error {
160	// This is somewhat hacky.
161	// As there is no way to pause gocui, run the editor and restart gocui,
162	// we have to stop it entirely and start a new one later.
163	//
164	// - an error channel is used to route the returned error of this new
165	// 		instance into the original launch function
166	// - a custom error (errTerminateMainloop) is used to terminate the original
167	//		instance's mainLoop. This error is then filtered.
168
169	ui.g.Close()
170	ui.g = nil
171
172	title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
173
174	if err != nil && err != input.ErrEmptyTitle {
175		return err
176	}
177
178	var b *cache.BugCache
179	if err == input.ErrEmptyTitle {
180		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
181		initGui(nil)
182
183		return errTerminateMainloop
184	} else {
185		b, err = repo.NewBug(title, message)
186		if err != nil {
187			return err
188		}
189
190		initGui(func(ui *termUI) error {
191			ui.showBug.SetBug(b)
192			return ui.activateWindow(ui.showBug)
193		})
194
195		return errTerminateMainloop
196	}
197}
198
199func addCommentWithEditor(bug *cache.BugCache) error {
200	// This is somewhat hacky.
201	// As there is no way to pause gocui, run the editor and restart gocui,
202	// we have to stop it entirely and start a new one later.
203	//
204	// - an error channel is used to route the returned error of this new
205	// 		instance into the original launch function
206	// - a custom error (errTerminateMainloop) is used to terminate the original
207	//		instance's mainLoop. This error is then filtered.
208
209	ui.g.Close()
210	ui.g = nil
211
212	message, err := input.BugCommentEditorInput(ui.cache.Repository())
213
214	if err != nil && err != input.ErrEmptyMessage {
215		return err
216	}
217
218	if err == input.ErrEmptyMessage {
219		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
220	} else {
221		err := bug.AddComment(message)
222		if err != nil {
223			return err
224		}
225	}
226
227	initGui(nil)
228
229	return errTerminateMainloop
230}
231
232func setTitleWithEditor(bug *cache.BugCache) error {
233	// This is somewhat hacky.
234	// As there is no way to pause gocui, run the editor and restart gocui,
235	// we have to stop it entirely and start a new one later.
236	//
237	// - an error channel is used to route the returned error of this new
238	// 		instance into the original launch function
239	// - a custom error (errTerminateMainloop) is used to terminate the original
240	//		instance's mainLoop. This error is then filtered.
241
242	ui.g.Close()
243	ui.g = nil
244
245	title, err := input.BugTitleEditorInput(ui.cache.Repository(), bug.Snapshot().Title)
246
247	if err != nil && err != input.ErrEmptyTitle {
248		return err
249	}
250
251	if err == input.ErrEmptyTitle {
252		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
253	} else {
254		err := bug.SetTitle(title)
255		if err != nil {
256			return err
257		}
258	}
259
260	initGui(nil)
261
262	return errTerminateMainloop
263}
264
265func maxInt(a, b int) int {
266	if a > b {
267		return a
268	}
269	return b
270}
271
272func minInt(a, b int) int {
273	if a > b {
274		return b
275	}
276	return a
277}