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