Move exline handling up to aerc, add :term
This commit is contained in:
parent
9e28a02f6a
commit
589db742cb
5 changed files with 186 additions and 115 deletions
33
commands/term.go
Normal file
33
commands/term.go
Normal 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
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
129
widgets/aerc.go
129
widgets/aerc.go
|
@ -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
11
widgets/tabhost.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TabHost interface {
|
||||||
|
BeginExCommand()
|
||||||
|
SetStatus(status string) *StatusMessage
|
||||||
|
PushStatus(text string, expiry time.Duration) *StatusMessage
|
||||||
|
}
|
Loading…
Reference in a new issue