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	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	if action != nil {
 89		err = action(ui)
 90		if err != nil {
 91			ui.g.Close()
 92			ui.g = nil
 93			ui.gError <- err
 94			return
 95		}
 96	}
 97
 98	err = g.MainLoop()
 99
100	if err != nil && err != errTerminateMainloop {
101		if ui.g != nil {
102			ui.g.Close()
103		}
104		ui.gError <- err
105	}
106
107	return
108}
109
110func layout(g *gocui.Gui) error {
111	g.Cursor = false
112
113	if err := ui.activeWindow.layout(g); err != nil {
114		return err
115	}
116
117	if err := ui.msgPopup.layout(g); err != nil {
118		return err
119	}
120
121	if err := ui.inputPopup.layout(g); err != nil {
122		return err
123	}
124
125	return nil
126}
127
128func keybindings(g *gocui.Gui) error {
129	// Quit
130	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
131		return err
132	}
133
134	if err := ui.bugTable.keybindings(g); err != nil {
135		return err
136	}
137
138	if err := ui.showBug.keybindings(g); err != nil {
139		return err
140	}
141
142	if err := ui.msgPopup.keybindings(g); err != nil {
143		return err
144	}
145
146	if err := ui.inputPopup.keybindings(g); err != nil {
147		return err
148	}
149
150	return nil
151}
152
153func quit(g *gocui.Gui, v *gocui.View) error {
154	return gocui.ErrQuit
155}
156
157func newBugWithEditor(repo *cache.RepoCache) error {
158	// This is somewhat hacky.
159	// As there is no way to pause gocui, run the editor and restart gocui,
160	// we have to stop it entirely and start a new one later.
161	//
162	// - an error channel is used to route the returned error of this new
163	// 		instance into the original launch function
164	// - a custom error (errTerminateMainloop) is used to terminate the original
165	//		instance's mainLoop. This error is then filtered.
166
167	ui.g.Close()
168	ui.g = nil
169
170	title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
171
172	if err != nil && err != input.ErrEmptyTitle {
173		return err
174	}
175
176	var b *cache.BugCache
177	if err == input.ErrEmptyTitle {
178		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
179		initGui(nil)
180
181		return errTerminateMainloop
182	} else {
183		b, err = repo.NewBug(title, message)
184		if err != nil {
185			return err
186		}
187
188		initGui(func(ui *termUI) error {
189			ui.showBug.SetBug(b)
190			return ui.activateWindow(ui.showBug)
191		})
192
193		return errTerminateMainloop
194	}
195}
196
197func addCommentWithEditor(bug *cache.BugCache) 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.BugCache) 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}