git-bug/termui/termui.go

372 lines
7.8 KiB
Go
Raw Normal View History

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 (
"fmt"
"github.com/awesome-gocui/gocui"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/commands/input"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/query"
"github.com/MichaelMure/git-bug/util/text"
2018-07-30 18:00:10 +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
cache *cache.RepoCache
2018-08-01 03:15:40 +03:00
activeWindow window
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
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-08-13 16:28:16 +03:00
// Run will launch the termUI in the terminal
func Run(cache *cache.RepoCache) error {
2018-07-30 18:00:10 +03:00
ui = &termUI{
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
}
ui.activeWindow = ui.bugTable
2018-08-17 13:37:58 +03:00
initGui(nil)
err := <-ui.gError
type errorStack interface {
ErrorStack() string
}
if err != nil && err != gocui.ErrQuit {
if e, ok := err.(errorStack); ok {
fmt.Println(e.ErrorStack())
}
return err
}
return nil
}
2018-08-17 13:37:58 +03:00
func initGui(action func(ui *termUI) error) {
g, err := gocui.NewGui(gocui.Output256, false)
2018-07-30 18:00:10 +03:00
if err != nil {
ui.gError <- err
return
2018-07-30 18:00:10 +03:00
}
ui.g = g
2018-07-30 18:00:10 +03:00
ui.g.SetManagerFunc(layout)
2018-07-30 18:00:10 +03:00
ui.g.InputEsc = true
err = keybindings(ui.g)
2018-07-30 18:00:10 +03:00
if err != nil {
ui.g.Close()
ui.g = nil
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()
if err != nil && err != errTerminateMainloop {
if ui.g != nil {
ui.g.Close()
}
ui.gError <- err
2018-07-30 18:00:10 +03:00
}
}
func layout(g *gocui.Gui) error {
g.Cursor = false
2018-07-30 18:00:10 +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 {
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 {
// Quit
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
2018-07-30 18:00:10 +03:00
return err
}
if err := ui.bugTable.keybindings(g); err != nil {
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
}
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 {
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
}
func newBugWithEditor(repo *cache.RepoCache) 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.
2018-07-30 18:00:10 +03:00
ui.g.Close()
ui.g = nil
2018-07-30 18:00:10 +03:00
title, message, err := input.BugCreateEditorInput(ui.cache, "", "")
2018-07-30 18:00:10 +03:00
if err != nil && err != input.ErrEmptyTitle {
2018-07-30 18:00:10 +03:00
return err
}
var b *cache.BugCache
if err == input.ErrEmptyTitle {
2018-08-12 22:09:30 +03:00
ui.msgPopup.Activate(msgPopupErrorTitle, "Empty title, aborting.")
initGui(nil)
return errTerminateMainloop
} else {
b, _, err = repo.NewBug(
text.CleanupOneLine(title),
text.Cleanup(message),
)
if err != nil {
return err
}
initGui(func(ui *termUI) error {
ui.showBug.SetBug(b)
return ui.activateWindow(ui.showBug)
})
return errTerminateMainloop
}
}
func addCommentWithEditor(bug *cache.BugCache) 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, "")
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.")
} else {
_, err := bug.AddComment(text.Cleanup(message))
if err != nil {
return err
}
}
2018-08-17 13:37:58 +03:00
initGui(nil)
return errTerminateMainloop
}
func editCommentWithEditor(bug *cache.BugCache, target entity.Id, preMessage string) error {
2018-10-04 19:31:46 +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
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 {
_, err := bug.EditComment(target, text.Cleanup(message))
2018-10-04 19:31:46 +03:00
if err != nil {
return err
}
}
initGui(nil)
return errTerminateMainloop
}
func setTitleWithEditor(bug *cache.BugCache) 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
snap := bug.Snapshot()
title, err := input.BugTitleEditorInput(ui.cache, snap.Title)
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.")
} else if title == snap.Title {
ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
} else {
_, err := bug.SetTitle(text.CleanupOneLine(title))
if err != nil {
return err
}
2018-07-30 18:00:10 +03:00
}
2018-08-17 13:37:58 +03:00
initGui(nil)
return errTerminateMainloop
2018-07-30 18:00:10 +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
queryStr, err := input.QueryEditorInput(bt.repo, bt.queryStr)
2018-09-11 20:28:32 +03:00
if err != nil {
return err
}
bt.queryStr = queryStr
q, err := query.Parse(queryStr)
2018-09-11 20:28:32 +03:00
if err != nil {
ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
} else {
bt.query = q
2018-09-11 20:28:32 +03:00
}
initGui(nil)
return errTerminateMainloop
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func minInt(a, b int) int {
if a > b {
return b
}
return a
}