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:
parent
92ba132d70
commit
45bff88515
7 changed files with 104 additions and 9 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
40
lib/open.go
40
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()
|
||||
|
|
Loading…
Reference in a new issue