termui.go

  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(nil)
 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(action func(ui *termUI) error) {
 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	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.RepoCacher) 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.Repository(), "", "")
174
175	if err != nil && err != input.ErrEmptyTitle {
176		return err
177	}
178
179	var b cache.BugCacher
180	if err == input.ErrEmptyTitle {
181		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
182	} else {
183		b, err = repo.NewBug(title, message)
184		if err != nil {
185			return err
186		}
187	}
188
189	initGui(func(ui *termUI) error {
190		ui.showBug.SetBug(b)
191		return ui.activateWindow(ui.showBug)
192	})
193
194	return errTerminateMainloop
195}
196
197func addCommentWithEditor(bug cache.BugCacher) 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.BugCacher) 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}