diff --git a/commands/term.go b/commands/term.go new file mode 100644 index 0000000..0a2aa3b --- /dev/null +++ b/commands/term.go @@ -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 []") + } + 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 +} diff --git a/lib/ui/tab.go b/lib/ui/tab.go index ecd48eb..e41e906 100644 --- a/lib/ui/tab.go +++ b/lib/ui/tab.go @@ -30,13 +30,15 @@ func NewTabs() *Tabs { return tabs } -func (tabs *Tabs) Add(content Drawable, name string) { - tabs.Tabs = append(tabs.Tabs, &Tab{ +func (tabs *Tabs) Add(content Drawable, name string) *Tab { + tab := &Tab{ Content: content, Name: name, - }) + } + tabs.Tabs = append(tabs.Tabs, tab) tabs.TabStrip.Invalidate() content.OnInvalidate(tabs.invalidateChild) + return tab } func (tabs *Tabs) invalidateChild(d Drawable) { diff --git a/widgets/account.go b/widgets/account.go index b6ba595..8a3b989 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -3,7 +3,6 @@ package widgets import ( "fmt" "log" - "time" "github.com/gdamore/tcell" @@ -19,63 +18,51 @@ type AccountView struct { conf *config.AercConfig dirlist *DirectoryList grid *ui.Grid + host TabHost logger *log.Logger - interactive []ui.Interactive onInvalidate func(d ui.Drawable) - runCmd func(cmd string) error msglist *MessageList msgStores map[string]*lib.MessageStore - pendingKeys []config.KeyStroke - statusline *StatusLine - statusbar *ui.Stack worker *types.Worker } func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig, - logger *log.Logger, runCmd func(cmd string) error) *AccountView { - - statusbar := ui.NewStack() - statusline := NewStatusLine() - statusbar.Push(statusline) + logger *log.Logger, host TabHost) *AccountView { grid := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_WEIGHT, 1}, - {ui.SIZE_EXACT, 1}, }).Columns([]ui.GridSpec{ {ui.SIZE_EXACT, conf.Ui.SidebarWidth}, {ui.SIZE_WEIGHT, 1}, }) - grid.AddChild(statusbar).At(1, 1) worker, err := worker.NewWorker(acct.Source, logger) if err != nil { - statusline.Set(fmt.Sprintf("%s", err)) + host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)) return &AccountView{ - acct: acct, - grid: grid, - logger: logger, - statusline: statusline, + acct: acct, + grid: grid, + host: host, + logger: logger, } } 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) grid.AddChild(msglist).At(0, 1) view := &AccountView{ - acct: acct, - conf: conf, - dirlist: dirlist, - grid: grid, - logger: logger, - msglist: msglist, - msgStores: make(map[string]*lib.MessageStore), - runCmd: runCmd, - statusbar: statusbar, - statusline: statusline, - worker: worker, + acct: acct, + conf: conf, + dirlist: dirlist, + grid: grid, + host: host, + logger: logger, + msglist: msglist, + msgStores: make(map[string]*lib.MessageStore), + worker: worker, } 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.Connect{}, view.connected) - statusline.Set("Connecting...") + host.SetStatus("Connecting...") return view } @@ -116,75 +103,14 @@ func (acct *AccountView) Draw(ctx *ui.Context) { acct.grid.Draw(ctx) } -func (acct *AccountView) popInteractive() { - acct.interactive = acct.interactive[:len(acct.interactive)-1] - 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) Focus(focus bool) { + // TODO: Unfocus children I guess } func (acct *AccountView) connected(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: - acct.statusline.Set("Listing mailboxes...") + acct.host.SetStatus("Listing mailboxes...") acct.logger.Println("Listing mailboxes...") acct.dirlist.UpdateList(func(dirs []string) { var dir string @@ -199,7 +125,7 @@ func (acct *AccountView) connected(msg types.WorkerMessage) { } acct.dirlist.Select(dir) acct.logger.Println("Connected.") - acct.statusline.Set("Connected.") + acct.host.SetStatus("Connected.") }) case *types.CertificateApprovalRequest: // TODO: Ask the user @@ -252,7 +178,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { store.Update(msg) case *types.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) } } diff --git a/widgets/aerc.go b/widgets/aerc.go index 3537897..5841876 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -2,6 +2,7 @@ package widgets import ( "log" + "time" "github.com/gdamore/tcell" @@ -11,10 +12,16 @@ import ( ) type Aerc struct { - accounts map[string]*AccountView - cmd func(cmd string) error - grid *libui.Grid - tabs *libui.Tabs + accounts map[string]*AccountView + cmd func(cmd string) error + conf *config.AercConfig + 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, @@ -22,29 +29,42 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, 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_WEIGHT, 1}, + {libui.SIZE_EXACT, 1}, }).Columns([]libui.GridSpec{ {libui.SIZE_EXACT, conf.Ui.SidebarWidth}, {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). Color(tcell.ColorBlack, tcell.ColorWhite)) - mainGrid.AddChild(tabs.TabStrip).At(0, 1) - mainGrid.AddChild(tabs.TabContent).At(1, 0).Span(1, 2) + grid.AddChild(tabs.TabStrip).At(0, 1) + grid.AddChild(tabs.TabContent).At(1, 0).Span(1, 2) aerc := &Aerc{ - accounts: make(map[string]*AccountView), - cmd: cmd, - grid: mainGrid, - tabs: tabs, + accounts: make(map[string]*AccountView), + conf: conf, + cmd: cmd, + grid: grid, + logger: logger, + statusbar: statusbar, + statusline: statusline, + tabs: tabs, } for _, acct := range conf.Accounts { - view := NewAccountView(conf, &acct, logger, cmd) + view := NewAccountView(conf, &acct, logger, aerc) aerc.accounts[acct.Name] = view tabs.Add(view, acct.Name) } @@ -75,8 +95,41 @@ func (aerc *Aerc) Draw(ctx *libui.Context) { } func (aerc *Aerc) Event(event tcell.Event) bool { - acct, _ := aerc.tabs.Tabs[aerc.tabs.Selected].Content.(*AccountView) - return acct.Event(event) + if aerc.focused != nil { + 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 { @@ -86,3 +139,49 @@ func (aerc *Aerc) SelectedAccount() *AccountView { } 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) +} diff --git a/widgets/tabhost.go b/widgets/tabhost.go new file mode 100644 index 0000000..7c502cb --- /dev/null +++ b/widgets/tabhost.go @@ -0,0 +1,11 @@ +package widgets + +import ( + "time" +) + +type TabHost interface { + BeginExCommand() + SetStatus(status string) *StatusMessage + PushStatus(text string, expiry time.Duration) *StatusMessage +}