termui.go

  1// Package termui contains the interactive terminal UI
  2package termui
  3
  4import (
  5	"github.com/MichaelMure/git-bug/cache"
  6	"github.com/MichaelMure/git-bug/input"
  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.RepoCache
 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(cache *cache.RepoCache) error {
 46	ui = &termUI{
 47		gError:     make(chan error, 1),
 48		cache:      cache,
 49		bugTable:   newBugTable(cache),
 50		showBug:    newShowBug(cache),
 51		msgPopup:   newMsgPopup(),
 52		inputPopup: newInputPopup(),
 53	}
 54
 55	ui.activeWindow = ui.bugTable
 56
 57	initGui(nil)
 58
 59	err := <-ui.gError
 60
 61	if err != nil && err != gocui.ErrQuit {
 62		return err
 63	}
 64
 65	return nil
 66}
 67
 68func initGui(action func(ui *termUI) error) {
 69	g, err := gocui.NewGui(gocui.OutputNormal)
 70
 71	if err != nil {
 72		ui.gError <- err
 73		return
 74	}
 75
 76	ui.g = g
 77
 78	ui.g.SetManagerFunc(layout)
 79
 80	ui.g.InputEsc = true
 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	if action != nil {
 92		err = action(ui)
 93		if err != nil {
 94			ui.g.Close()
 95			ui.g = nil
 96			ui.gError <- err
 97			return
 98		}
 99	}
100
101	err = g.MainLoop()
102
103	if err != nil && err != errTerminateMainloop {
104		if ui.g != nil {
105			ui.g.Close()
106		}
107		ui.gError <- err
108	}
109
110	return
111}
112
113func layout(g *gocui.Gui) error {
114	g.Cursor = false
115
116	if err := ui.activeWindow.layout(g); err != nil {
117		return err
118	}
119
120	if err := ui.msgPopup.layout(g); err != nil {
121		return err
122	}
123
124	if err := ui.inputPopup.layout(g); err != nil {
125		return err
126	}
127
128	return nil
129}
130
131func keybindings(g *gocui.Gui) error {
132	// Quit
133	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
134		return err
135	}
136
137	if err := ui.bugTable.keybindings(g); err != nil {
138		return err
139	}
140
141	if err := ui.showBug.keybindings(g); err != nil {
142		return err
143	}
144
145	if err := ui.msgPopup.keybindings(g); err != nil {
146		return err
147	}
148
149	if err := ui.inputPopup.keybindings(g); err != nil {
150		return err
151	}
152
153	return nil
154}
155
156func quit(g *gocui.Gui, v *gocui.View) error {
157	return gocui.ErrQuit
158}
159
160func newBugWithEditor(repo *cache.RepoCache) error {
161	// This is somewhat hacky.
162	// As there is no way to pause gocui, run the editor and restart gocui,
163	// we have to stop it entirely and start a new one later.
164	//
165	// - an error channel is used to route the returned error of this new
166	// 		instance into the original launch function
167	// - a custom error (errTerminateMainloop) is used to terminate the original
168	//		instance's mainLoop. This error is then filtered.
169
170	ui.g.Close()
171	ui.g = nil
172
173	title, message, err := input.BugCreateEditorInput(ui.cache, "", "")
174
175	if err != nil && err != input.ErrEmptyTitle {
176		return err
177	}
178
179	var b *cache.BugCache
180	if err == input.ErrEmptyTitle {
181		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
182		initGui(nil)
183
184		return errTerminateMainloop
185	} else {
186		b, err = repo.NewBug(title, message)
187		if err != nil {
188			return err
189		}
190
191		initGui(func(ui *termUI) error {
192			ui.showBug.SetBug(b)
193			return ui.activateWindow(ui.showBug)
194		})
195
196		return errTerminateMainloop
197	}
198}
199
200func addCommentWithEditor(bug *cache.BugCache) error {
201	// This is somewhat hacky.
202	// As there is no way to pause gocui, run the editor and restart gocui,
203	// we have to stop it entirely and start a new one later.
204	//
205	// - an error channel is used to route the returned error of this new
206	// 		instance into the original launch function
207	// - a custom error (errTerminateMainloop) is used to terminate the original
208	//		instance's mainLoop. This error is then filtered.
209
210	ui.g.Close()
211	ui.g = nil
212
213	message, err := input.BugCommentEditorInput(ui.cache)
214
215	if err != nil && err != input.ErrEmptyMessage {
216		return err
217	}
218
219	if err == input.ErrEmptyMessage {
220		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
221	} else {
222		err := bug.AddComment(message)
223		if err != nil {
224			return err
225		}
226	}
227
228	initGui(nil)
229
230	return errTerminateMainloop
231}
232
233func setTitleWithEditor(bug *cache.BugCache) error {
234	// This is somewhat hacky.
235	// As there is no way to pause gocui, run the editor and restart gocui,
236	// we have to stop it entirely and start a new one later.
237	//
238	// - an error channel is used to route the returned error of this new
239	// 		instance into the original launch function
240	// - a custom error (errTerminateMainloop) is used to terminate the original
241	//		instance's mainLoop. This error is then filtered.
242
243	ui.g.Close()
244	ui.g = nil
245
246	snap := bug.Snapshot()
247
248	title, err := input.BugTitleEditorInput(ui.cache, snap.Title)
249
250	if err != nil && err != input.ErrEmptyTitle {
251		return err
252	}
253
254	if err == input.ErrEmptyTitle {
255		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
256	} else if title == snap.Title {
257		ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
258	} else {
259		err := bug.SetTitle(title)
260		if err != nil {
261			return err
262		}
263	}
264
265	initGui(nil)
266
267	return errTerminateMainloop
268}
269
270func editQueryWithEditor(bt *bugTable) error {
271	// This is somewhat hacky.
272	// As there is no way to pause gocui, run the editor and restart gocui,
273	// we have to stop it entirely and start a new one later.
274	//
275	// - an error channel is used to route the returned error of this new
276	// 		instance into the original launch function
277	// - a custom error (errTerminateMainloop) is used to terminate the original
278	//		instance's mainLoop. This error is then filtered.
279
280	ui.g.Close()
281	ui.g = nil
282
283	queryStr, err := input.QueryEditorInput(bt.repo, bt.queryStr)
284
285	if err != nil {
286		return err
287	}
288
289	bt.queryStr = queryStr
290
291	query, err := cache.ParseQuery(queryStr)
292
293	if err != nil {
294		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
295	} else {
296		bt.query = query
297	}
298
299	initGui(nil)
300
301	return errTerminateMainloop
302}
303
304func maxInt(a, b int) int {
305	if a > b {
306		return a
307	}
308	return b
309}
310
311func minInt(a, b int) int {
312	if a > b {
313		return b
314	}
315	return a
316}