open: allow overriding default program

Instead of xdg-open (or open on MacOS), allow forcing a program to open
a message part. The program is determined in that order of priority:

1) If :open has arguments, they will be used as command to open the
   attachment. If the arguments contain the {} placeholder, the
   temporary file will be substituted, otherwise the file path is added
   at the end of the arguments.

2) If a command is specified in the [openers] section of aerc.conf for
   the part MIME type, then it is used with the same rules of {}
   substitution.

3) Finally, fallback to xdg-open/open with the file path as argument.

Update the docs and default config accordingly with examples.

Fixes: https://todo.sr.ht/~rjarry/aerc/64
Co-authored-by: Jason Stewart <support@eggplantsd.com>
Signed-off-by: Robin Jarry <robin@jarry.cc>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Moritz Poldrack <moritz@poldrack.dev>
This commit is contained in:
Robin Jarry 2022-09-30 14:12:07 +02:00
parent 92ba132d70
commit 45bff88515
7 changed files with 104 additions and 9 deletions

View file

@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Bracketed paste support.
- Display current directory in `status-line.render-format` with `%p`.
- Change accounts while composing a message with `:switch-account`.
- Override `:open` handler on a per-MIME-type basis in `aerc.conf`.
- Specify opener as the first `:open` param instead of always using default
handler (i.e. `:open gimp` to open attachment in GIMP).
### Changed

View file

@ -48,10 +48,11 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
mv.MessageView().FetchBodyPart(p.Index, func(reader io.Reader) {
extension := ""
mimeType := ""
// try to determine the correct extension based on mimetype
if part, err := mv.MessageView().BodyStructure().PartAtIndex(p.Index); err == nil {
mimeType := fmt.Sprintf("%s/%s", part.MIMEType, part.MIMESubType)
mimeType = fmt.Sprintf("%s/%s", part.MIMEType, part.MIMESubType)
if exts, _ := mime.ExtensionsByType(mimeType); len(exts) > 0 {
extension = exts[0]
}
@ -71,7 +72,8 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
}
go func() {
err = lib.XDGOpen(tmpFile.Name())
openers := aerc.Config().Openers
err = lib.XDGOpenMime(tmpFile.Name(), mimeType, openers, args[1:])
if err != nil {
aerc.PushError("open: " + err.Error())
}

View file

@ -308,6 +308,20 @@ text/plain=sed 's/^>\+.*/\x1b[36m&\x1b[0m/'
#text/html=w3m -dump -I UTF-8 -T text/html
#image/*=catimg -w $(tput cols) -
[openers]
#
# Openers allow you to specify the command to use for the :open action on a
# per-MIME-type basis.
#
# {} is expanded as the temporary filename to be opened. If it is not
# encountered in the command, the temporary filename will be appened to the end
# of the command.
#
# Examples:
# text/html=surf -dfgms
# text/plain=gvim {} +125
# message/rfc822=thunderbird
[triggers]
#
# Triggers specify commands to execute when certain events occur.

View file

@ -17,6 +17,7 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/go-ini/ini"
"github.com/google/shlex"
"github.com/imdario/mergo"
"github.com/kyoh86/xdg"
"github.com/mitchellh/go-homedir"
@ -257,6 +258,7 @@ type AercConfig struct {
ContextualUis []UIConfigContext
General GeneralConfig
Templates TemplateConfig
Openers map[string][]string
}
// Input: TimestampFormat
@ -484,6 +486,16 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
config.Filters = append(config.Filters, filter)
}
}
if openers, err := file.GetSection("openers"); err == nil {
for mimeType, command := range openers.KeysHash() {
mimeType = strings.ToLower(mimeType)
if args, err := shlex.Split(command); err != nil {
return err
} else {
config.Openers[mimeType] = args
}
}
}
if viewer, err := file.GetSection("viewer"); err == nil {
if err := viewer.MapTo(&config.Viewer); err != nil {
return err
@ -807,6 +819,8 @@ func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) {
QuotedReply: "quoted_reply",
Forwards: "forward_as_body",
},
Openers: make(map[string][]string),
}
// These bindings are not configurable
@ -835,6 +849,7 @@ func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) {
logging.Debugf("aerc.conf: [viewer] %#v", config.Viewer)
logging.Debugf("aerc.conf: [compose] %#v", config.Compose)
logging.Debugf("aerc.conf: [filters] %#v", config.Filters)
logging.Debugf("aerc.conf: [openers] %#v", config.Openers)
logging.Debugf("aerc.conf: [triggers] %#v", config.Triggers)
logging.Debugf("aerc.conf: [templates] %#v", config.Templates)

View file

@ -508,6 +508,25 @@ that aerc does not have alone.
Note that said email body is converted into UTF-8 before being passed to
filters.
## OPENERS
Openers allow you to specify the command to use for the *:open* action on a
per-MIME-type basis. They are configured in the *[openers]* section of
aerc.conf.
*{}* is expanded as the temporary filename to be opened. If it is not
encountered in the command, the temporary filename will be appened to the end
of the command. Environment variables are also expanded. Tilde is not expanded.
Example:
```
[openers]
text/html=surf -dfgms
text/plain=gvim {} +125
message/rfc822=thunderbird
```
## TRIGGERS
Triggers specify commands to execute when certain events occur.

View file

@ -392,8 +392,18 @@ message list, the message in the message viewer, etc).
at the bottom of the message viewer.
*open* [args...]
Saves the current message part in a temporary file and opens it
with the system handler. Any given args are forwarded to the open handler
Saves the current message part to a temporary file, then opens it. If no
arguments are provided, it will open the current MIME part with the
matching command in the *[openers]* section of _aerc.conf_. When no match
is found in *[openers]*, it falls back to the default system handler.
When arguments are provided:
- The first argument must be the program to open the message part with.
Subsequent args are passed to that program.
- *{}* will be expanded as the temporary filename to be opened. If it is
not encountered in the arguments, the temporary filename will be
appened to the end of the command.
*save* [-fpa] <path>
Saves the current message part to the given path.

View file

@ -4,16 +4,48 @@ import (
"fmt"
"os/exec"
"runtime"
"strings"
"git.sr.ht/~rjarry/aerc/logging"
)
func XDGOpen(uri string) error {
openBin := "xdg-open"
if runtime.GOOS == "darwin" {
openBin = "open"
return XDGOpenMime(uri, "", nil, nil)
}
args := []string{openBin, uri}
func XDGOpenMime(
uri string, mimeType string,
openers map[string][]string, args []string,
) error {
if len(args) == 0 {
// no explicit command provided, lookup opener from mime type
opener, ok := openers[mimeType]
if ok {
args = opener
} else {
// no opener defined in config, fallback to default
if runtime.GOOS == "darwin" {
args = append(args, "open")
} else {
args = append(args, "xdg-open")
}
}
}
i := 0
for ; i < len(args); i++ {
if strings.Contains(args[i], "{}") {
break
}
}
if i < len(args) {
// found {} placeholder in args, replace with uri
args[i] = strings.Replace(args[i], "{}", uri, 1)
} else {
// no {} placeholder in args, add uri at the end
args = append(args, uri)
}
logging.Infof("running command: %v", args)
cmd := exec.Command(args[0], args[1:]...)
out, err := cmd.CombinedOutput()