mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-01-01 16:01:22 +01:00
When reloading the configuration with :reload, global variables in the
config package are reset to their startup values and then, the config is
parsed from disk. While the parsing is done, these variables are
temporarily in an inconsistent and possibly invalid state.
When commands are executed interactively from aerc, they are handled by
the main goroutine which also deals with UI rendering. No UI render will
be done while :reload is in progress.
However, the IPC socket handler runs in an independent goroutine. This
has the unfortunate side effect to let the UI goroutine to run while
config parsing is in progress and causes crashes:
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6bb142]
goroutine 1 [running]:
git.sr.ht/~rjarry/aerc/lib/log.PanicHandler()
lib/log/panic-logger.go:51 +0x6cf
panic({0xc1d960?, 0x134a6e0?})
/usr/lib/go/src/runtime/panic.go:783 +0x132
git.sr.ht/~rjarry/aerc/config.(*StyleConf).getStyle(0xc00038b908?, 0x4206b7?)
config/style.go:386 +0x42
git.sr.ht/~rjarry/aerc/config.StyleSet.Get({0x0, 0x0, 0x0, {0x0, 0x0, 0x0}}, 0x421a65?, 0x0)
config/style.go:408 +0x8b
git.sr.ht/~rjarry/aerc/config.(*UIConfig).GetStyle(...)
config/ui.go:379
git.sr.ht/~rjarry/aerc/lib/ui.(*TabStrip).Draw(0xc000314700, 0xc000192230)
lib/ui/tab.go:378 +0x15b
git.sr.ht/~rjarry/aerc/lib/ui.(*Grid).Draw(0xc000186fc0, 0xc0002c25f0)
lib/ui/grid.go:126 +0x28e
git.sr.ht/~rjarry/aerc/app.(*Aerc).Draw(0x14b9f00, 0xc0002c25f0)
app/aerc.go:192 +0x1fe
git.sr.ht/~rjarry/aerc/lib/ui.Render()
lib/ui/ui.go:155 +0x16b
main.main()
main.go:310 +0x997
Make the reload operation safe by changing how config objects are
exposed and updated. Change all objects to be atomic pointers. Expose
public functions to access their value atomically. Only update their
value after a complete and successful config parse. This way the UI
thread will always have access to a valid configuration.
NB: The account configuration is not included in this change since it
cannot be reloaded.
Fixes: https://todo.sr.ht/~rjarry/aerc/319
Reported-by: Anachron <gith@cron.world>
Signed-off-by: Robin Jarry <robin@jarry.cc>
804 lines
17 KiB
Go
804 lines
17 KiB
Go
package state
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"github.com/danwakefield/fnmatch"
|
|
sortthread "github.com/emersion/go-imap-sortthread"
|
|
"github.com/emersion/go-message/mail"
|
|
)
|
|
|
|
type Composer interface {
|
|
AddAttachment(string)
|
|
}
|
|
|
|
type DataSetter interface {
|
|
Data() models.TemplateData
|
|
SetHeaders(*mail.Header, *models.OriginalMail)
|
|
SetInfo(*models.MessageInfo, int, bool)
|
|
SetVisual(bool)
|
|
SetThreading(ThreadInfo)
|
|
SetComposer(Composer)
|
|
SetAccount(*config.AccountConfig)
|
|
SetFolder(*models.Directory)
|
|
SetRUE([]string, func(string) (int, int, int))
|
|
SetState(s *AccountState)
|
|
SetPendingKeys([]config.KeyStroke)
|
|
SetHasNew(bool)
|
|
SetBell(bool)
|
|
SetTitle(string)
|
|
}
|
|
|
|
type ThreadInfo struct {
|
|
SameSubject bool
|
|
Prefix string
|
|
Count int
|
|
Unread int
|
|
Folded bool
|
|
Context bool
|
|
Orphan bool
|
|
}
|
|
|
|
type templateData struct {
|
|
// only available when composing/replying/forwarding
|
|
headers *mail.Header
|
|
// only available when replying with a quote
|
|
parent *models.OriginalMail
|
|
// only available for the message list
|
|
info *models.MessageInfo
|
|
marked bool
|
|
msgNum int
|
|
visual bool
|
|
hasNew bool
|
|
|
|
// message list threading
|
|
threadInfo ThreadInfo
|
|
|
|
// selected account
|
|
account *config.AccountConfig
|
|
myAddresses map[string]bool
|
|
folder *models.Directory // selected folder
|
|
folders []string
|
|
getRUEcount func(string) (int, int, int)
|
|
|
|
state *AccountState
|
|
pendingKeys []config.KeyStroke
|
|
|
|
composer Composer
|
|
|
|
bell bool
|
|
title string
|
|
}
|
|
|
|
func NewDataSetter() DataSetter {
|
|
return &templateData{}
|
|
}
|
|
|
|
// Data returns the template data
|
|
func (d *templateData) Data() models.TemplateData {
|
|
return d
|
|
}
|
|
|
|
// only used for compose/reply/forward
|
|
func (d *templateData) SetHeaders(h *mail.Header, o *models.OriginalMail) {
|
|
d.headers = h
|
|
d.parent = o
|
|
}
|
|
|
|
// only used for message list templates
|
|
func (d *templateData) SetInfo(info *models.MessageInfo, num int, marked bool,
|
|
) {
|
|
d.info = info
|
|
d.msgNum = num
|
|
d.marked = marked
|
|
}
|
|
|
|
func (d *templateData) SetVisual(visual bool) {
|
|
d.visual = visual
|
|
}
|
|
|
|
func (d *templateData) SetThreading(info ThreadInfo) {
|
|
d.threadInfo = info
|
|
}
|
|
|
|
func (d *templateData) SetAccount(acct *config.AccountConfig) {
|
|
d.account = acct
|
|
d.myAddresses = make(map[string]bool)
|
|
if acct != nil {
|
|
d.myAddresses[acct.From.Address] = true
|
|
for _, addr := range acct.Aliases {
|
|
d.myAddresses[addr.Address] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *templateData) SetFolder(folder *models.Directory) {
|
|
d.folder = folder
|
|
}
|
|
|
|
func (d *templateData) SetComposer(c Composer) {
|
|
d.composer = c
|
|
}
|
|
|
|
func (d *templateData) SetHasNew(hasNew bool) {
|
|
d.hasNew = hasNew
|
|
}
|
|
|
|
func (d *templateData) SetRUE(folders []string,
|
|
cb func(string) (int, int, int),
|
|
) {
|
|
d.folders = folders
|
|
d.getRUEcount = cb
|
|
}
|
|
|
|
func (d *templateData) SetState(state *AccountState) {
|
|
d.state = state
|
|
}
|
|
|
|
func (d *templateData) SetPendingKeys(keys []config.KeyStroke) {
|
|
d.pendingKeys = keys
|
|
}
|
|
|
|
func (d *templateData) Attach(s string) string {
|
|
if d.composer != nil {
|
|
d.composer.AddAttachment(s)
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("Failed to attach: %s", s)
|
|
}
|
|
|
|
func (d *templateData) Account() string {
|
|
if d.account != nil {
|
|
return d.account.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) AccountBackend() string {
|
|
if d.account != nil {
|
|
return d.account.Backend
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) AccountFrom() *mail.Address {
|
|
if d.account != nil {
|
|
return d.account.From
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *templateData) Folder() string {
|
|
if d.folder != nil {
|
|
return d.folder.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) Role() string {
|
|
if d.folder != nil {
|
|
return string(d.folder.Role)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) ui() *config.UIConfig {
|
|
return config.Ui().ForAccount(d.Account()).ForFolder(d.Folder())
|
|
}
|
|
|
|
func (d *templateData) To() []*mail.Address {
|
|
var to []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
to = d.info.Envelope.To
|
|
case d.headers != nil:
|
|
to, _ = d.headers.AddressList("to")
|
|
}
|
|
return to
|
|
}
|
|
|
|
func (d *templateData) Cc() []*mail.Address {
|
|
var cc []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
cc = d.info.Envelope.Cc
|
|
case d.headers != nil:
|
|
cc, _ = d.headers.AddressList("cc")
|
|
}
|
|
return cc
|
|
}
|
|
|
|
func (d *templateData) Bcc() []*mail.Address {
|
|
var bcc []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
bcc = d.info.Envelope.Bcc
|
|
case d.headers != nil:
|
|
bcc, _ = d.headers.AddressList("bcc")
|
|
}
|
|
return bcc
|
|
}
|
|
|
|
func (d *templateData) From() []*mail.Address {
|
|
var from []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
from = d.info.Envelope.From
|
|
case d.headers != nil:
|
|
from, _ = d.headers.AddressList("from")
|
|
}
|
|
return from
|
|
}
|
|
|
|
func (d *templateData) Peer() []*mail.Address {
|
|
var from, to []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
from = d.info.Envelope.From
|
|
to = d.info.Envelope.To
|
|
case d.headers != nil:
|
|
from, _ = d.headers.AddressList("from")
|
|
to, _ = d.headers.AddressList("to")
|
|
}
|
|
for _, addr := range from {
|
|
for myAddr := range d.myAddresses {
|
|
if fnmatch.Match(myAddr, addr.Address, 0) {
|
|
return to
|
|
}
|
|
}
|
|
}
|
|
return from
|
|
}
|
|
|
|
func (d *templateData) ReplyTo() []*mail.Address {
|
|
var replyTo []*mail.Address
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
replyTo = d.info.Envelope.ReplyTo
|
|
case d.headers != nil:
|
|
replyTo, _ = d.headers.AddressList("reply-to")
|
|
}
|
|
return replyTo
|
|
}
|
|
|
|
func (d *templateData) Date() time.Time {
|
|
var date time.Time
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
date = d.info.Envelope.Date
|
|
case d.info != nil:
|
|
date = d.info.InternalDate
|
|
default:
|
|
date = time.Now()
|
|
}
|
|
return date
|
|
}
|
|
|
|
func (d *templateData) DateAutoFormat(date time.Time) string {
|
|
if date.IsZero() {
|
|
return ""
|
|
}
|
|
ui := d.ui()
|
|
year := date.Year()
|
|
day := date.YearDay()
|
|
now := time.Now()
|
|
thisYear := now.Year()
|
|
thisDay := now.YearDay()
|
|
fmt := ui.TimestampFormat
|
|
if year == thisYear {
|
|
switch {
|
|
case day == thisDay && ui.ThisDayTimeFormat != "":
|
|
fmt = ui.ThisDayTimeFormat
|
|
case day > thisDay-7 && ui.ThisWeekTimeFormat != "":
|
|
fmt = ui.ThisWeekTimeFormat
|
|
case ui.ThisYearTimeFormat != "":
|
|
fmt = ui.ThisYearTimeFormat
|
|
}
|
|
}
|
|
return date.Format(fmt)
|
|
}
|
|
|
|
func (d *templateData) Header(name string) string {
|
|
var h *mail.Header
|
|
switch {
|
|
case d.headers != nil:
|
|
h = d.headers
|
|
case d.info != nil && d.info.RFC822Headers != nil:
|
|
h = d.info.RFC822Headers
|
|
default:
|
|
return ""
|
|
}
|
|
text, err := h.Text(name)
|
|
if err != nil {
|
|
text = h.Get(name)
|
|
}
|
|
return text
|
|
}
|
|
|
|
func (d *templateData) ThreadPrefix() string {
|
|
return d.threadInfo.Prefix
|
|
}
|
|
|
|
func (d *templateData) ThreadCount() int {
|
|
return d.threadInfo.Count
|
|
}
|
|
|
|
func (d *templateData) ThreadUnread() int {
|
|
return d.threadInfo.Unread
|
|
}
|
|
|
|
func (d *templateData) ThreadFolded() bool {
|
|
return d.threadInfo.Folded
|
|
}
|
|
|
|
func (d *templateData) ThreadContext() bool {
|
|
return d.threadInfo.Context
|
|
}
|
|
|
|
func (d *templateData) ThreadOrphan() bool {
|
|
return d.threadInfo.Orphan
|
|
}
|
|
|
|
func (d *templateData) Subject() string {
|
|
var subject string
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
subject = d.info.Envelope.Subject
|
|
case d.headers != nil:
|
|
subject = d.Header("subject")
|
|
}
|
|
if d.threadInfo.SameSubject {
|
|
subject = ""
|
|
} else if subject == "" {
|
|
subject = config.Ui().EmptySubject
|
|
}
|
|
return subject
|
|
}
|
|
|
|
func (d *templateData) SubjectBase() string {
|
|
var subject string
|
|
switch {
|
|
case d.info != nil && d.info.Envelope != nil:
|
|
subject = d.info.Envelope.Subject
|
|
case d.headers != nil:
|
|
subject = d.Header("subject")
|
|
}
|
|
base, _ := sortthread.GetBaseSubject(subject)
|
|
return base
|
|
}
|
|
|
|
func (d *templateData) Number() int {
|
|
return d.msgNum
|
|
}
|
|
|
|
func (d *templateData) Labels() []string {
|
|
if d.info == nil {
|
|
return nil
|
|
}
|
|
return d.info.Labels
|
|
}
|
|
|
|
func (d *templateData) Filename() string {
|
|
if d.info == nil {
|
|
return ""
|
|
}
|
|
if (d.info.Filenames != nil) && len(d.info.Filenames) > 0 {
|
|
return d.info.Filenames[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) Filenames() []string {
|
|
if d.info == nil {
|
|
return nil
|
|
}
|
|
return d.info.Filenames
|
|
}
|
|
|
|
func (d *templateData) Flags() []string {
|
|
var flags []string
|
|
if d.info == nil {
|
|
return flags
|
|
}
|
|
|
|
switch {
|
|
case d.info.Flags.Has(models.SeenFlag | models.AnsweredFlag):
|
|
flags = append(flags, d.ui().IconReplied) // message has been replied to
|
|
case d.info.Flags.Has(models.SeenFlag):
|
|
break
|
|
case d.info.Flags.Has(models.RecentFlag):
|
|
flags = append(flags, d.ui().IconNew) // message is unread and new
|
|
default:
|
|
flags = append(flags, d.ui().IconOld) // message is unread and old
|
|
}
|
|
if d.info.Flags.Has(models.DraftFlag) {
|
|
flags = append(flags, d.ui().IconDraft)
|
|
}
|
|
if d.info.Flags.Has(models.DeletedFlag) {
|
|
flags = append(flags, d.ui().IconDeleted)
|
|
}
|
|
if d.info.Flags.Has(models.ForwardedFlag) {
|
|
flags = append(flags, d.ui().IconForwarded)
|
|
}
|
|
if d.info.BodyStructure != nil {
|
|
for _, bS := range d.info.BodyStructure.Parts {
|
|
if strings.ToLower(bS.Disposition) == "attachment" {
|
|
flags = append(flags, d.ui().IconAttachment)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if d.info.Flags.Has(models.FlaggedFlag) {
|
|
flags = append(flags, d.ui().IconFlagged)
|
|
}
|
|
if d.marked {
|
|
flags = append(flags, d.ui().IconMarked)
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func (d *templateData) IsReplied() bool {
|
|
if d.info != nil && d.info.Flags.Has(models.AnsweredFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsForwarded() bool {
|
|
if d.info != nil && d.info.Flags.Has(models.ForwardedFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) HasAttachment() bool {
|
|
if d.info != nil && d.info.BodyStructure != nil {
|
|
for _, bS := range d.info.BodyStructure.Parts {
|
|
if strings.ToLower(bS.Disposition) == "attachment" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsRecent() bool {
|
|
if d.info != nil && d.info.Flags.Has(models.RecentFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsUnread() bool {
|
|
if d.info != nil && !d.info.Flags.Has(models.SeenFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsFlagged() bool {
|
|
if d.info != nil && d.info.Flags.Has(models.FlaggedFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsDraft() bool {
|
|
if d.info != nil && d.info.Flags.Has(models.DraftFlag) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) IsMarked() bool {
|
|
return d.marked
|
|
}
|
|
|
|
func (d *templateData) MessageId() string {
|
|
if d.info == nil || d.info.Envelope == nil {
|
|
return ""
|
|
}
|
|
return d.info.Envelope.MessageId
|
|
}
|
|
|
|
func (d *templateData) Size() int {
|
|
if d.info == nil || d.info.Envelope == nil {
|
|
return 0
|
|
}
|
|
return int(d.info.Size)
|
|
}
|
|
|
|
func (d *templateData) OriginalText() string {
|
|
if d.parent == nil {
|
|
return ""
|
|
}
|
|
return d.parent.Text
|
|
}
|
|
|
|
func (d *templateData) OriginalDate() time.Time {
|
|
if d.parent == nil {
|
|
return time.Time{}
|
|
}
|
|
return d.parent.Date
|
|
}
|
|
|
|
func (d *templateData) OriginalFrom() []*mail.Address {
|
|
if d.parent == nil || d.parent.RFC822Headers == nil {
|
|
return nil
|
|
}
|
|
from, _ := d.parent.RFC822Headers.AddressList("from")
|
|
return from
|
|
}
|
|
|
|
func (d *templateData) OriginalMIMEType() string {
|
|
if d.parent == nil {
|
|
return ""
|
|
}
|
|
return d.parent.MIMEType
|
|
}
|
|
|
|
func (d *templateData) OriginalHeader(name string) string {
|
|
if d.parent == nil || d.parent.RFC822Headers == nil {
|
|
return ""
|
|
}
|
|
text, err := d.parent.RFC822Headers.Text(name)
|
|
if err != nil {
|
|
text = d.parent.RFC822Headers.Get(name)
|
|
}
|
|
return text
|
|
}
|
|
|
|
func (d *templateData) rue(folders ...string) (int, int, int) {
|
|
var recent, unread, exists int
|
|
if d.getRUEcount != nil {
|
|
if len(folders) == 0 {
|
|
folders = d.folders
|
|
}
|
|
for _, dir := range folders {
|
|
r, u, e := d.getRUEcount(dir)
|
|
recent += r
|
|
unread += u
|
|
exists += e
|
|
}
|
|
}
|
|
return recent, unread, exists
|
|
}
|
|
|
|
func (d *templateData) Recent(folders ...string) int {
|
|
r, _, _ := d.rue(folders...)
|
|
return r
|
|
}
|
|
|
|
func (d *templateData) Unread(folders ...string) int {
|
|
_, u, _ := d.rue(folders...)
|
|
return u
|
|
}
|
|
|
|
func (d *templateData) HasNew() bool {
|
|
return d.hasNew
|
|
}
|
|
|
|
func (d *templateData) Exists(folders ...string) int {
|
|
_, _, e := d.rue(folders...)
|
|
return e
|
|
}
|
|
|
|
func (d *templateData) RUE(folders ...string) string {
|
|
r, u, e := d.rue(folders...)
|
|
switch {
|
|
case r > 0:
|
|
return fmt.Sprintf("%d/%d/%d", r, u, e)
|
|
case u > 0:
|
|
return fmt.Sprintf("%d/%d", u, e)
|
|
case e > 0:
|
|
return fmt.Sprintf("%d", e)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (d *templateData) Connected() bool {
|
|
if d.state != nil {
|
|
return d.state.Connected
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *templateData) ConnectionInfo() string {
|
|
switch {
|
|
case d.state == nil:
|
|
return ""
|
|
case d.state.connActivity != "":
|
|
return d.state.connActivity
|
|
case d.state.Connected:
|
|
return texter().Connected()
|
|
default:
|
|
return texter().Disconnected()
|
|
}
|
|
}
|
|
|
|
func (d *templateData) ContentInfo() string {
|
|
if d.state == nil {
|
|
return ""
|
|
}
|
|
var content []string
|
|
fldr := d.state.folderState(d.Folder())
|
|
if fldr.FilterActivity != "" {
|
|
content = append(content, fldr.FilterActivity)
|
|
} else if fldr.Filter != "" {
|
|
content = append(content, texter().FormatFilter(fldr.Filter))
|
|
}
|
|
if fldr.Search != "" {
|
|
content = append(content, texter().FormatSearch(fldr.Search))
|
|
}
|
|
return strings.Join(content, config.Statusline().Separator)
|
|
}
|
|
|
|
func (d *templateData) StatusInfo() string {
|
|
stat := d.ConnectionInfo()
|
|
if content := d.ContentInfo(); content != "" {
|
|
stat += config.Statusline().Separator + content
|
|
}
|
|
return stat
|
|
}
|
|
|
|
func (d *templateData) TrayInfo() string {
|
|
if d.state == nil {
|
|
return ""
|
|
}
|
|
var tray []string
|
|
fldr := d.state.folderState(d.Folder())
|
|
if fldr.Sorting {
|
|
tray = append(tray, texter().Sorting())
|
|
}
|
|
if fldr.Threading {
|
|
tray = append(tray, texter().Threading())
|
|
}
|
|
if d.state.passthrough {
|
|
tray = append(tray, texter().Passthrough())
|
|
}
|
|
if d.visual {
|
|
tray = append(tray, texter().Visual())
|
|
}
|
|
return strings.Join(tray, config.Statusline().Separator)
|
|
}
|
|
|
|
func (d *templateData) PendingKeys() string {
|
|
return config.FormatKeyStrokes(d.pendingKeys)
|
|
}
|
|
|
|
func (d *templateData) Style(content, name string) string {
|
|
cfg := config.Ui().ForAccount(d.Account())
|
|
style := cfg.GetUserStyle(name)
|
|
return ui.ApplyStyle(style, content)
|
|
}
|
|
|
|
func (d *templateData) StyleSwitch(content string, cases ...models.Case) string {
|
|
for _, c := range cases {
|
|
if c.Matches(content) {
|
|
cfg := config.Ui().ForAccount(d.Account())
|
|
style := cfg.GetUserStyle(c.Value())
|
|
return ui.ApplyStyle(style, content)
|
|
}
|
|
}
|
|
return content
|
|
}
|
|
|
|
func (d *templateData) StyleMap(elems []string, cases ...models.Case) []string {
|
|
mapped := make([]string, 0, len(elems))
|
|
top:
|
|
for _, e := range elems {
|
|
for _, c := range cases {
|
|
if c.Matches(e) {
|
|
if c.Skip() {
|
|
continue top
|
|
}
|
|
cfg := config.Ui().ForAccount(d.Account())
|
|
style := cfg.GetUserStyle(c.Value())
|
|
e = ui.ApplyStyle(style, e)
|
|
break
|
|
}
|
|
}
|
|
mapped = append(mapped, e)
|
|
}
|
|
return mapped
|
|
}
|
|
|
|
func (d *templateData) Signature() string {
|
|
if d.account == nil {
|
|
return ""
|
|
}
|
|
var signature []byte
|
|
if d.account.SignatureCmd != "" {
|
|
var err error
|
|
signature, err = d.readSignatureFromCmd()
|
|
if err != nil {
|
|
var execErr *exec.ExitError
|
|
if errors.As(err, &execErr) {
|
|
log.Warnf("signature command failed with error (%d): %s", execErr.ExitCode(), execErr.Stderr)
|
|
}
|
|
signature = d.readSignatureFromFile()
|
|
}
|
|
} else {
|
|
signature = d.readSignatureFromFile()
|
|
}
|
|
if len(bytes.TrimSpace(signature)) == 0 {
|
|
return ""
|
|
}
|
|
signature = d.ensureSignatureDelimiter(signature)
|
|
return string(signature)
|
|
}
|
|
|
|
func (d *templateData) readSignatureFromCmd() ([]byte, error) {
|
|
sigCmd := d.account.SignatureCmd
|
|
cmd := exec.Command("sh", "-c", sigCmd)
|
|
env := os.Environ()
|
|
if d.account != nil {
|
|
env = append(env, fmt.Sprintf("AERC_ACCOUNT=%s", d.account.Name))
|
|
}
|
|
if d.folder != nil {
|
|
env = append(env, fmt.Sprintf("AERC_FOLDER=%s", d.folder.Name))
|
|
}
|
|
cmd.Env = env
|
|
signature, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signature, nil
|
|
}
|
|
|
|
func (d *templateData) readSignatureFromFile() []byte {
|
|
sigFile := d.account.SignatureFile
|
|
if sigFile == "" {
|
|
return nil
|
|
}
|
|
sigFile = xdg.ExpandHome(sigFile)
|
|
signature, err := os.ReadFile(sigFile)
|
|
if err != nil {
|
|
log.Errorf(" Error loading signature from file: %v", sigFile)
|
|
return nil
|
|
}
|
|
return signature
|
|
}
|
|
|
|
func (d *templateData) ensureSignatureDelimiter(signature []byte) []byte {
|
|
buf := bytes.NewBuffer(signature)
|
|
scanner := bufio.NewScanner(buf)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line == "-- " {
|
|
// signature contains standard delimiter, we're good
|
|
return signature
|
|
}
|
|
}
|
|
// signature does not contain standard delimiter, prepend one
|
|
sig := "\n\n-- \n" + strings.TrimLeft(string(signature), " \t\r\n")
|
|
return []byte(sig)
|
|
}
|
|
|
|
func (d *templateData) Bell() bool {
|
|
return d.bell
|
|
}
|
|
|
|
func (d *templateData) Title() string {
|
|
return d.title
|
|
}
|
|
|
|
func (d *templateData) SetBell(bell bool) {
|
|
d.bell = bell
|
|
}
|
|
|
|
func (d *templateData) SetTitle(title string) {
|
|
d.title = title
|
|
}
|