2018-09-16 14:50:53 +03:00
|
|
|
// Package termui contains the interactive terminal UI
|
2018-07-30 18:00:10 +03:00
|
|
|
package termui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/MichaelMure/git-bug/cache"
|
2018-07-31 17:43:43 +03:00
|
|
|
"github.com/MichaelMure/git-bug/input"
|
2018-10-04 19:31:46 +03:00
|
|
|
"github.com/MichaelMure/git-bug/util/git"
|
2018-12-25 22:46:55 +03:00
|
|
|
"github.com/MichaelMure/gocui"
|
2018-07-31 17:43:43 +03:00
|
|
|
"github.com/pkg/errors"
|
2018-07-30 18:00:10 +03:00
|
|
|
)
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
var errTerminateMainloop = errors.New("terminate gocui mainloop")
|
|
|
|
|
2018-07-30 18:00:10 +03:00
|
|
|
type termUI struct {
|
2018-08-01 03:15:40 +03:00
|
|
|
g *gocui.Gui
|
|
|
|
gError chan error
|
2018-08-23 22:24:57 +03:00
|
|
|
cache *cache.RepoCache
|
2018-08-01 03:15:40 +03:00
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
activeWindow window
|
|
|
|
|
2018-10-04 22:21:27 +03:00
|
|
|
bugTable *bugTable
|
|
|
|
showBug *showBug
|
|
|
|
labelSelect *labelSelect
|
|
|
|
msgPopup *msgPopup
|
|
|
|
inputPopup *inputPopup
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-08-01 03:15:40 +03:00
|
|
|
func (tui *termUI) activateWindow(window window) error {
|
|
|
|
if err := tui.activeWindow.disable(tui.g); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tui.activeWindow = window
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-30 18:00:10 +03:00
|
|
|
var ui *termUI
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
type window interface {
|
|
|
|
keybindings(g *gocui.Gui) error
|
|
|
|
layout(g *gocui.Gui) error
|
2018-08-01 03:15:40 +03:00
|
|
|
disable(g *gocui.Gui) error
|
2018-07-31 17:43:43 +03:00
|
|
|
}
|
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// Run will launch the termUI in the terminal
|
2018-08-31 14:18:03 +03:00
|
|
|
func Run(cache *cache.RepoCache) error {
|
2018-07-30 18:00:10 +03:00
|
|
|
ui = &termUI{
|
2018-10-04 22:21:27 +03:00
|
|
|
gError: make(chan error, 1),
|
|
|
|
cache: cache,
|
|
|
|
bugTable: newBugTable(cache),
|
|
|
|
showBug: newShowBug(cache),
|
|
|
|
labelSelect: newLabelSelect(),
|
|
|
|
msgPopup: newMsgPopup(),
|
|
|
|
inputPopup: newInputPopup(),
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.activeWindow = ui.bugTable
|
|
|
|
|
2018-08-17 13:37:58 +03:00
|
|
|
initGui(nil)
|
2018-07-31 17:43:43 +03:00
|
|
|
|
2018-08-31 14:18:03 +03:00
|
|
|
err := <-ui.gError
|
2018-07-31 17:43:43 +03:00
|
|
|
|
|
|
|
if err != nil && err != gocui.ErrQuit {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-17 13:37:58 +03:00
|
|
|
func initGui(action func(ui *termUI) error) {
|
2018-07-30 18:00:10 +03:00
|
|
|
g, err := gocui.NewGui(gocui.OutputNormal)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.gError <- err
|
|
|
|
return
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.g = g
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.g.SetManagerFunc(layout)
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-09-11 18:45:59 +03:00
|
|
|
ui.g.InputEsc = true
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
err = keybindings(ui.g)
|
2018-07-30 18:00:10 +03:00
|
|
|
|
|
|
|
if err != nil {
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.g.Close()
|
2018-07-31 23:19:11 +03:00
|
|
|
ui.g = nil
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.gError <- err
|
|
|
|
return
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-08-17 13:37:58 +03:00
|
|
|
if action != nil {
|
|
|
|
err = action(ui)
|
|
|
|
if err != nil {
|
|
|
|
ui.g.Close()
|
|
|
|
ui.g = nil
|
|
|
|
ui.gError <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-30 18:00:10 +03:00
|
|
|
err = g.MainLoop()
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
if err != nil && err != errTerminateMainloop {
|
2018-07-31 23:19:11 +03:00
|
|
|
if ui.g != nil {
|
|
|
|
ui.g.Close()
|
|
|
|
}
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.gError <- err
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
return
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func layout(g *gocui.Gui) error {
|
2018-07-31 17:43:43 +03:00
|
|
|
g.Cursor = false
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
if err := ui.activeWindow.layout(g); err != nil {
|
2018-07-30 18:00:10 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-12 22:09:30 +03:00
|
|
|
if err := ui.msgPopup.layout(g); err != nil {
|
2018-07-31 23:19:11 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-12 03:42:03 +03:00
|
|
|
if err := ui.inputPopup.layout(g); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-07-30 18:00:10 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func keybindings(g *gocui.Gui) error {
|
2018-07-31 17:43:43 +03:00
|
|
|
// Quit
|
|
|
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
2018-07-30 18:00:10 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
if err := ui.bugTable.keybindings(g); err != nil {
|
2018-07-30 19:09:44 +03:00
|
|
|
return err
|
|
|
|
}
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-08-01 03:15:40 +03:00
|
|
|
if err := ui.showBug.keybindings(g); err != nil {
|
|
|
|
return err
|
2018-10-04 22:21:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := ui.labelSelect.keybindings(g); err != nil {
|
|
|
|
return err
|
2018-08-01 03:15:40 +03:00
|
|
|
}
|
|
|
|
|
2018-08-12 22:09:30 +03:00
|
|
|
if err := ui.msgPopup.keybindings(g); err != nil {
|
2018-07-31 23:19:11 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-12 03:42:03 +03:00
|
|
|
if err := ui.inputPopup.keybindings(g); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-07-30 18:00:10 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
|
|
|
return gocui.ErrQuit
|
|
|
|
}
|
|
|
|
|
2018-08-23 22:24:57 +03:00
|
|
|
func newBugWithEditor(repo *cache.RepoCache) error {
|
2018-07-31 17:43:43 +03:00
|
|
|
// This is somewhat hacky.
|
2018-08-02 17:35:13 +03:00
|
|
|
// As there is no way to pause gocui, run the editor and restart gocui,
|
2018-07-31 17:43:43 +03:00
|
|
|
// we have to stop it entirely and start a new one later.
|
|
|
|
//
|
|
|
|
// - an error channel is used to route the returned error of this new
|
|
|
|
// instance into the original launch function
|
|
|
|
// - a custom error (errTerminateMainloop) is used to terminate the original
|
|
|
|
// instance's mainLoop. This error is then filtered.
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
ui.g.Close()
|
2018-07-31 23:19:11 +03:00
|
|
|
ui.g = nil
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-09-21 19:18:51 +03:00
|
|
|
title, message, err := input.BugCreateEditorInput(ui.cache, "", "")
|
2018-07-30 18:00:10 +03:00
|
|
|
|
2018-07-31 23:19:11 +03:00
|
|
|
if err != nil && err != input.ErrEmptyTitle {
|
2018-07-30 18:00:10 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-23 22:24:57 +03:00
|
|
|
var b *cache.BugCache
|
2018-07-31 23:19:11 +03:00
|
|
|
if err == input.ErrEmptyTitle {
|
2018-08-12 22:09:30 +03:00
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
|
2018-08-17 17:42:10 +03:00
|
|
|
initGui(nil)
|
|
|
|
|
|
|
|
return errTerminateMainloop
|
2018-07-31 23:19:11 +03:00
|
|
|
} else {
|
2019-06-16 22:04:36 +03:00
|
|
|
b, _, err = repo.NewBug(title, message)
|
2018-08-02 17:35:13 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-17 17:42:10 +03:00
|
|
|
initGui(func(ui *termUI) error {
|
|
|
|
ui.showBug.SetBug(b)
|
|
|
|
return ui.activateWindow(ui.showBug)
|
|
|
|
})
|
2018-08-02 17:35:13 +03:00
|
|
|
|
2018-08-17 17:42:10 +03:00
|
|
|
return errTerminateMainloop
|
|
|
|
}
|
2018-08-02 17:35:13 +03:00
|
|
|
}
|
|
|
|
|
2018-08-23 22:24:57 +03:00
|
|
|
func addCommentWithEditor(bug *cache.BugCache) error {
|
2018-08-02 17:35:13 +03:00
|
|
|
// This is somewhat hacky.
|
|
|
|
// As there is no way to pause gocui, run the editor and restart gocui,
|
|
|
|
// we have to stop it entirely and start a new one later.
|
|
|
|
//
|
|
|
|
// - an error channel is used to route the returned error of this new
|
|
|
|
// instance into the original launch function
|
|
|
|
// - a custom error (errTerminateMainloop) is used to terminate the original
|
|
|
|
// instance's mainLoop. This error is then filtered.
|
|
|
|
|
|
|
|
ui.g.Close()
|
|
|
|
ui.g = nil
|
|
|
|
|
2018-10-04 19:29:19 +03:00
|
|
|
message, err := input.BugCommentEditorInput(ui.cache, "")
|
2018-08-02 17:35:13 +03:00
|
|
|
|
|
|
|
if err != nil && err != input.ErrEmptyMessage {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err == input.ErrEmptyMessage {
|
2018-08-12 22:09:30 +03:00
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
|
2018-08-02 17:35:13 +03:00
|
|
|
} else {
|
2019-02-24 14:58:04 +03:00
|
|
|
_, err := bug.AddComment(message)
|
2018-08-02 17:35:13 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-17 13:37:58 +03:00
|
|
|
initGui(nil)
|
2018-08-02 17:35:13 +03:00
|
|
|
|
|
|
|
return errTerminateMainloop
|
|
|
|
}
|
|
|
|
|
2018-10-04 19:31:46 +03:00
|
|
|
func editCommentWithEditor(bug *cache.BugCache, target git.Hash, preMessage string) error {
|
|
|
|
// This is somewhat hacky.
|
|
|
|
// As there is no way to pause gocui, run the editor and restart gocui,
|
|
|
|
// we have to stop it entirely and start a new one later.
|
|
|
|
//
|
|
|
|
// - an error channel is used to route the returned error of this new
|
|
|
|
// instance into the original launch function
|
|
|
|
// - a custom error (errTerminateMainloop) is used to terminate the original
|
|
|
|
// instance's mainLoop. This error is then filtered.
|
|
|
|
|
|
|
|
ui.g.Close()
|
|
|
|
ui.g = nil
|
|
|
|
|
|
|
|
message, err := input.BugCommentEditorInput(ui.cache, preMessage)
|
|
|
|
if err != nil && err != input.ErrEmptyMessage {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err == input.ErrEmptyMessage {
|
|
|
|
// TODO: Allow comments to be deleted?
|
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
|
|
|
|
} else if message == preMessage {
|
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
|
|
|
|
} else {
|
2019-02-24 14:58:04 +03:00
|
|
|
_, err := bug.EditComment(target, message)
|
2018-10-04 19:31:46 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
initGui(nil)
|
|
|
|
|
|
|
|
return errTerminateMainloop
|
|
|
|
}
|
|
|
|
|
2018-08-23 22:24:57 +03:00
|
|
|
func setTitleWithEditor(bug *cache.BugCache) error {
|
2018-08-02 17:35:13 +03:00
|
|
|
// This is somewhat hacky.
|
|
|
|
// As there is no way to pause gocui, run the editor and restart gocui,
|
|
|
|
// we have to stop it entirely and start a new one later.
|
|
|
|
//
|
|
|
|
// - an error channel is used to route the returned error of this new
|
|
|
|
// instance into the original launch function
|
|
|
|
// - a custom error (errTerminateMainloop) is used to terminate the original
|
|
|
|
// instance's mainLoop. This error is then filtered.
|
|
|
|
|
|
|
|
ui.g.Close()
|
|
|
|
ui.g = nil
|
|
|
|
|
2018-09-26 17:27:26 +03:00
|
|
|
snap := bug.Snapshot()
|
|
|
|
|
|
|
|
title, err := input.BugTitleEditorInput(ui.cache, snap.Title)
|
2018-08-02 17:35:13 +03:00
|
|
|
|
|
|
|
if err != nil && err != input.ErrEmptyTitle {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err == input.ErrEmptyTitle {
|
2018-08-12 22:09:30 +03:00
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
|
2018-09-26 17:27:26 +03:00
|
|
|
} else if title == snap.Title {
|
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
|
2018-08-02 17:35:13 +03:00
|
|
|
} else {
|
2019-02-24 14:58:04 +03:00
|
|
|
_, err := bug.SetTitle(title)
|
2018-07-31 23:19:11 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
|
|
|
|
2018-08-17 13:37:58 +03:00
|
|
|
initGui(nil)
|
2018-07-31 17:43:43 +03:00
|
|
|
|
|
|
|
return errTerminateMainloop
|
2018-07-30 18:00:10 +03:00
|
|
|
}
|
2018-07-30 19:09:44 +03:00
|
|
|
|
2018-09-11 20:28:32 +03:00
|
|
|
func editQueryWithEditor(bt *bugTable) error {
|
|
|
|
// This is somewhat hacky.
|
|
|
|
// As there is no way to pause gocui, run the editor and restart gocui,
|
|
|
|
// we have to stop it entirely and start a new one later.
|
|
|
|
//
|
|
|
|
// - an error channel is used to route the returned error of this new
|
|
|
|
// instance into the original launch function
|
|
|
|
// - a custom error (errTerminateMainloop) is used to terminate the original
|
|
|
|
// instance's mainLoop. This error is then filtered.
|
|
|
|
|
|
|
|
ui.g.Close()
|
|
|
|
ui.g = nil
|
|
|
|
|
2018-09-21 19:18:51 +03:00
|
|
|
queryStr, err := input.QueryEditorInput(bt.repo, bt.queryStr)
|
2018-09-11 20:28:32 +03:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bt.queryStr = queryStr
|
|
|
|
|
|
|
|
query, err := cache.ParseQuery(queryStr)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
|
|
|
|
} else {
|
|
|
|
bt.query = query
|
|
|
|
}
|
|
|
|
|
|
|
|
initGui(nil)
|
|
|
|
|
|
|
|
return errTerminateMainloop
|
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
func maxInt(a, b int) int {
|
|
|
|
if a > b {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
return b
|
2018-07-30 19:09:44 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 17:43:43 +03:00
|
|
|
func minInt(a, b int) int {
|
|
|
|
if a > b {
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
return a
|
2018-07-30 19:09:44 +03:00
|
|
|
}
|