Move exline handling up to aerc, add :term

This commit is contained in:
Drew DeVault 2019-03-17 16:19:15 -04:00
parent 9e28a02f6a
commit 589db742cb
5 changed files with 186 additions and 115 deletions

33
commands/term.go Normal file
View file

@ -0,0 +1,33 @@
package commands
import (
"errors"
"os/exec"
"git.sr.ht/~sircmpwn/aerc2/lib/ui"
"git.sr.ht/~sircmpwn/aerc2/widgets"
)
func init() {
Register("term", Term)
}
func Term(aerc *widgets.Aerc, args []string) error {
if len(args) > 2 {
return errors.New("Usage: term [<command>]")
}
term, err := widgets.NewTerminal(exec.Command(args[1], args[2:]...))
if err != nil {
return err
}
grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{
{ui.SIZE_EXACT, aerc.Config().Ui.SidebarWidth},
{ui.SIZE_WEIGHT, 1},
})
grid.AddChild(term).At(0, 1)
aerc.NewTab(grid, "Terminal")
// TODO: update tab name when child process changes it
return nil
}

View file

@ -30,13 +30,15 @@ func NewTabs() *Tabs {
return tabs return tabs
} }
func (tabs *Tabs) Add(content Drawable, name string) { func (tabs *Tabs) Add(content Drawable, name string) *Tab {
tabs.Tabs = append(tabs.Tabs, &Tab{ tab := &Tab{
Content: content, Content: content,
Name: name, Name: name,
}) }
tabs.Tabs = append(tabs.Tabs, tab)
tabs.TabStrip.Invalidate() tabs.TabStrip.Invalidate()
content.OnInvalidate(tabs.invalidateChild) content.OnInvalidate(tabs.invalidateChild)
return tab
} }
func (tabs *Tabs) invalidateChild(d Drawable) { func (tabs *Tabs) invalidateChild(d Drawable) {

View file

@ -3,7 +3,6 @@ package widgets
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -19,63 +18,51 @@ type AccountView struct {
conf *config.AercConfig conf *config.AercConfig
dirlist *DirectoryList dirlist *DirectoryList
grid *ui.Grid grid *ui.Grid
host TabHost
logger *log.Logger logger *log.Logger
interactive []ui.Interactive
onInvalidate func(d ui.Drawable) onInvalidate func(d ui.Drawable)
runCmd func(cmd string) error
msglist *MessageList msglist *MessageList
msgStores map[string]*lib.MessageStore msgStores map[string]*lib.MessageStore
pendingKeys []config.KeyStroke
statusline *StatusLine
statusbar *ui.Stack
worker *types.Worker worker *types.Worker
} }
func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig, func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
logger *log.Logger, runCmd func(cmd string) error) *AccountView { logger *log.Logger, host TabHost) *AccountView {
statusbar := ui.NewStack()
statusline := NewStatusLine()
statusbar.Push(statusline)
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
{ui.SIZE_EXACT, 1},
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_EXACT, conf.Ui.SidebarWidth}, {ui.SIZE_EXACT, conf.Ui.SidebarWidth},
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}) })
grid.AddChild(statusbar).At(1, 1)
worker, err := worker.NewWorker(acct.Source, logger) worker, err := worker.NewWorker(acct.Source, logger)
if err != nil { if err != nil {
statusline.Set(fmt.Sprintf("%s", err)) host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err))
return &AccountView{ return &AccountView{
acct: acct, acct: acct,
grid: grid, grid: grid,
logger: logger, host: host,
statusline: statusline, logger: logger,
} }
} }
dirlist := NewDirectoryList(acct, logger, worker) dirlist := NewDirectoryList(acct, logger, worker)
grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT)).Span(2, 1) grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT))
msglist := NewMessageList(logger) msglist := NewMessageList(logger)
grid.AddChild(msglist).At(0, 1) grid.AddChild(msglist).At(0, 1)
view := &AccountView{ view := &AccountView{
acct: acct, acct: acct,
conf: conf, conf: conf,
dirlist: dirlist, dirlist: dirlist,
grid: grid, grid: grid,
logger: logger, host: host,
msglist: msglist, logger: logger,
msgStores: make(map[string]*lib.MessageStore), msglist: msglist,
runCmd: runCmd, msgStores: make(map[string]*lib.MessageStore),
statusbar: statusbar, worker: worker,
statusline: statusline,
worker: worker,
} }
go worker.Backend.Run() go worker.Backend.Run()
@ -89,7 +76,7 @@ func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
worker.PostAction(&types.Configure{Config: acct}, nil) worker.PostAction(&types.Configure{Config: acct}, nil)
worker.PostAction(&types.Connect{}, view.connected) worker.PostAction(&types.Connect{}, view.connected)
statusline.Set("Connecting...") host.SetStatus("Connecting...")
return view return view
} }
@ -116,75 +103,14 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
acct.grid.Draw(ctx) acct.grid.Draw(ctx)
} }
func (acct *AccountView) popInteractive() { func (acct *AccountView) Focus(focus bool) {
acct.interactive = acct.interactive[:len(acct.interactive)-1] // TODO: Unfocus children I guess
if len(acct.interactive) != 0 {
acct.interactive[len(acct.interactive)-1].Focus(true)
}
}
func (acct *AccountView) pushInteractive(item ui.Interactive) {
if len(acct.interactive) != 0 {
acct.interactive[len(acct.interactive)-1].Focus(false)
}
acct.interactive = append(acct.interactive, item)
item.Focus(true)
}
func (acct *AccountView) beginExCommand() {
exline := NewExLine(func(command string) {
err := acct.runCmd(command)
if err != nil {
acct.statusline.Push(" "+err.Error(), 10*time.Second).
Color(tcell.ColorRed, tcell.ColorWhite)
}
acct.statusbar.Pop()
acct.popInteractive()
}, func() {
acct.statusbar.Pop()
acct.popInteractive()
})
acct.pushInteractive(exline)
acct.statusbar.Push(exline)
}
func (acct *AccountView) Event(event tcell.Event) bool {
if len(acct.interactive) != 0 {
return acct.interactive[len(acct.interactive)-1].Event(event)
}
switch event := event.(type) {
case *tcell.EventKey:
acct.pendingKeys = append(acct.pendingKeys, config.KeyStroke{
Key: event.Key(),
Rune: event.Rune(),
})
result, output := acct.conf.Lbinds.GetBinding(acct.pendingKeys)
switch result {
case config.BINDING_FOUND:
acct.pendingKeys = []config.KeyStroke{}
for _, stroke := range output {
simulated := tcell.NewEventKey(
stroke.Key, stroke.Rune, tcell.ModNone)
acct.Event(simulated)
}
case config.BINDING_INCOMPLETE:
return false
case config.BINDING_NOT_FOUND:
acct.pendingKeys = []config.KeyStroke{}
if event.Rune() == ':' {
acct.beginExCommand()
return true
}
}
}
return false
} }
func (acct *AccountView) connected(msg types.WorkerMessage) { func (acct *AccountView) connected(msg types.WorkerMessage) {
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.Done: case *types.Done:
acct.statusline.Set("Listing mailboxes...") acct.host.SetStatus("Listing mailboxes...")
acct.logger.Println("Listing mailboxes...") acct.logger.Println("Listing mailboxes...")
acct.dirlist.UpdateList(func(dirs []string) { acct.dirlist.UpdateList(func(dirs []string) {
var dir string var dir string
@ -199,7 +125,7 @@ func (acct *AccountView) connected(msg types.WorkerMessage) {
} }
acct.dirlist.Select(dir) acct.dirlist.Select(dir)
acct.logger.Println("Connected.") acct.logger.Println("Connected.")
acct.statusline.Set("Connected.") acct.host.SetStatus("Connected.")
}) })
case *types.CertificateApprovalRequest: case *types.CertificateApprovalRequest:
// TODO: Ask the user // TODO: Ask the user
@ -252,7 +178,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
store.Update(msg) store.Update(msg)
case *types.Error: case *types.Error:
acct.logger.Printf("%v", msg.Error) acct.logger.Printf("%v", msg.Error)
acct.statusline.Set(fmt.Sprintf("%v", msg.Error)). acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)).
Color(tcell.ColorRed, tcell.ColorDefault) Color(tcell.ColorRed, tcell.ColorDefault)
} }
} }

View file

@ -2,6 +2,7 @@ package widgets
import ( import (
"log" "log"
"time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -11,10 +12,16 @@ import (
) )
type Aerc struct { type Aerc struct {
accounts map[string]*AccountView accounts map[string]*AccountView
cmd func(cmd string) error cmd func(cmd string) error
grid *libui.Grid conf *config.AercConfig
tabs *libui.Tabs focused libui.Interactive
grid *libui.Grid
logger *log.Logger
statusbar *libui.Stack
statusline *StatusLine
pendingKeys []config.KeyStroke
tabs *libui.Tabs
} }
func NewAerc(conf *config.AercConfig, logger *log.Logger, func NewAerc(conf *config.AercConfig, logger *log.Logger,
@ -22,29 +29,42 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
tabs := libui.NewTabs() tabs := libui.NewTabs()
mainGrid := libui.NewGrid().Rows([]libui.GridSpec{ statusbar := ui.NewStack()
statusline := NewStatusLine()
statusbar.Push(statusline)
grid := libui.NewGrid().Rows([]libui.GridSpec{
{libui.SIZE_EXACT, 1}, {libui.SIZE_EXACT, 1},
{libui.SIZE_WEIGHT, 1}, {libui.SIZE_WEIGHT, 1},
{libui.SIZE_EXACT, 1},
}).Columns([]libui.GridSpec{ }).Columns([]libui.GridSpec{
{libui.SIZE_EXACT, conf.Ui.SidebarWidth}, {libui.SIZE_EXACT, conf.Ui.SidebarWidth},
{libui.SIZE_WEIGHT, 1}, {libui.SIZE_WEIGHT, 1},
}) })
grid.AddChild(statusbar).At(2, 1)
// Minor hack
grid.AddChild(libui.NewBordered(
libui.NewFill(' '), libui.BORDER_RIGHT)).At(2, 0)
mainGrid.AddChild(libui.NewText("aerc"). grid.AddChild(libui.NewText("aerc").
Strategy(libui.TEXT_CENTER). Strategy(libui.TEXT_CENTER).
Color(tcell.ColorBlack, tcell.ColorWhite)) Color(tcell.ColorBlack, tcell.ColorWhite))
mainGrid.AddChild(tabs.TabStrip).At(0, 1) grid.AddChild(tabs.TabStrip).At(0, 1)
mainGrid.AddChild(tabs.TabContent).At(1, 0).Span(1, 2) grid.AddChild(tabs.TabContent).At(1, 0).Span(1, 2)
aerc := &Aerc{ aerc := &Aerc{
accounts: make(map[string]*AccountView), accounts: make(map[string]*AccountView),
cmd: cmd, conf: conf,
grid: mainGrid, cmd: cmd,
tabs: tabs, grid: grid,
logger: logger,
statusbar: statusbar,
statusline: statusline,
tabs: tabs,
} }
for _, acct := range conf.Accounts { for _, acct := range conf.Accounts {
view := NewAccountView(conf, &acct, logger, cmd) view := NewAccountView(conf, &acct, logger, aerc)
aerc.accounts[acct.Name] = view aerc.accounts[acct.Name] = view
tabs.Add(view, acct.Name) tabs.Add(view, acct.Name)
} }
@ -75,8 +95,41 @@ func (aerc *Aerc) Draw(ctx *libui.Context) {
} }
func (aerc *Aerc) Event(event tcell.Event) bool { func (aerc *Aerc) Event(event tcell.Event) bool {
acct, _ := aerc.tabs.Tabs[aerc.tabs.Selected].Content.(*AccountView) if aerc.focused != nil {
return acct.Event(event) aerc.logger.Println("sending event to focused child")
return aerc.focused.Event(event)
}
switch event := event.(type) {
case *tcell.EventKey:
aerc.pendingKeys = append(aerc.pendingKeys, config.KeyStroke{
Key: event.Key(),
Rune: event.Rune(),
})
result, output := aerc.conf.Lbinds.GetBinding(aerc.pendingKeys)
switch result {
case config.BINDING_FOUND:
aerc.pendingKeys = []config.KeyStroke{}
for _, stroke := range output {
simulated := tcell.NewEventKey(
stroke.Key, stroke.Rune, tcell.ModNone)
aerc.Event(simulated)
}
case config.BINDING_INCOMPLETE:
return false
case config.BINDING_NOT_FOUND:
aerc.pendingKeys = []config.KeyStroke{}
if event.Rune() == ':' {
aerc.BeginExCommand()
return true
}
}
}
return false
}
func (aerc *Aerc) Config() *config.AercConfig {
return aerc.conf
} }
func (aerc *Aerc) SelectedAccount() *AccountView { func (aerc *Aerc) SelectedAccount() *AccountView {
@ -86,3 +139,49 @@ func (aerc *Aerc) SelectedAccount() *AccountView {
} }
return acct return acct
} }
func (aerc *Aerc) NewTab(drawable ui.Drawable, name string) *ui.Tab {
tab := aerc.tabs.Add(drawable, name)
aerc.tabs.Select(len(aerc.tabs.Tabs) - 1)
return tab
}
// TODO: Use per-account status lines, but a global ex line
func (aerc *Aerc) SetStatus(status string) *StatusMessage {
return aerc.statusline.Set(status)
}
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
return aerc.statusline.Push(text, expiry)
}
func (aerc *Aerc) focus(item libui.Interactive) {
if aerc.focused == item {
return
}
if aerc.focused != nil {
aerc.focused.Focus(false)
}
aerc.focused = item
if item != nil {
item.Focus(true)
}
}
func (aerc *Aerc) BeginExCommand() {
previous := aerc.focused
exline := NewExLine(func(cmd string) {
err := aerc.cmd(cmd)
if err != nil {
aerc.PushStatus(" "+err.Error(), 10*time.Second).
Color(tcell.ColorRed, tcell.ColorWhite)
}
aerc.statusbar.Pop()
aerc.focus(previous)
}, func() {
aerc.statusbar.Pop()
aerc.focus(previous)
})
aerc.statusbar.Push(exline)
aerc.focus(exline)
}

11
widgets/tabhost.go Normal file
View file

@ -0,0 +1,11 @@
package widgets
import (
"time"
)
type TabHost interface {
BeginExCommand()
SetStatus(status string) *StatusMessage
PushStatus(text string, expiry time.Duration) *StatusMessage
}