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 (
"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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -42,6 +42,7 @@ func (NextPrevTab) Execute(aerc *widgets.Aerc, args []string) error {
aerc.NextTab()
}
}
aerc.UpdateStatus()
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"
"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

View file

@ -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 {

View file

@ -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
}