diff options
author | Robin Jarry <robin@jarry.cc> | 2023-02-01 23:35:42 +0100 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-02-20 14:48:42 +0100 |
commit | aaaa0c184fb43879cc84551983309cad06d665ee (patch) | |
tree | 93bb83d32495f679a70dbe5d3913549c29edf675 | |
parent | 420a82a356d53e4b600ba54768f7ed21a43cf85e (diff) | |
download | aerc-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.md | 2 | ||||
-rw-r--r-- | aerc.go | 15 | ||||
-rw-r--r-- | commands/commands.go | 21 | ||||
-rw-r--r-- | config/triggers.go | 94 | ||||
-rw-r--r-- | config/ui.go | 134 | ||||
-rw-r--r-- | doc/aerc-config.5.scd | 16 | ||||
-rw-r--r-- | lib/format/format.go | 17 | ||||
-rw-r--r-- | widgets/account.go | 10 | ||||
-rw-r--r-- | widgets/aerc.go | 13 |
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 @@ -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()) } |