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