statusline: implement per-account status

Implement a statusline state for each account. Keep the ex line and the
push notifications global. Add account name prefix to push
notifications. Prefix status line with account name when multiple
accounts are available.

Use account-specific status line for each tab where an account is
defined.

Handle threading, filter/search, viewer passthrough and connection
status.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Koni Marti 2022-03-18 22:35:33 +01:00 committed by Robin Jarry
parent 807870ea35
commit 2512c0403f
10 changed files with 196 additions and 45 deletions

View File

@ -3,6 +3,7 @@ package account
import ( import (
"errors" "errors"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
) )
@ -30,6 +31,7 @@ func (Clear) Execute(aerc *widgets.Aerc, args []string) error {
return errors.New("Cannot perform action. Messages still loading") return errors.New("Cannot perform action. Messages still loading")
} }
store.ApplyClear() store.ApplyClear()
aerc.ClearExtraStatus() acct.SetStatus(statusline.SearchFilterClear())
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package account
import ( import (
"errors" "errors"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~rjarry/aerc/worker/types" "git.sr.ht/~rjarry/aerc/worker/types"
) )
@ -26,12 +27,15 @@ func (Connection) Execute(aerc *widgets.Aerc, args []string) error {
if acct == nil { if acct == nil {
return errors.New("No account selected") return errors.New("No account selected")
} }
cb := func(msg types.WorkerMessage) {
acct.SetStatus(statusline.ConnectionActivity(""))
}
if args[0] == "connect" { if args[0] == "connect" {
acct.Worker().PostAction(&types.Connect{}, nil) acct.Worker().PostAction(&types.Connect{}, cb)
acct.SetStatus("Connecting...") acct.SetStatus(statusline.ConnectionActivity("Connecting..."))
} else { } else {
acct.Worker().PostAction(&types.Disconnect{}, nil) acct.Worker().PostAction(&types.Disconnect{}, cb)
acct.SetStatus("Disconnecting...") acct.SetStatus(statusline.ConnectionActivity("Disconnecting..."))
} }
return nil return nil
} }

View File

@ -2,8 +2,9 @@ package account
import ( import (
"errors" "errors"
"fmt" "strings"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
) )
@ -33,16 +34,16 @@ func (SearchFilter) Execute(aerc *widgets.Aerc, args []string) error {
var cb func([]uint32) var cb func([]uint32)
if args[0] == "filter" { if args[0] == "filter" {
aerc.SetExtraStatus("Filtering...") acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search(""))
cb = func(uids []uint32) { cb = func(uids []uint32) {
aerc.SetExtraStatus(fmt.Sprintf("%s", args)) acct.SetStatus(statusline.FilterResult(strings.Join(args, " ")))
acct.Logger().Printf("Filter results: %v", uids) acct.Logger().Printf("Filter results: %v", uids)
store.ApplyFilter(uids) store.ApplyFilter(uids)
} }
} else { } else {
aerc.SetExtraStatus("Searching...") acct.SetStatus(statusline.Search("Searching..."))
cb = func(uids []uint32) { cb = func(uids []uint32) {
aerc.SetExtraStatus(fmt.Sprintf("%s", args)) acct.SetStatus(statusline.Search(strings.Join(args, " ")))
acct.Logger().Printf("Search results: %v", uids) acct.Logger().Printf("Search results: %v", uids)
store.ApplySearch(uids) store.ApplySearch(uids)
// TODO: Remove when stores have multiple OnUpdate handlers // TODO: Remove when stores have multiple OnUpdate handlers

View File

@ -3,6 +3,7 @@ package msg
import ( import (
"errors" "errors"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
) )
@ -34,6 +35,7 @@ func (ToggleThreads) Execute(aerc *widgets.Aerc, args []string) error {
return err return err
} }
store.SetBuildThreads(!store.BuildThreads()) store.SetBuildThreads(!store.BuildThreads())
acct.SetStatus(statusline.Threading(store.BuildThreads()))
acct.Messages().Invalidate() acct.Messages().Invalidate()
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package msgview
import ( import (
"errors" "errors"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
) )
@ -26,10 +27,8 @@ func (ToggleKeyPassthrough) Execute(aerc *widgets.Aerc, args []string) error {
} }
mv, _ := aerc.SelectedTab().(*widgets.MessageViewer) mv, _ := aerc.SelectedTab().(*widgets.MessageViewer)
keyPassthroughEnabled := mv.ToggleKeyPassthrough() keyPassthroughEnabled := mv.ToggleKeyPassthrough()
if keyPassthroughEnabled { if acct := mv.SelectedAccount(); acct != nil {
aerc.SetExtraStatus("[passthrough]") acct.SetStatus(statusline.Passthrough(keyPassthroughEnabled))
} else {
aerc.ClearExtraStatus()
} }
return nil return nil
} }

View File

@ -42,6 +42,7 @@ func (NextPrevTab) Execute(aerc *widgets.Aerc, args []string) error {
aerc.NextTab() aerc.NextTab()
} }
} }
aerc.UpdateStatus()
return nil return nil
} }

133
lib/statusline/state.go Normal file
View File

@ -0,0 +1,133 @@
package statusline
import (
"fmt"
"strings"
)
type State struct {
Name string
Multiple bool
Separator string
Connection string
ConnActivity string
Connected bool
Search string
Filter string
FilterActivity string
Threading string
Passthrough string
}
func NewState(name string, multipleAccts bool, sep string) *State {
return &State{Name: name, Multiple: multipleAccts, Separator: sep}
}
func (s *State) String() string {
var line []string
if s.Connection != "" || s.ConnActivity != "" {
conn := s.Connection
if s.ConnActivity != "" {
conn = s.ConnActivity
}
if s.Multiple {
line = append(line, fmt.Sprintf("[%s] %s", s.Name, conn))
} else {
line = append(line, conn)
}
}
if s.Connected {
if s.FilterActivity != "" {
line = append(line, s.FilterActivity)
} else {
if s.Filter != "" {
line = append(line, s.Filter)
}
}
if s.Search != "" {
line = append(line, s.Search)
}
if s.Threading != "" {
line = append(line, s.Threading)
}
if s.Passthrough != "" {
line = append(line, s.Passthrough)
}
}
return strings.Join(line, s.Separator)
}
type SetStateFunc func(s *State)
func Connected(state bool) SetStateFunc {
return func(s *State) {
s.ConnActivity = ""
s.Connected = state
if state {
s.Connection = "Connected"
} else {
s.Connection = "Disconnected"
}
}
}
func ConnectionActivity(desc string) SetStateFunc {
return func(s *State) {
s.ConnActivity = desc
}
}
func SearchFilterClear() SetStateFunc {
return func(s *State) {
s.Search = ""
s.FilterActivity = ""
s.Filter = ""
}
}
func FilterActivity(str string) SetStateFunc {
return func(s *State) {
s.FilterActivity = str
}
}
func FilterResult(str string) SetStateFunc {
return func(s *State) {
s.FilterActivity = ""
s.Filter = concatFilters(s.Filter, str)
}
}
func concatFilters(existing, next string) string {
if existing == "" {
return next
}
return fmt.Sprintf("%s && %s", existing, next)
}
func Search(desc string) SetStateFunc {
return func(s *State) {
s.Search = desc
}
}
func Threading(on bool) SetStateFunc {
return func(s *State) {
s.Threading = ""
if on {
s.Threading = "threading"
}
}
}
func Passthrough(on bool) SetStateFunc {
return func(s *State) {
s.Passthrough = ""
if on {
s.Passthrough = "passthrough"
}
}
}

View File

@ -4,12 +4,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/lib/statusline"
"git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker" "git.sr.ht/~rjarry/aerc/worker"
@ -29,6 +31,7 @@ type AccountView struct {
logger *log.Logger logger *log.Logger
msglist *MessageList msglist *MessageList
worker *types.Worker worker *types.Worker
state *statusline.State
} }
func (acct *AccountView) UiConfig() config.UIConfig { func (acct *AccountView) UiConfig() config.UIConfig {
@ -55,6 +58,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
conf: conf, conf: conf,
host: host, host: host,
logger: logger, logger: logger,
state: statusline.NewState(acct.Name, len(conf.Accounts) > 1, " | "),
} }
view.grid = ui.NewGrid().Rows([]ui.GridSpec{ view.grid = ui.NewGrid().Rows([]ui.GridSpec{
@ -86,7 +90,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
worker.PostAction(&types.Configure{Config: acct}, nil) worker.PostAction(&types.Configure{Config: acct}, nil)
worker.PostAction(&types.Connect{}, nil) worker.PostAction(&types.Connect{}, nil)
host.SetStatus("Connecting...") view.SetStatus(statusline.ConnectionActivity("Connecting..."))
return view, nil return view, nil
} }
@ -105,8 +109,22 @@ func (acct *AccountView) Tick() bool {
} }
} }
func (acct *AccountView) SetStatus(msg string) { func (acct *AccountView) SetStatus(setters ...statusline.SetStateFunc) {
acct.host.SetStatus(msg) for _, fn := range setters {
fn(acct.state)
}
}
func (acct *AccountView) UpdateStatus() {
acct.host.SetStatus(acct.state.String())
}
func (acct *AccountView) PushStatus(status string, expiry time.Duration) {
acct.aerc.PushStatus(fmt.Sprintf("%s: %v", acct.acct.Name, status), expiry)
}
func (acct *AccountView) PushError(err error) {
acct.aerc.PushError(fmt.Sprintf("%s: %v", acct.acct.Name, err))
} }
func (acct *AccountView) AccountConfig() *config.AccountConfig { func (acct *AccountView) AccountConfig() *config.AccountConfig {
@ -140,6 +158,7 @@ func (acct *AccountView) Invalidate() {
} }
func (acct *AccountView) Draw(ctx *ui.Context) { func (acct *AccountView) Draw(ctx *ui.Context) {
acct.UpdateStatus()
acct.grid.Draw(ctx) acct.grid.Draw(ctx)
} }
@ -203,7 +222,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
case *types.Done: case *types.Done:
switch msg.InResponseTo().(type) { switch msg.InResponseTo().(type) {
case *types.Connect, *types.Reconnect: case *types.Connect, *types.Reconnect:
acct.host.SetStatus("Listing mailboxes...") acct.SetStatus(statusline.ConnectionActivity("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
@ -221,13 +240,13 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
} }
acct.msglist.SetInitDone() acct.msglist.SetInitDone()
acct.logger.Println("Connected.") acct.logger.Println("Connected.")
acct.host.SetStatus("Connected.") acct.SetStatus(statusline.Connected(true))
}) })
case *types.Disconnect: case *types.Disconnect:
acct.dirlist.UpdateList(nil) acct.dirlist.UpdateList(nil)
acct.msglist.SetStore(nil) acct.msglist.SetStore(nil)
acct.logger.Println("Disconnected.") acct.logger.Println("Disconnected.")
acct.host.SetStatus("Disconnected.") acct.SetStatus(statusline.Connected(false))
case *types.OpenDirectory: case *types.OpenDirectory:
if store, ok := acct.dirlist.SelectedMsgStore(); ok { if store, ok := acct.dirlist.SelectedMsgStore(); ok {
// If we've opened this dir before, we can re-render it from // If we've opened this dir before, we can re-render it from
@ -289,14 +308,14 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
case *types.LabelList: case *types.LabelList:
acct.labels = msg.Labels acct.labels = msg.Labels
case *types.ConnError: case *types.ConnError:
acct.logger.Printf("connection error: %v", msg.Error) acct.logger.Printf("connection error: [%s] %v", acct.acct.Name, msg.Error)
acct.host.SetStatus("Disconnected.") acct.SetStatus(statusline.Connected(false))
acct.aerc.PushError(fmt.Sprintf("%v", msg.Error)) acct.PushError(msg.Error)
acct.msglist.SetStore(nil) acct.msglist.SetStore(nil)
acct.worker.PostAction(&types.Reconnect{}, nil) acct.worker.PostAction(&types.Reconnect{}, nil)
case *types.Error: case *types.Error:
acct.logger.Printf("%v", msg.Error) acct.logger.Printf("%v", msg.Error)
acct.aerc.PushError(fmt.Sprintf("%v", msg.Error)) acct.PushError(msg.Error)
} }
} }
@ -306,7 +325,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
} }
criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort) criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
if err != nil { if err != nil {
acct.aerc.PushError(" ui.sort: " + err.Error()) acct.PushError(fmt.Errorf("ui sort: %v", err))
return nil return nil
} }
return criteria return criteria

View File

@ -337,6 +337,7 @@ func (aerc *Aerc) NumTabs() int {
func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab { func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab {
tab := aerc.tabs.Add(clickable, name) tab := aerc.tabs.Add(clickable, name)
aerc.tabs.Select(len(aerc.tabs.Tabs) - 1) aerc.tabs.Select(len(aerc.tabs.Tabs) - 1)
aerc.UpdateStatus()
return tab return tab
} }
@ -400,17 +401,20 @@ func (aerc *Aerc) SelectPreviousTab() bool {
return aerc.tabs.SelectPrevious() return aerc.tabs.SelectPrevious()
} }
// TODO: Use per-account status lines, but a global ex line
func (aerc *Aerc) SetStatus(status string) *StatusMessage { func (aerc *Aerc) SetStatus(status string) *StatusMessage {
return aerc.statusline.Set(status) return aerc.statusline.Set(status)
} }
func (aerc *Aerc) SetExtraStatus(status string) { func (aerc *Aerc) UpdateStatus() {
aerc.statusline.SetExtra(status) if acct := aerc.SelectedAccount(); acct != nil {
acct.UpdateStatus()
} else {
aerc.ClearStatus()
}
} }
func (aerc *Aerc) ClearExtraStatus() { func (aerc *Aerc) ClearStatus() {
aerc.statusline.ClearExtra() aerc.statusline.Set("")
} }
func (aerc *Aerc) SetError(status string) *StatusMessage { func (aerc *Aerc) SetError(status string) *StatusMessage {

View File

@ -14,7 +14,6 @@ type StatusLine struct {
ui.Invalidatable ui.Invalidatable
stack []*StatusMessage stack []*StatusMessage
fallback StatusMessage fallback StatusMessage
extra string
aerc *Aerc aerc *Aerc
uiConfig config.UIConfig uiConfig config.UIConfig
} }
@ -30,7 +29,6 @@ func NewStatusLine(uiConfig config.UIConfig) *StatusLine {
style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT), style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
message: "Idle", message: "Idle",
}, },
extra: "",
uiConfig: uiConfig, uiConfig: uiConfig,
} }
} }
@ -51,11 +49,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
pendingKeys += string(pendingKey.Rune) pendingKeys += string(pendingKey.Rune)
} }
} }
text := line.message message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
if status.extra != "" {
text += " " + status.extra
}
message := runewidth.FillRight(text, ctx.Width()-len(pendingKeys)-5)
ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys) ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys)
} }
@ -109,14 +103,6 @@ func (status *StatusLine) PushSuccess(text string) *StatusMessage {
return msg return msg
} }
func (status *StatusLine) SetExtra(text string) {
status.extra = text
}
func (status *StatusLine) ClearExtra() {
status.extra = ""
}
func (status *StatusLine) Expire() { func (status *StatusLine) Expire() {
status.stack = nil status.stack = nil
} }