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.
|
- Bracketed paste support.
|
||||||
- Display current directory in `status-line.render-format` with `%p`.
|
- Display current directory in `status-line.render-format` with `%p`.
|
||||||
- Change accounts while composing a message with `:switch-account`.
|
- 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
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,11 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
|
|
||||||
mv.MessageView().FetchBodyPart(p.Index, func(reader io.Reader) {
|
mv.MessageView().FetchBodyPart(p.Index, func(reader io.Reader) {
|
||||||
extension := ""
|
extension := ""
|
||||||
|
mimeType := ""
|
||||||
|
|
||||||
// try to determine the correct extension based on mimetype
|
// try to determine the correct extension based on mimetype
|
||||||
if part, err := mv.MessageView().BodyStructure().PartAtIndex(p.Index); err == nil {
|
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 {
|
if exts, _ := mime.ExtensionsByType(mimeType); len(exts) > 0 {
|
||||||
extension = exts[0]
|
extension = exts[0]
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,8 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = lib.XDGOpen(tmpFile.Name())
|
openers := aerc.Config().Openers
|
||||||
|
err = lib.XDGOpenMime(tmpFile.Name(), mimeType, openers, args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerc.PushError("open: " + err.Error())
|
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
|
#text/html=w3m -dump -I UTF-8 -T text/html
|
||||||
#image/*=catimg -w $(tput cols) -
|
#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]
|
||||||
#
|
#
|
||||||
# Triggers specify commands to execute when certain events occur.
|
# Triggers specify commands to execute when certain events occur.
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/go-ini/ini"
|
"github.com/go-ini/ini"
|
||||||
|
"github.com/google/shlex"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
"github.com/kyoh86/xdg"
|
"github.com/kyoh86/xdg"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
@ -257,6 +258,7 @@ type AercConfig struct {
|
||||||
ContextualUis []UIConfigContext
|
ContextualUis []UIConfigContext
|
||||||
General GeneralConfig
|
General GeneralConfig
|
||||||
Templates TemplateConfig
|
Templates TemplateConfig
|
||||||
|
Openers map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input: TimestampFormat
|
// Input: TimestampFormat
|
||||||
|
@ -484,6 +486,16 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
config.Filters = append(config.Filters, filter)
|
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 viewer, err := file.GetSection("viewer"); err == nil {
|
||||||
if err := viewer.MapTo(&config.Viewer); err != nil {
|
if err := viewer.MapTo(&config.Viewer); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -807,6 +819,8 @@ func LoadConfigFromFile(root *string, accts []string) (*AercConfig, error) {
|
||||||
QuotedReply: "quoted_reply",
|
QuotedReply: "quoted_reply",
|
||||||
Forwards: "forward_as_body",
|
Forwards: "forward_as_body",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Openers: make(map[string][]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
// These bindings are not configurable
|
// 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: [viewer] %#v", config.Viewer)
|
||||||
logging.Debugf("aerc.conf: [compose] %#v", config.Compose)
|
logging.Debugf("aerc.conf: [compose] %#v", config.Compose)
|
||||||
logging.Debugf("aerc.conf: [filters] %#v", config.Filters)
|
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: [triggers] %#v", config.Triggers)
|
||||||
logging.Debugf("aerc.conf: [templates] %#v", config.Templates)
|
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
|
Note that said email body is converted into UTF-8 before being passed to
|
||||||
filters.
|
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
|
||||||
|
|
||||||
Triggers specify commands to execute when certain events occur.
|
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.
|
at the bottom of the message viewer.
|
||||||
|
|
||||||
*open* [args...]
|
*open* [args...]
|
||||||
Saves the current message part in a temporary file and opens it
|
Saves the current message part to a temporary file, then opens it. If no
|
||||||
with the system handler. Any given args are forwarded to the open handler
|
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>
|
*save* [-fpa] <path>
|
||||||
Saves the current message part to the given path.
|
Saves the current message part to the given path.
|
||||||
|
|
38
lib/open.go
38
lib/open.go
|
@ -4,16 +4,48 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/logging"
|
"git.sr.ht/~rjarry/aerc/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func XDGOpen(uri string) error {
|
func XDGOpen(uri string) error {
|
||||||
openBin := "xdg-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" {
|
if runtime.GOOS == "darwin" {
|
||||||
openBin = "open"
|
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)
|
logging.Infof("running command: %v", args)
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
Loading…
Reference in a new issue