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/logging"
"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, deferLoop chan struct{},
) (*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 func() {
defer logging.PanicHandler()
if deferLoop != nil {
<-deferLoop
}
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, acct.SelectedDirectory())
}
acct.UpdateStatus()
}
func (acct *AccountView) UpdateStatus() {
acct.host.SetStatus(acct.state.StatusLine(acct.SelectedDirectory()))
}
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.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)
}
acct.UpdateStatus()
}
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
}