aerc/widgets/account.go
Koni Marti 2512c0403f 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>
2022-03-18 23:42:07 +01:00

332 lines
8.1 KiB
Go

package widgets
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"
"git.sr.ht/~rjarry/aerc/worker/types"
)
var _ ProvidesMessages = (*AccountView)(nil)
type AccountView struct {
acct *config.AccountConfig
aerc *Aerc
conf *config.AercConfig
dirlist DirectoryLister
labels []string
grid *ui.Grid
host TabHost
logger *log.Logger
msglist *MessageList
worker *types.Worker
state *statusline.State
}
func (acct *AccountView) UiConfig() config.UIConfig {
var folder string
if dirlist := acct.Directories(); dirlist != nil {
folder = dirlist.Selected()
}
return acct.conf.GetUiConfig(map[config.ContextType]string{
config.UI_CONTEXT_ACCOUNT: acct.AccountConfig().Name,
config.UI_CONTEXT_FOLDER: folder,
})
}
func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig,
logger *log.Logger, host TabHost) (*AccountView, error) {
acctUiConf := conf.GetUiConfig(map[config.ContextType]string{
config.UI_CONTEXT_ACCOUNT: acct.Name,
})
view := &AccountView{
acct: acct,
aerc: aerc,
conf: conf,
host: host,
logger: logger,
state: statusline.NewState(acct.Name, len(conf.Accounts) > 1, " | "),
}
view.grid = ui.NewGrid().Rows([]ui.GridSpec{
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}).Columns([]ui.GridSpec{
{Strategy: ui.SIZE_EXACT, Size: func() int {
return view.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
worker, err := worker.NewWorker(acct.Source, logger)
if err != nil {
host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
logger.Printf("%s: %s\n", acct.Name, err)
return view, err
}
view.worker = worker
view.dirlist = NewDirectoryList(conf, acct, logger, worker)
if acctUiConf.SidebarWidth > 0 {
view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT, acctUiConf))
}
view.msglist = NewMessageList(conf, logger, aerc)
view.grid.AddChild(view.msglist).At(0, 1)
go worker.Backend.Run()
worker.PostAction(&types.Configure{Config: acct}, nil)
worker.PostAction(&types.Connect{}, nil)
view.SetStatus(statusline.ConnectionActivity("Connecting..."))
return view, nil
}
func (acct *AccountView) Tick() bool {
if acct.worker == nil {
return false
}
select {
case msg := <-acct.worker.Messages:
msg = acct.worker.ProcessMessage(msg)
acct.onMessage(msg)
return true
default:
return false
}
}
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 {
return acct.acct
}
func (acct *AccountView) Worker() *types.Worker {
return acct.worker
}
func (acct *AccountView) Logger() *log.Logger {
return acct.logger
}
func (acct *AccountView) Name() string {
return acct.acct.Name
}
func (acct *AccountView) Children() []ui.Drawable {
return acct.grid.Children()
}
func (acct *AccountView) OnInvalidate(onInvalidate func(d ui.Drawable)) {
acct.grid.OnInvalidate(func(_ ui.Drawable) {
onInvalidate(acct)
})
}
func (acct *AccountView) Invalidate() {
acct.grid.Invalidate()
}
func (acct *AccountView) Draw(ctx *ui.Context) {
acct.UpdateStatus()
acct.grid.Draw(ctx)
}
func (acct *AccountView) MouseEvent(localX int, localY int, event tcell.Event) {
acct.grid.MouseEvent(localX, localY, event)
}
func (acct *AccountView) Focus(focus bool) {
// TODO: Unfocus children I guess
}
func (acct *AccountView) Directories() DirectoryLister {
return acct.dirlist
}
func (acct *AccountView) Labels() []string {
return acct.labels
}
func (acct *AccountView) Messages() *MessageList {
return acct.msglist
}
func (acct *AccountView) Store() *lib.MessageStore {
if acct.msglist == nil {
return nil
}
return acct.msglist.Store()
}
func (acct *AccountView) SelectedAccount() *AccountView {
return acct
}
func (acct *AccountView) SelectedDirectory() string {
return acct.dirlist.Selected()
}
func (acct *AccountView) SelectedMessage() (*models.MessageInfo, error) {
if len(acct.msglist.Store().Uids()) == 0 {
return nil, errors.New("no message selected")
}
msg := acct.msglist.Selected()
if msg == nil {
return nil, errors.New("message not loaded")
}
return msg, nil
}
func (acct *AccountView) MarkedMessages() ([]uint32, error) {
store := acct.Store()
return store.Marked(), nil
}
func (acct *AccountView) SelectedMessagePart() *PartInfo {
return nil
}
func (acct *AccountView) onMessage(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
switch msg.InResponseTo().(type) {
case *types.Connect, *types.Reconnect:
acct.SetStatus(statusline.ConnectionActivity("Listing mailboxes..."))
acct.logger.Println("Listing mailboxes...")
acct.dirlist.UpdateList(func(dirs []string) {
var dir string
for _, _dir := range dirs {
if _dir == acct.acct.Default {
dir = _dir
break
}
}
if dir == "" && len(dirs) > 0 {
dir = dirs[0]
}
if dir != "" {
acct.dirlist.Select(dir)
}
acct.msglist.SetInitDone()
acct.logger.Println("Connected.")
acct.SetStatus(statusline.Connected(true))
})
case *types.Disconnect:
acct.dirlist.UpdateList(nil)
acct.msglist.SetStore(nil)
acct.logger.Println("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
// memory while we wait for the update and the UI feels
// snappier. If not, we'll unset the store and show the spinner
// while we download the UID list.
acct.msglist.SetStore(store)
} else {
acct.msglist.SetStore(nil)
}
case *types.CreateDirectory:
acct.dirlist.UpdateList(nil)
case *types.RemoveDirectory:
acct.dirlist.UpdateList(nil)
}
case *types.DirectoryInfo:
if store, ok := acct.dirlist.MsgStore(msg.Info.Name); ok {
store.Update(msg)
} else {
store = lib.NewMessageStore(acct.worker, msg.Info,
acct.getSortCriteria(),
acct.UiConfig().ThreadingEnabled,
func(msg *models.MessageInfo) {
acct.conf.Triggers.ExecNewEmail(acct.acct,
acct.conf, msg)
}, func() {
if acct.UiConfig().NewMessageBell {
acct.host.Beep()
}
})
acct.dirlist.SetMsgStore(msg.Info.Name, store)
}
case *types.DirectoryContents:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
if acct.msglist.Store() == nil {
acct.msglist.SetStore(store)
}
store.Update(msg)
}
case *types.DirectoryThreaded:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
if acct.msglist.Store() == nil {
acct.msglist.SetStore(store)
}
store.Update(msg)
}
case *types.FullMessage:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
store.Update(msg)
}
case *types.MessageInfo:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
store.Update(msg)
}
case *types.MessagesDeleted:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
store.Update(msg)
}
case *types.LabelList:
acct.labels = msg.Labels
case *types.ConnError:
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.PushError(msg.Error)
}
}
func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
if len(acct.UiConfig().Sort) == 0 {
return nil
}
criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
if err != nil {
acct.PushError(fmt.Errorf("ui sort: %v", err))
return nil
}
return criteria
}