diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e3eb2..7fb0cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/commands/msgview/open.go b/commands/msgview/open.go index 82c1acc..bb22026 100644 --- a/commands/msgview/open.go +++ b/commands/msgview/open.go @@ -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()) } diff --git a/config/aerc.conf b/config/aerc.conf index 6e5efe7..0bd96fb 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -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. diff --git a/config/config.go b/config/config.go index 24a1b50..e31d1a1 100644 --- a/config/config.go +++ b/config/config.go @@ -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) diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index d6b56fa..8b7aa55 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -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. diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 38862a5..355a08d 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -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] Saves the current message part to the given path. diff --git a/lib/open.go b/lib/open.go index a189980..e091d91 100644 --- a/lib/open.go +++ b/lib/open.go @@ -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) +} + +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") + } + } } - args := []string{openBin, uri} + + 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()