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