1
0
Fork 0
mirror of https://git.sr.ht/~rjarry/aerc synced 2025-02-22 14:53:57 +01:00
aerc/app/account.go
Robin Jarry 7069217834 viewer: avoid crashes on opening invalid messages
When an error occurs during the opening of a message because its
contents cannot be parsed, the PartSwitcher object is left to nil and
the err field is set to the reported error.

This defers the error reporting after the viewer tab is displayed but it
is not handled in all sub functions which assume that switcher cannot be
nil.

 Error: runtime error: invalid memory address or nil pointer dereference

 git.sr.ht/~rjarry/aerc/app.(*PartSwitcher).Show(...)
     /build/aerc/src/aerc/app/partswitcher.go:77
 git.sr.ht/~rjarry/aerc/app.(*MessageViewer).Show(...)
     /build/aerc/src/aerc/app/msgviewer.go:409
 git.sr.ht/~rjarry/aerc/lib/ui.(*Tabs).selectPriv(...)
     /build/aerc/src/aerc/lib/ui/tab.go:181
 git.sr.ht/~rjarry/aerc/lib/ui.(*Tabs).Add(...)
     /build/aerc/src/aerc/lib/ui/tab.go:75
 git.sr.ht/~rjarry/aerc/app.(*Aerc).NewTab(...)
     /build/aerc/src/aerc/app/aerc.go:511
 git.sr.ht/~rjarry/aerc/app.NewTab(...)
     /build/aerc/src/aerc/app/app.go:61
 git.sr.ht/~rjarry/aerc/commands/account.ViewMessage.Execute.func1(...)
     /build/aerc/src/aerc/commands/account/view.go:71
 git.sr.ht/~rjarry/aerc/lib.NewMessageStoreView.func1(...)
     /build/aerc/src/aerc/lib/messageview.go:80
 git.sr.ht/~rjarry/aerc/lib.NewMessageStoreView(...)
     /build/aerc/src/aerc/lib/messageview.go:124
 git.sr.ht/~rjarry/aerc/commands/account.ViewMessage.Execute(...)
     /build/aerc/src/aerc/commands/account/view.go:52
 git.sr.ht/~rjarry/aerc/commands.ExecuteCommand(...)
     /build/aerc/src/aerc/commands/commands.go:205
 main.execCommand(...)

Remove that private err field and return an explicit error when the
message cannot be opened to enforce handling of the error by the caller.

When the msg argument is nil (only used in split viewer), return an
empty message viewer object and ensure that all code paths that read the
switcher or msg fields perform a nil check before accessing it.

Link: https://lists.sr.ht/~rjarry/aerc-devel/%3C12c465e4-b733-4b15-b4b0-62f87429fdf7@gmail.com%3E
Link: https://lists.sr.ht/~rjarry/aerc-devel/%3C2C55CF50-A636-46E5-9BA8-FE60A2303ECA@proton.me%3E
Link: https://lists.sr.ht/~rjarry/aerc-devel/%3CD51PEB6OMNDT.1KVSX0UCNL2MB@posteo.de%3E
Reported-by: Benjamin Braun <ben.braun@posteo.de>
Reported-by: Filip <filip.sh@proton.me>
Reported-by: Sarthak Bhan <sbstratos79@gmail.com>
Signed-off-by: Robin Jarry <robin@jarry.cc>
2024-10-22 19:44:36 +02:00

776 lines
19 KiB
Go

package app
import (
"bytes"
"errors"
"fmt"
"strings"
"sync"
"time"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/hooks"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/marker"
"git.sr.ht/~rjarry/aerc/lib/pama"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/templates"
"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"
"git.sr.ht/~rockorager/vaxis"
)
var _ ProvidesMessages = (*AccountView)(nil)
type AccountView struct {
sync.Mutex
acct *config.AccountConfig
dirlist DirectoryLister
labels []string
grid *ui.Grid
tab *ui.Tab
msglist *MessageList
worker *types.Worker
state state.AccountState
newConn bool // True if this is a first run after a new connection/reconnection
split *MessageViewer
splitSize int
splitDebounce *time.Timer
splitDir config.SplitDirection
splitLoaded bool
// Check-mail ticker
ticker *time.Ticker
checkingMail bool
}
func (acct *AccountView) UiConfig() *config.UIConfig {
if dirlist := acct.Directories(); dirlist != nil {
return dirlist.UiConfig("")
}
return config.Ui.ForAccount(acct.acct.Name)
}
func NewAccountView(
acct *config.AccountConfig, deferLoop chan struct{},
) (*AccountView, error) {
view := &AccountView{
acct: acct,
}
worker, err := worker.NewWorker(acct.Source, acct.Name)
if err != nil {
SetError(fmt.Sprintf("%s: %s", acct.Name, err))
log.Errorf("%s: %v", acct.Name, err)
return view, err
}
view.worker = worker
view.dirlist = NewDirectoryList(acct, worker)
view.msglist = NewMessageList(view)
view.Configure()
go func() {
defer log.PanicHandler()
if deferLoop != nil {
<-deferLoop
}
worker.Backend.Run()
}()
worker.PostAction(&types.Configure{Config: acct}, nil)
worker.PostAction(&types.Connect{}, nil)
view.SetStatus(state.ConnectionActivity("Connecting..."))
if acct.CheckMail.Minutes() > 0 {
view.CheckMailTimer(acct.CheckMail)
}
return view, nil
}
func (acct *AccountView) Configure() {
acct.dirlist.OnVirtualNode(func() {
acct.msglist.SetStore(nil)
acct.Invalidate()
})
sidebar := acct.UiConfig().SidebarWidth
acct.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 sidebar
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
if sidebar > 0 {
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig()))
}
acct.grid.AddChild(acct.msglist).At(0, 1)
acct.setTitle()
// handle splits
if acct.split != nil {
acct.split.Close()
}
splitDirection := acct.splitDir
acct.splitDir = config.SPLIT_NONE
switch splitDirection {
case config.SPLIT_HORIZONTAL:
acct.Split(acct.SplitSize())
case config.SPLIT_VERTICAL:
acct.Vsplit(acct.SplitSize())
}
}
func (acct *AccountView) SetStatus(setters ...state.SetStateFunc) {
for _, fn := range setters {
fn(&acct.state, acct.SelectedDirectory())
}
acct.UpdateStatus()
}
func (acct *AccountView) UpdateStatus() {
if acct.isSelected() {
UpdateStatus()
}
}
func (acct *AccountView) Select() {
for i, widget := range aerc.tabs.TabContent.Children() {
if widget == acct {
aerc.SelectTabIndex(i)
}
}
}
func (acct *AccountView) PushStatus(status string, expiry time.Duration) {
PushStatus(fmt.Sprintf("%s: %s", acct.acct.Name, status), expiry)
}
func (acct *AccountView) PushError(err error) {
PushError(fmt.Sprintf("%s: %v", acct.acct.Name, err))
}
func (acct *AccountView) PushWarning(warning string) {
PushWarning(fmt.Sprintf("%s: %s", acct.acct.Name, warning))
}
func (acct *AccountView) AccountConfig() *config.AccountConfig {
return acct.acct
}
func (acct *AccountView) Worker() *types.Worker {
return acct.worker
}
func (acct *AccountView) Name() string {
return acct.acct.Name
}
func (acct *AccountView) Invalidate() {
ui.Invalidate()
}
func (acct *AccountView) Draw(ctx *ui.Context) {
acct.grid.Draw(ctx)
}
func (acct *AccountView) MouseEvent(localX int, localY int, event vaxis.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) SetDirectories(d DirectoryLister) {
if acct.grid != nil {
acct.grid.ReplaceChild(acct.dirlist, d)
}
acct.dirlist = d
}
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 acct.msglist == nil || acct.msglist.Store() == nil {
return nil, errors.New("init in progress")
}
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() ([]models.UID, error) {
if store := acct.Store(); store != nil {
return store.Marker().Marked(), nil
}
return nil, errors.New("no store available")
}
func (acct *AccountView) SelectedMessagePart() *PartInfo {
return nil
}
func (acct *AccountView) Terminal() *Terminal {
if acct.split == nil {
return nil
}
return acct.split.Terminal()
}
func (acct *AccountView) isSelected() bool {
return acct == SelectedAccount()
}
func (acct *AccountView) newStore(name string) *lib.MessageStore {
uiConf := acct.dirlist.UiConfig(name)
dir := acct.dirlist.Directory(name)
role := ""
if dir != nil {
role = string(dir.Role)
}
backend := acct.AccountConfig().Backend
store := lib.NewMessageStore(acct.worker, name,
func() *config.UIConfig {
return config.Ui.
ForAccount(acct.Name()).
ForFolder(name)
},
func(msg *models.MessageInfo) {
err := hooks.RunHook(&hooks.MailReceived{
Account: acct.Name(),
Backend: backend,
Folder: name,
Role: role,
MsgInfo: msg,
})
if err != nil {
msg := fmt.Sprintf("mail-received hook: %s", err)
PushError(msg)
}
}, func() {
if uiConf.NewMessageBell {
aerc.Beep()
}
}, func() {
err := hooks.RunHook(&hooks.MailDeleted{
Account: acct.Name(),
Backend: backend,
Folder: name,
Role: role,
})
if err != nil {
msg := fmt.Sprintf("mail-deleted hook: %s", err)
PushError(msg)
}
}, func(dest string) {
err := hooks.RunHook(&hooks.MailAdded{
Account: acct.Name(),
Backend: backend,
Folder: dest,
Role: role,
})
if err != nil {
msg := fmt.Sprintf("mail-added hook: %s", err)
PushError(msg)
}
}, func(add []string, remove []string) {
err := hooks.RunHook(&hooks.TagModified{
Account: acct.Name(),
Backend: backend,
Add: add,
Remove: remove,
})
if err != nil {
msg := fmt.Sprintf("tag-modified hook: %s", err)
PushError(msg)
}
}, func(flagname string) {
err := hooks.RunHook(&hooks.FlagChanged{
Account: acct.Name(),
Backend: backend,
Folder: acct.SelectedDirectory(),
Role: role,
FlagName: flagname,
})
if err != nil {
msg := fmt.Sprintf("flag-changed hook: %s", err)
PushError(msg)
}
},
func(msg *models.MessageInfo) {
acct.updateSplitView(msg)
auto := false
if c := acct.AccountConfig(); c != nil {
r, ok := c.Params["pama-auto-switch"]
if ok {
if strings.ToLower(r) == "true" {
auto = true
}
}
}
if !auto {
return
}
var name string
if msg != nil && msg.Envelope != nil {
name = pama.FromSubject(msg.Envelope.Subject)
}
pama.DebouncedSwitchProject(name)
},
)
store.Configure(acct.SortCriteria(uiConf))
store.SetMarker(marker.New(store))
return store
}
func (acct *AccountView) onMessage(msg types.WorkerMessage) {
msg = acct.worker.ProcessMessage(msg)
switch msg := msg.(type) {
case *types.Done:
switch resp := msg.InResponseTo().(type) {
case *types.Connect, *types.Reconnect:
acct.SetStatus(state.ConnectionActivity("Listing mailboxes..."))
log.Infof("[%s] connected.", acct.acct.Name)
acct.SetStatus(state.SetConnected(true))
log.Tracef("Listing mailboxes...")
acct.worker.PostAction(&types.ListDirectories{}, nil)
case *types.Disconnect:
acct.dirlist.ClearList()
acct.msglist.SetStore(nil)
log.Infof("[%s] disconnected.", acct.acct.Name)
acct.SetStatus(state.SetConnected(false))
case *types.OpenDirectory:
acct.dirlist.Update(msg)
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)
acct.Store().Update(msg.InResponseTo())
} else {
acct.msglist.SetStore(nil)
}
case *types.CreateDirectory:
store := acct.newStore(resp.Directory)
acct.dirlist.SetMsgStore(&models.Directory{
Name: resp.Directory,
}, store)
acct.dirlist.Update(msg)
case *types.RemoveDirectory:
acct.dirlist.Update(msg)
case *types.FetchMessageHeaders:
if acct.newConn {
acct.checkMailOnStartup()
}
case *types.ListDirectories:
acct.dirlist.Update(msg)
if dir := acct.dirlist.Selected(); dir != "" {
acct.dirlist.Select(dir)
return
}
// Nothing selected, select based on config
dirs := acct.dirlist.List()
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.newConn = true
}
case *types.Directory:
store, ok := acct.dirlist.MsgStore(msg.Dir.Name)
if !ok {
store = acct.newStore(msg.Dir.Name)
}
acct.dirlist.SetMsgStore(msg.Dir, store)
case *types.DirectoryInfo:
acct.dirlist.Update(msg)
case *types.DirectoryContents:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
if acct.msglist.Store() == nil {
acct.msglist.SetStore(store)
}
store.Update(msg)
acct.SetStatus(state.Threading(store.ThreadedView()))
}
if acct.newConn && len(msg.Uids) == 0 {
acct.checkMailOnStartup()
}
case *types.DirectoryThreaded:
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
if acct.msglist.Store() == nil {
acct.msglist.SetStore(store)
}
store.Update(msg)
acct.SetStatus(state.Threading(store.ThreadedView()))
}
if acct.newConn && len(msg.Threads) == 0 {
acct.checkMailOnStartup()
}
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 dir := acct.dirlist.SelectedDirectory(); dir != nil {
dir.Exists -= len(msg.Uids)
}
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
store.Update(msg)
}
case *types.MessagesCopied:
acct.updateDirCounts(msg.Destination, msg.Uids)
case *types.MessagesMoved:
acct.updateDirCounts(msg.Destination, msg.Uids)
case *types.LabelList:
acct.labels = msg.Labels
case *types.ConnError:
log.Errorf("[%s] connection error: %v", acct.acct.Name, msg.Error)
acct.SetStatus(state.SetConnected(false))
acct.PushError(msg.Error)
acct.msglist.SetStore(nil)
acct.worker.PostAction(&types.Reconnect{}, nil)
case *types.Error:
log.Errorf("[%s] unexpected error: %v", acct.acct.Name, msg.Error)
acct.PushError(msg.Error)
}
acct.UpdateStatus()
acct.setTitle()
}
func (acct *AccountView) updateDirCounts(destination string, uids []models.UID) {
// Only update the destination destDir if it is initialized
if destDir := acct.dirlist.Directory(destination); destDir != nil {
var recent, unseen int
var accurate bool = true
for _, uid := range uids {
// Get the message from the originating store
msg, ok := acct.Store().Messages[uid]
if !ok {
continue
}
// If message that was not yet loaded is copied
if msg == nil {
accurate = false
break
}
seen := msg.Flags.Has(models.SeenFlag)
if msg.Flags.Has(models.RecentFlag) {
recent++
}
if !seen {
unseen++
}
}
if accurate {
destDir.Recent += recent
destDir.Unseen += unseen
destDir.Exists += len(uids)
} else {
destDir.Exists += len(uids)
}
}
}
func (acct *AccountView) SortCriteria(uiConf *config.UIConfig) []*types.SortCriterion {
if uiConf == nil {
return nil
}
if len(uiConf.Sort) == 0 {
return nil
}
criteria, err := sort.GetSortCriteria(uiConf.Sort)
if err != nil {
acct.PushError(fmt.Errorf("ui sort: %w", err))
return nil
}
return criteria
}
func (acct *AccountView) GetSortCriteria() []*types.SortCriterion {
return acct.SortCriteria(acct.UiConfig())
}
func (acct *AccountView) CheckMail() {
acct.Lock()
defer acct.Unlock()
if acct.checkingMail {
return
}
// Exclude selected mailbox, per IMAP specification
exclude := append(acct.AccountConfig().CheckMailExclude, acct.dirlist.Selected()) //nolint:gocritic // intentional append to different slice
dirs := acct.dirlist.List()
dirs = acct.dirlist.FilterDirs(dirs, acct.AccountConfig().CheckMailInclude, false)
dirs = acct.dirlist.FilterDirs(dirs, exclude, true)
log.Debugf("Checking for new mail on account %s", acct.Name())
acct.SetStatus(state.ConnectionActivity("Checking for new mail..."))
msg := &types.CheckMail{
Directories: dirs,
Command: acct.acct.CheckMailCmd,
Timeout: acct.acct.CheckMailTimeout,
}
acct.checkingMail = true
var cb func(types.WorkerMessage)
cb = func(response types.WorkerMessage) {
dirsMsg, ok := response.(*types.CheckMailDirectories)
if ok {
checkMailMsg := &types.CheckMail{
Directories: dirsMsg.Directories,
Command: acct.acct.CheckMailCmd,
Timeout: acct.acct.CheckMailTimeout,
}
acct.worker.PostAction(checkMailMsg, cb)
} else { // Done
acct.SetStatus(state.ConnectionActivity(""))
acct.Lock()
acct.checkingMail = false
acct.Unlock()
}
}
acct.worker.PostAction(msg, cb)
}
// CheckMailReset resets the check-mail timer
func (acct *AccountView) CheckMailReset() {
if acct.ticker != nil {
d := acct.AccountConfig().CheckMail
acct.ticker = time.NewTicker(d)
}
}
func (acct *AccountView) checkMailOnStartup() {
if acct.AccountConfig().CheckMail.Minutes() > 0 {
acct.newConn = false
acct.CheckMail()
}
}
func (acct *AccountView) CheckMailTimer(d time.Duration) {
acct.ticker = time.NewTicker(d)
go func() {
defer log.PanicHandler()
for range acct.ticker.C {
if !acct.state.Connected {
continue
}
acct.CheckMail()
}
}()
}
func (acct *AccountView) closeSplit() {
if acct.split != nil {
acct.split.Close()
}
acct.splitSize = 0
acct.splitDir = config.SPLIT_NONE
acct.split = nil
acct.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 acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig()))
acct.grid.AddChild(acct.msglist).At(0, 1)
ui.Invalidate()
}
func (acct *AccountView) updateSplitView(msg *models.MessageInfo) {
uiConf := acct.UiConfig()
if !acct.splitLoaded {
switch uiConf.MessageListSplit.Direction {
case config.SPLIT_HORIZONTAL:
acct.Split(uiConf.MessageListSplit.Size)
case config.SPLIT_VERTICAL:
acct.Vsplit(uiConf.MessageListSplit.Size)
}
acct.splitLoaded = true
}
if acct.splitSize == 0 || !acct.splitLoaded {
return
}
if acct.splitDebounce != nil {
acct.splitDebounce.Stop()
}
fn := func() {
if acct.split != nil {
acct.grid.RemoveChild(acct.split)
acct.split.Close()
}
lib.NewMessageStoreView(msg, false, acct.Store(), CryptoProvider(), DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
PushError(err.Error())
return
}
viewer, err := NewMessageViewer(acct, view)
if err != nil {
PushError(err.Error())
return
}
acct.split = viewer
switch acct.splitDir {
case config.SPLIT_HORIZONTAL:
acct.grid.AddChild(acct.split).At(1, 1)
case config.SPLIT_VERTICAL:
acct.grid.AddChild(acct.split).At(0, 2)
}
})
}
acct.splitDebounce = time.AfterFunc(100*time.Millisecond, func() {
ui.QueueFunc(fn)
})
}
func (acct *AccountView) SplitSize() int {
return acct.splitSize
}
func (acct *AccountView) SetSplitSize(n int) {
if n == 0 {
acct.closeSplit()
}
acct.splitSize = n
}
// Split splits the message list view horizontally. The message list will be n
// rows high. If n is 0, any existing split is removed
func (acct *AccountView) Split(n int) {
acct.SetSplitSize(n)
if acct.splitDir == config.SPLIT_HORIZONTAL || n == 0 {
return
}
acct.splitDir = config.SPLIT_HORIZONTAL
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
// Add 1 so that the splitSize is the number of visible messages
{Strategy: ui.SIZE_EXACT, Size: func() int { return acct.SplitSize() + 1 }},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}).Columns([]ui.GridSpec{
{Strategy: ui.SIZE_EXACT, Size: func() int {
return acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig())).Span(2, 1)
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_BOTTOM, acct.UiConfig())).At(0, 1)
acct.split, _ = NewMessageViewer(acct, nil)
acct.grid.AddChild(acct.split).At(1, 1)
msg, err := acct.SelectedMessage()
if err != nil {
log.Debugf("split: load message error: %v", err)
}
acct.updateSplitView(msg)
}
// Vsplit splits the message list view vertically. The message list will be n
// rows wide. If n is 0, any existing split is removed
func (acct *AccountView) Vsplit(n int) {
acct.SetSplitSize(n)
if acct.splitDir == config.SPLIT_VERTICAL || n == 0 {
return
}
acct.splitDir = config.SPLIT_VERTICAL
acct.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 acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_EXACT, Size: acct.SplitSize},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig())).At(0, 0)
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_RIGHT, acct.UiConfig())).At(0, 1)
acct.split, _ = NewMessageViewer(acct, nil)
acct.grid.AddChild(acct.split).At(0, 2)
msg, err := acct.SelectedMessage()
if err != nil {
log.Debugf("split: load message error: %v", err)
}
acct.updateSplitView(msg)
}
// setTitle executes the title template and sets the tab title
func (acct *AccountView) setTitle() {
if acct.tab == nil {
return
}
data := state.NewDataSetter()
data.SetAccount(acct.acct)
data.SetFolder(acct.Directories().SelectedDirectory())
data.SetRUE(acct.dirlist.List(), acct.dirlist.GetRUECount)
data.SetState(&acct.state)
var buf bytes.Buffer
err := templates.Render(acct.UiConfig().TabTitleAccount, &buf, data.Data())
if err != nil {
acct.PushError(err)
return
}
acct.tab.SetTitle(buf.String())
}