diff --git a/commands/account/clear.go b/commands/account/clear.go index 259a9de..64e7012 100644 --- a/commands/account/clear.go +++ b/commands/account/clear.go @@ -3,6 +3,7 @@ package account import ( "errors" + "git.sr.ht/~rjarry/aerc/lib/statusline" "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") } store.ApplyClear() - aerc.ClearExtraStatus() + acct.SetStatus(statusline.SearchFilterClear()) + return nil } diff --git a/commands/account/connection.go b/commands/account/connection.go index a87993b..52b569c 100644 --- a/commands/account/connection.go +++ b/commands/account/connection.go @@ -3,6 +3,7 @@ package account import ( "errors" + "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" ) @@ -26,12 +27,15 @@ func (Connection) Execute(aerc *widgets.Aerc, args []string) error { if acct == nil { return errors.New("No account selected") } + cb := func(msg types.WorkerMessage) { + acct.SetStatus(statusline.ConnectionActivity("")) + } if args[0] == "connect" { - acct.Worker().PostAction(&types.Connect{}, nil) - acct.SetStatus("Connecting...") + acct.Worker().PostAction(&types.Connect{}, cb) + acct.SetStatus(statusline.ConnectionActivity("Connecting...")) } else { - acct.Worker().PostAction(&types.Disconnect{}, nil) - acct.SetStatus("Disconnecting...") + acct.Worker().PostAction(&types.Disconnect{}, cb) + acct.SetStatus(statusline.ConnectionActivity("Disconnecting...")) } return nil } diff --git a/commands/account/search.go b/commands/account/search.go index 86d9dea..eeee7bd 100644 --- a/commands/account/search.go +++ b/commands/account/search.go @@ -2,8 +2,9 @@ package account import ( "errors" - "fmt" + "strings" + "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -33,16 +34,16 @@ func (SearchFilter) Execute(aerc *widgets.Aerc, args []string) error { var cb func([]uint32) if args[0] == "filter" { - aerc.SetExtraStatus("Filtering...") + acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search("")) cb = func(uids []uint32) { - aerc.SetExtraStatus(fmt.Sprintf("%s", args)) + acct.SetStatus(statusline.FilterResult(strings.Join(args, " "))) acct.Logger().Printf("Filter results: %v", uids) store.ApplyFilter(uids) } } else { - aerc.SetExtraStatus("Searching...") + acct.SetStatus(statusline.Search("Searching...")) cb = func(uids []uint32) { - aerc.SetExtraStatus(fmt.Sprintf("%s", args)) + acct.SetStatus(statusline.Search(strings.Join(args, " "))) acct.Logger().Printf("Search results: %v", uids) store.ApplySearch(uids) // TODO: Remove when stores have multiple OnUpdate handlers diff --git a/commands/msg/toggle-threads.go b/commands/msg/toggle-threads.go index e93cb42..79d515c 100644 --- a/commands/msg/toggle-threads.go +++ b/commands/msg/toggle-threads.go @@ -3,6 +3,7 @@ package msg import ( "errors" + "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -34,6 +35,7 @@ func (ToggleThreads) Execute(aerc *widgets.Aerc, args []string) error { return err } store.SetBuildThreads(!store.BuildThreads()) + acct.SetStatus(statusline.Threading(store.BuildThreads())) acct.Messages().Invalidate() return nil } diff --git a/commands/msgview/toggle-key-passthrough.go b/commands/msgview/toggle-key-passthrough.go index 6cd575b..1ac370e 100644 --- a/commands/msgview/toggle-key-passthrough.go +++ b/commands/msgview/toggle-key-passthrough.go @@ -3,6 +3,7 @@ package msgview import ( "errors" + "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -26,10 +27,8 @@ func (ToggleKeyPassthrough) Execute(aerc *widgets.Aerc, args []string) error { } mv, _ := aerc.SelectedTab().(*widgets.MessageViewer) keyPassthroughEnabled := mv.ToggleKeyPassthrough() - if keyPassthroughEnabled { - aerc.SetExtraStatus("[passthrough]") - } else { - aerc.ClearExtraStatus() + if acct := mv.SelectedAccount(); acct != nil { + acct.SetStatus(statusline.Passthrough(keyPassthroughEnabled)) } return nil } diff --git a/commands/next-tab.go b/commands/next-tab.go index 9d6a09b..854353f 100644 --- a/commands/next-tab.go +++ b/commands/next-tab.go @@ -42,6 +42,7 @@ func (NextPrevTab) Execute(aerc *widgets.Aerc, args []string) error { aerc.NextTab() } } + aerc.UpdateStatus() return nil } diff --git a/lib/statusline/state.go b/lib/statusline/state.go new file mode 100644 index 0000000..30029c1 --- /dev/null +++ b/lib/statusline/state.go @@ -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" + } + } +} diff --git a/widgets/account.go b/widgets/account.go index 87a8cef..647a3ae 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -4,12 +4,14 @@ import ( "errors" "fmt" "log" + "time" "github.com/gdamore/tcell/v2" "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "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/models" "git.sr.ht/~rjarry/aerc/worker" @@ -29,6 +31,7 @@ type AccountView struct { logger *log.Logger msglist *MessageList worker *types.Worker + state *statusline.State } func (acct *AccountView) UiConfig() config.UIConfig { @@ -55,6 +58,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon conf: conf, host: host, logger: logger, + state: statusline.NewState(acct.Name, len(conf.Accounts) > 1, " | "), } 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.Connect{}, nil) - host.SetStatus("Connecting...") + view.SetStatus(statusline.ConnectionActivity("Connecting...")) return view, nil } @@ -105,8 +109,22 @@ func (acct *AccountView) Tick() bool { } } -func (acct *AccountView) SetStatus(msg string) { - acct.host.SetStatus(msg) +func (acct *AccountView) SetStatus(setters ...statusline.SetStateFunc) { + 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 { @@ -140,6 +158,7 @@ func (acct *AccountView) Invalidate() { } func (acct *AccountView) Draw(ctx *ui.Context) { + acct.UpdateStatus() acct.grid.Draw(ctx) } @@ -203,7 +222,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { case *types.Done: switch msg.InResponseTo().(type) { case *types.Connect, *types.Reconnect: - acct.host.SetStatus("Listing mailboxes...") + acct.SetStatus(statusline.ConnectionActivity("Listing mailboxes...")) acct.logger.Println("Listing mailboxes...") acct.dirlist.UpdateList(func(dirs []string) { var dir string @@ -221,13 +240,13 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { } acct.msglist.SetInitDone() acct.logger.Println("Connected.") - acct.host.SetStatus("Connected.") + acct.SetStatus(statusline.Connected(true)) }) case *types.Disconnect: acct.dirlist.UpdateList(nil) acct.msglist.SetStore(nil) acct.logger.Println("Disconnected.") - acct.host.SetStatus("Disconnected.") + acct.SetStatus(statusline.Connected(false)) case *types.OpenDirectory: if store, ok := acct.dirlist.SelectedMsgStore(); ok { // 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: acct.labels = msg.Labels case *types.ConnError: - acct.logger.Printf("connection error: %v", msg.Error) - acct.host.SetStatus("Disconnected.") - acct.aerc.PushError(fmt.Sprintf("%v", msg.Error)) + acct.logger.Printf("connection error: [%s] %v", acct.acct.Name, msg.Error) + acct.SetStatus(statusline.Connected(false)) + acct.PushError(msg.Error) acct.msglist.SetStore(nil) acct.worker.PostAction(&types.Reconnect{}, nil) case *types.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) if err != nil { - acct.aerc.PushError(" ui.sort: " + err.Error()) + acct.PushError(fmt.Errorf("ui sort: %v", err)) return nil } return criteria diff --git a/widgets/aerc.go b/widgets/aerc.go index 3a8f47f..a8b23fe 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -337,6 +337,7 @@ func (aerc *Aerc) NumTabs() int { func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab { tab := aerc.tabs.Add(clickable, name) aerc.tabs.Select(len(aerc.tabs.Tabs) - 1) + aerc.UpdateStatus() return tab } @@ -400,17 +401,20 @@ func (aerc *Aerc) SelectPreviousTab() bool { return aerc.tabs.SelectPrevious() } -// 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) SetExtraStatus(status string) { - aerc.statusline.SetExtra(status) +func (aerc *Aerc) UpdateStatus() { + if acct := aerc.SelectedAccount(); acct != nil { + acct.UpdateStatus() + } else { + aerc.ClearStatus() + } } -func (aerc *Aerc) ClearExtraStatus() { - aerc.statusline.ClearExtra() +func (aerc *Aerc) ClearStatus() { + aerc.statusline.Set("") } func (aerc *Aerc) SetError(status string) *StatusMessage { diff --git a/widgets/status.go b/widgets/status.go index 960f244..c70d215 100644 --- a/widgets/status.go +++ b/widgets/status.go @@ -14,7 +14,6 @@ type StatusLine struct { ui.Invalidatable stack []*StatusMessage fallback StatusMessage - extra string aerc *Aerc uiConfig config.UIConfig } @@ -30,7 +29,6 @@ func NewStatusLine(uiConfig config.UIConfig) *StatusLine { style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT), message: "Idle", }, - extra: "", uiConfig: uiConfig, } } @@ -51,11 +49,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) { pendingKeys += string(pendingKey.Rune) } } - text := line.message - if status.extra != "" { - text += " " + status.extra - } - message := runewidth.FillRight(text, ctx.Width()-len(pendingKeys)-5) + message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5) ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys) } @@ -109,14 +103,6 @@ func (status *StatusLine) PushSuccess(text string) *StatusMessage { return msg } -func (status *StatusLine) SetExtra(text string) { - status.extra = text -} - -func (status *StatusLine) ClearExtra() { - status.extra = "" -} - func (status *StatusLine) Expire() { status.stack = nil }