aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-02-01 23:35:42 +0100
committerRobin Jarry <robin@jarry.cc>2023-02-20 14:48:42 +0100
commitaaaa0c184fb43879cc84551983309cad06d665ee (patch)
tree93bb83d32495f679a70dbe5d3913549c29edf675
parent420a82a356d53e4b600ba54768f7ed21a43cf85e (diff)
downloadaerc-aaaa0c184fb43879cc84551983309cad06d665ee.tar.gz
triggers: use templates instead of % mini language
Since previous commit, all commands now support expanding text/template markup. Reuse that for the new-email trigger command. Update commands.ExecuteCommand to take optional *AccountConfig and *MessageInfo arguments. If these are nil, fallback to using the currently selected account and message (if any). Pass the proper *AccountConfig and *MessageInfo objects when firing the trigger command so that these are used instead of the currently selected ones. If new-email contains % placeholders, try to convert them to template markup reusing the same conversion added in commit 535300cfdbfc ("config: add columns based index format"). Warn the user that they need to update their configuration file. Signed-off-by: Robin Jarry <robin@jarry.cc> Reviewed-by: Moritz Poldrack <moritz@poldrack.dev>
-rw-r--r--CHANGELOG.md2
-rw-r--r--aerc.go15
-rw-r--r--commands/commands.go21
-rw-r--r--config/triggers.go94
-rw-r--r--config/ui.go134
-rw-r--r--doc/aerc-config.5.scd16
-rw-r--r--lib/format/format.go17
-rw-r--r--widgets/account.go10
-rw-r--r--widgets/aerc.go13
9 files changed, 178 insertions, 144 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67d0d264..7bd13e6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Deprecated
- `[ui].index-format` setting has been replaced by `index-columns`.
+- `[triggers].new-email` now needs to use `aerc-templates(7)` syntax instead
+ of the (now deprecated) `index-format` placeholders.
## [0.14.0](https://git.sr.ht/~rjarry/aerc/refs/0.14.0) - 2023-01-04
diff --git a/aerc.go b/aerc.go
index d563ccf8..36dcde0e 100644
--- a/aerc.go
+++ b/aerc.go
@@ -27,6 +27,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/templates"
libui "git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
+ "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~rjarry/aerc/worker/types"
)
@@ -60,10 +61,13 @@ func getCommands(selected libui.Drawable) []*commands.Commands {
}
}
-func execCommand(aerc *widgets.Aerc, ui *libui.UI, cmd []string) error {
+func execCommand(
+ aerc *widgets.Aerc, ui *libui.UI, cmd []string,
+ acct *config.AccountConfig, msg *models.MessageInfo,
+) error {
cmds := getCommands(aerc.SelectedTabContent())
for i, set := range cmds {
- err := set.ExecuteCommand(aerc, cmd)
+ err := set.ExecuteCommand(aerc, cmd, acct, msg)
if err != nil {
if errors.As(err, new(commands.NoSuchCommand)) {
if i == len(cmds)-1 {
@@ -189,8 +193,11 @@ func main() {
}
defer c.Close()
- aerc = widgets.NewAerc(c, func(cmd []string) error {
- return execCommand(aerc, ui, cmd)
+ aerc = widgets.NewAerc(c, func(
+ cmd []string, acct *config.AccountConfig,
+ msg *models.MessageInfo,
+ ) error {
+ return execCommand(aerc, ui, cmd, acct, msg)
}, func(cmd string) []string {
return getCompletions(aerc, cmd)
}, &commands.CmdHistory, deferLoop)
diff --git a/commands/commands.go b/commands/commands.go
index bbd03237..cf96a39e 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -70,15 +70,21 @@ type CommandSource interface {
Commands() *Commands
}
-func templateData(aerc *widgets.Aerc) models.TemplateData {
+func templateData(
+ aerc *widgets.Aerc,
+ cfg *config.AccountConfig,
+ msg *models.MessageInfo,
+) models.TemplateData {
var folder string
- var cfg *config.AccountConfig
- var msg *models.MessageInfo
acct := aerc.SelectedAccount()
if acct != nil {
folder = acct.SelectedDirectory()
+ }
+ if cfg == nil && acct != nil {
cfg = acct.AccountConfig()
+ }
+ if msg == nil && acct != nil {
msg, _ = acct.SelectedMessage()
}
@@ -91,14 +97,19 @@ func templateData(aerc *widgets.Aerc) models.TemplateData {
return &data
}
-func (cmds *Commands) ExecuteCommand(aerc *widgets.Aerc, args []string) error {
+func (cmds *Commands) ExecuteCommand(
+ aerc *widgets.Aerc,
+ args []string,
+ account *config.AccountConfig,
+ msg *models.MessageInfo,
+) error {
if len(args) == 0 {
return errors.New("Expected a command.")
}
if cmd, ok := cmds.dict()[args[0]]; ok {
log.Tracef("executing command %v", args)
var buf bytes.Buffer
- data := templateData(aerc)
+ data := templateData(aerc, account, msg)
processedArgs := make([]string, len(args))
for i, arg := range args {
diff --git a/config/triggers.go b/config/triggers.go
index 906928ae..c0e70d40 100644
--- a/config/triggers.go
+++ b/config/triggers.go
@@ -1,82 +1,62 @@
package config
import (
- "errors"
- "fmt"
-
"github.com/go-ini/ini"
"github.com/google/shlex"
"git.sr.ht/~rjarry/aerc/lib/format"
"git.sr.ht/~rjarry/aerc/log"
- "git.sr.ht/~rjarry/aerc/models"
)
type TriggersConfig struct {
- NewEmail string `ini:"new-email"`
- ExecuteCommand func(command []string) error
+ NewEmail []string `ini:"-"`
}
var Triggers = &TriggersConfig{}
func parseTriggers(file *ini.File) error {
+ var cmd string
triggers, err := file.GetSection("triggers")
if err != nil {
goto out
}
- if err := triggers.MapTo(&Triggers); err != nil {
- return err
- }
-out:
- log.Debugf("aerc.conf: [triggers] %#v", Triggers)
- return nil
-}
-
-func (trig *TriggersConfig) ExecTrigger(triggerCmd string,
- triggerFmt func(string) (string, error),
-) error {
- if len(triggerCmd) == 0 {
- return errors.New("Trigger command empty")
- }
- triggerCmdParts, err := shlex.Split(triggerCmd)
- if err != nil {
- return err
- }
-
- var command []string
- for _, part := range triggerCmdParts {
- formattedPart, err := triggerFmt(part)
+ if key := triggers.Key("new-email"); key != nil {
+ cmd = indexFmtRegexp.ReplaceAllStringFunc(
+ key.String(),
+ func(s string) string {
+ runes := []rune(s)
+ t, _ := indexVerbToTemplate(runes[len(runes)-1])
+ return t
+ },
+ )
+ Triggers.NewEmail, err = shlex.Split(cmd)
if err != nil {
return err
}
- command = append(command, formattedPart)
- }
- return trig.ExecuteCommand(command)
-}
-
-func (trig *TriggersConfig) ExecNewEmail(
- account *AccountConfig, msg *models.MessageInfo,
-) {
- err := trig.ExecTrigger(trig.NewEmail,
- func(part string) (string, error) {
- formatstr, args, err := format.ParseMessageFormat(
- part, Ui.TimestampFormat,
- Ui.ThisDayTimeFormat,
- Ui.ThisWeekTimeFormat,
- Ui.ThisYearTimeFormat,
- Ui.IconAttachment,
- format.Ctx{
- FromAddress: format.AddressForHumans(account.From),
- AccountName: account.Name,
- MsgInfo: msg,
- },
- )
- if err != nil {
- return "", err
- }
- return fmt.Sprintf(formatstr, args...), nil
- })
- if err != nil {
- log.Errorf("failed to run new-email trigger: %v", err)
+ if cmd != key.String() {
+ log.Warnf("%s %s",
+ "The new-email trigger now uses templates instead of %-based placeholders.",
+ "Backward compatibility will be removed in aerc 0.17.")
+ Warnings = append(Warnings, Warning{
+ Title: "FORMAT CHANGED: [triggers].new-email",
+ Body: `
+The new-email trigger now uses templates instead of %-based placeholders.
+
+Your configuration in this instance was automatically converted to:
+
+[triggers]
+new-email = ` + format.ShellQuote(Triggers.NewEmail) + `
+
+Your configuration file was not changed. To make this change permanent and to
+dismiss this warning on launch, replace the above line into aerc.conf. See
+aerc-config(5) for more details.
+
+The automatic conversion of new-email will be removed in aerc 0.17.
+`,
+ })
+ }
}
+out:
+ log.Debugf("aerc.conf: [triggers] %#v", Triggers)
+ return nil
}
diff --git a/config/ui.go b/config/ui.go
index db596d97..b59c9b09 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -391,75 +391,12 @@ func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) {
alignWidth := m[1]
verb := m[3]
- var f string
var width float64 = 0
var flags ColumnFlags = ALIGN_LEFT
- name := ""
-
- switch verb {
- case "%":
- f = verb
- case "a":
- f = `{{(index .From 0).Address}}`
- name = "sender"
- case "A":
- f = `{{if eq (len .ReplyTo) 0}}{{(index .From 0).Address}}{{else}}{{(index .ReplyTo 0).Address}}{{end}}`
- name = "reply-to"
- case "C":
- f = "{{.Number}}"
- name = "num"
- case "d", "D":
- f = "{{.DateAutoFormat .Date.Local}}"
- name = "date"
- case "f":
- f = `{{index (.From | persons) 0}}`
- name = "from"
- case "F":
- f = `{{.Peer | names | join ", "}}`
- name = "peers"
- case "g":
- f = `{{.Labels | join ", "}}`
- name = "labels"
- case "i":
- f = "{{.MessageId}}"
- name = "msg-id"
- case "n":
- f = `{{index (.From | names) 0}}`
- name = "name"
- case "r":
- f = `{{.To | persons | join ", "}}`
- name = "to"
- case "R":
- f = `{{.Cc | persons | join ", "}}`
- name = "cc"
- case "s":
- f = "{{.Subject}}"
- name = "subject"
- case "t":
- f = "{{(index .To 0).Address}}"
- name = "to0"
- case "T":
- f = "{{.Account}}"
- name = "account"
- case "u":
- f = "{{index (.From | mboxes) 0}}"
- name = "mboxes"
- case "v":
- f = "{{index (.From | names) 0}}"
- name = "name"
- case "Z":
- f = `{{.Flags | join ""}}`
- name = "flags"
+ f, name := indexVerbToTemplate([]rune(verb)[0])
+ if verb == "Z" {
width = 4
flags = ALIGN_RIGHT
- case "l":
- f = "{{.Size}}"
- name = "size"
- default:
- f = "%" + verb
- }
- if name == "" {
- name = "wtf"
}
t, err := templates.ParseTemplate(fmt.Sprintf("column-%s", name), f)
@@ -495,6 +432,73 @@ func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) {
return columns, nil
}
+func indexVerbToTemplate(verb rune) (f, name string) {
+ switch verb {
+ case '%':
+ f = string(verb)
+ case 'a':
+ f = `{{(index .From 0).Address}}`
+ name = "sender"
+ case 'A':
+ f = `{{if eq (len .ReplyTo) 0}}{{(index .From 0).Address}}{{else}}{{(index .ReplyTo 0).Address}}{{end}}`
+ name = "reply-to"
+ case 'C':
+ f = "{{.Number}}"
+ name = "num"
+ case 'd', 'D':
+ f = "{{.DateAutoFormat .Date.Local}}"
+ name = "date"
+ case 'f':
+ f = `{{index (.From | persons) 0}}`
+ name = "from"
+ case 'F':
+ f = `{{.Peer | names | join ", "}}`
+ name = "peers"
+ case 'g':
+ f = `{{.Labels | join ", "}}`
+ name = "labels"
+ case 'i':
+ f = "{{.MessageId}}"
+ name = "msg-id"
+ case 'n':
+ f = `{{index (.From | names) 0}}`
+ name = "name"
+ case 'r':
+ f = `{{.To | persons | join ", "}}`
+ name = "to"
+ case 'R':
+ f = `{{.Cc | persons | join ", "}}`
+ name = "cc"
+ case 's':
+ f = "{{.Subject}}"
+ name = "subject"
+ case 't':
+ f = "{{(index .To 0).Address}}"
+ name = "to0"
+ case 'T':
+ f = "{{.Account}}"
+ name = "account"
+ case 'u':
+ f = "{{index (.From | mboxes) 0}}"
+ name = "mboxes"
+ case 'v':
+ f = "{{index (.From | names) 0}}"
+ name = "name"
+ case 'Z':
+ f = `{{.Flags | join ""}}`
+ name = "flags"
+ case 'l':
+ f = "{{.Size}}"
+ name = "size"
+ default:
+ f = "%" + string(verb)
+ }
+ if name == "" {
+ name = "wtf"
+ }
+ return
+}
+
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
ui.style = NewStyleSet()
err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 3669d26e..6e20fd71 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -821,17 +821,21 @@ message/rfc822=thunderbird
# TRIGGERS
-Triggers specify commands to execute when certain events occur.
+Triggers specify commands to execute when certain events occur. They are
+configured in the *[triggers]* section of _aerc.conf_.
-They are configured in the *[triggers]* section of _aerc.conf_.
+The commands are not shell commands (i.e. they are not executed with _sh -c_)
+and will be split in multiple arguments following basic shell quoting. They need
+to use one of the commands described in *aerc*(1) without the leading colon *:*
+(e.g. _exec foo bar_ instead of _:exec foo bar_).
*new-email* = _<command>_
- Executed when a new email arrives in the selected folder.
+ Executed when a new email arrives in the selected folder. Example:
- e.g. new-email=exec notify-send "New email from %n" "%s"
+ exec notify-send 'New email from {{.From | names | join ", "}}' '{{.Subject}}'
- Format specifiers from *index-format* are expanded with respect to the new
- message.
+ Templates specifiers from *aerc-templates*(7) are expanded with respect
+ to the new message.
# TEMPLATES
diff --git a/lib/format/format.go b/lib/format/format.go
index ad0b778c..7466cfd5 100644
--- a/lib/format/format.go
+++ b/lib/format/format.go
@@ -93,6 +93,23 @@ func TruncateHead(s string, w int, head string) string {
return head + s[pos:]
}
+func ShellQuote(args []string) string {
+ quoted := make([]string, len(args))
+
+ for i, arg := range args {
+ if strings.ContainsAny(arg, " '\"|&!#$;[](){}<>*\n\t") {
+ if strings.ContainsAny(arg, "!\"$") {
+ arg = "'" + arg + "'"
+ } else {
+ arg = "\"" + arg + "\""
+ }
+ }
+ quoted[i] = arg
+ }
+
+ return strings.Join(quoted, " ")
+}
+
type Ctx struct {
FromAddress string
AccountName string
diff --git a/widgets/account.go b/widgets/account.go
index 3176a2bf..ded71f99 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -293,7 +293,15 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
acct.dirlist.UiConfig(name).ReverseThreadOrder,
acct.dirlist.UiConfig(name).SortThreadSiblings,
func(msg *models.MessageInfo) {
- config.Triggers.ExecNewEmail(acct.acct, msg)
+ if len(config.Triggers.NewEmail) == 0 {
+ return
+ }
+ err := acct.aerc.cmd(
+ config.Triggers.NewEmail,
+ acct.acct, msg)
+ if err != nil {
+ acct.aerc.PushError(err.Error())
+ }
}, func() {
if acct.dirlist.UiConfig(name).NewMessageBell {
acct.host.Beep()
diff --git a/widgets/aerc.go b/widgets/aerc.go
index b946b158..b8be1100 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -20,12 +20,13 @@ import (
"git.sr.ht/~rjarry/aerc/lib/crypto"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
+ "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Aerc struct {
accounts map[string]*AccountView
- cmd func(cmd []string) error
+ cmd func([]string, *config.AccountConfig, *models.MessageInfo) error
cmdHistory lib.History
complete func(cmd string) []string
focused ui.Interactive
@@ -51,7 +52,8 @@ type Choice struct {
}
func NewAerc(
- crypto crypto.Provider, cmd func(cmd []string) error,
+ crypto crypto.Provider,
+ cmd func([]string, *config.AccountConfig, *models.MessageInfo) error,
complete func(cmd string) []string, cmdHistory lib.History,
deferLoop chan struct{},
) *Aerc {
@@ -86,7 +88,6 @@ func NewAerc(
}
statusline.SetAerc(aerc)
- config.Triggers.ExecuteCommand = cmd
for _, acct := range config.Accounts {
view, err := NewAccountView(aerc, acct, aerc, deferLoop)
@@ -608,7 +609,7 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
if err != nil {
aerc.PushError(err.Error())
}
- err = aerc.cmd(parts)
+ err = aerc.cmd(parts, nil, nil)
if err != nil {
aerc.PushError(err.Error())
}
@@ -634,7 +635,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
if text != "" {
cmd = append(cmd, text)
}
- err := aerc.cmd(cmd)
+ err := aerc.cmd(cmd, nil, nil)
if err != nil {
aerc.PushError(err.Error())
}
@@ -661,7 +662,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
if !ok {
return
}
- err := aerc.cmd(cmd)
+ err := aerc.cmd(cmd, nil, nil)
if err != nil {
aerc.PushError(err.Error())
}