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()
 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() {
 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	err = g.MainLoop()
 92
 93	if err != nil && err != errTerminateMainloop {
 94		if ui.g != nil {
 95			ui.g.Close()
 96		}
 97		ui.gError <- err
 98	}
 99
100	return
101}
102
103func layout(g *gocui.Gui) error {
104	g.Cursor = false
105
106	if err := ui.activeWindow.layout(g); err != nil {
107		return err
108	}
109
110	if err := ui.msgPopup.layout(g); err != nil {
111		return err
112	}
113
114	if err := ui.inputPopup.layout(g); err != nil {
115		return err
116	}
117
118	return nil
119}
120
121func keybindings(g *gocui.Gui) error {
122	// Quit
123	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
124		return err
125	}
126
127	if err := ui.bugTable.keybindings(g); err != nil {
128		return err
129	}
130
131	if err := ui.showBug.keybindings(g); err != nil {
132		return err
133	}
134
135	if err := ui.msgPopup.keybindings(g); err != nil {
136		return err
137	}
138
139	if err := ui.inputPopup.keybindings(g); err != nil {
140		return err
141	}
142
143	return nil
144}
145
146func quit(g *gocui.Gui, v *gocui.View) error {
147	return gocui.ErrQuit
148}
149
150func newBugWithEditor(repo cache.RepoCacher) error {
151	// This is somewhat hacky.
152	// As there is no way to pause gocui, run the editor and restart gocui,
153	// we have to stop it entirely and start a new one later.
154	//
155	// - an error channel is used to route the returned error of this new
156	// 		instance into the original launch function
157	// - a custom error (errTerminateMainloop) is used to terminate the original
158	//		instance's mainLoop. This error is then filtered.
159
160	ui.g.Close()
161	ui.g = nil
162
163	title, message, err := input.BugCreateEditorInput(ui.cache.Repository(), "", "")
164
165	if err != nil && err != input.ErrEmptyTitle {
166		return err
167	}
168
169	if err == input.ErrEmptyTitle {
170		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
171	} else {
172		_, err := repo.NewBug(title, message)
173		if err != nil {
174			return err
175		}
176	}
177
178	initGui()
179
180	return errTerminateMainloop
181}
182
183func addCommentWithEditor(bug cache.BugCacher) error {
184	// This is somewhat hacky.
185	// As there is no way to pause gocui, run the editor and restart gocui,
186	// we have to stop it entirely and start a new one later.
187	//
188	// - an error channel is used to route the returned error of this new
189	// 		instance into the original launch function
190	// - a custom error (errTerminateMainloop) is used to terminate the original
191	//		instance's mainLoop. This error is then filtered.
192
193	ui.g.Close()
194	ui.g = nil
195
196	message, err := input.BugCommentEditorInput(ui.cache.Repository())
197
198	if err != nil && err != input.ErrEmptyMessage {
199		return err
200	}
201
202	if err == input.ErrEmptyMessage {
203		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
204	} else {
205		err := bug.AddComment(message)
206		if err != nil {
207			return err
208		}
209	}
210
211	initGui()
212
213	return errTerminateMainloop
214}
215
216func setTitleWithEditor(bug cache.BugCacher) error {
217	// This is somewhat hacky.
218	// As there is no way to pause gocui, run the editor and restart gocui,
219	// we have to stop it entirely and start a new one later.
220	//
221	// - an error channel is used to route the returned error of this new
222	// 		instance into the original launch function
223	// - a custom error (errTerminateMainloop) is used to terminate the original
224	//		instance's mainLoop. This error is then filtered.
225
226	ui.g.Close()
227	ui.g = nil
228
229	title, err := input.BugTitleEditorInput(ui.cache.Repository(), bug.Snapshot().Title)
230
231	if err != nil && err != input.ErrEmptyTitle {
232		return err
233	}
234
235	if err == input.ErrEmptyTitle {
236		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
237	} else {
238		err := bug.SetTitle(title)
239		if err != nil {
240			return err
241		}
242	}
243
244	initGui()
245
246	return errTerminateMainloop
247}
248
249func maxInt(a, b int) int {
250	if a > b {
251		return a
252	}
253	return b
254}
255
256func minInt(a, b int) int {
257	if a > b {
258		return b
259	}
260	return a
261}