diff --git a/commands/account/view-message.go b/commands/account/view-message.go index bef2740..edafd09 100644 --- a/commands/account/view-message.go +++ b/commands/account/view-message.go @@ -19,7 +19,7 @@ func ViewMessage(aerc *widgets.Aerc, args []string) error { acct := aerc.SelectedAccount() store := acct.Messages().Store() msg := acct.Messages().Selected() - viewer := widgets.NewMessageViewer(store, msg) + viewer := widgets.NewMessageViewer(aerc.Config(), store, msg) aerc.NewTab(viewer, runewidth.Truncate( msg.Envelope.Subject, 32, "…")) return nil diff --git a/config/aerc.conf b/config/aerc.conf index 5a4317f..3a89151 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -54,32 +54,12 @@ empty-message=(no messages) [viewer] # -# We can use different programs to display various kinds of email attachments. -# These programs will have the mail piped into them and are expected to output -# it ready to display on a terminal (you can include terminal control -# characters if you like, for colors and such). Emails will be stripped of -# non-printable characters before being piped into these commands, and will be -# encoded with UTF-8. These commands are invoked with sh and run -# non-interactively, and their output is piped into your pager command -# (interactively). The following environment variables will be set: +# Specifies the pager to use when displaying emails. Note that some filters +# may add ANSI codes to add color to rendered emails, so you may want to use a +# pager which supports ANSI codes. # -# $WIDTH: the width of the terminal window -# $HEIGHT: the height of the terminal window -# $MIMETYPE: the email's mimetype -# -# You can use * as a wildcard for any subtype of a given mimetype. When -# displaying a text/* message and no command matches, the message will just be -# piped directly into your pager (after being stripped of non-printable -# characters). - -# Examples: -# -#text/html=w3m -T text/html -cols $WIDTH -dump -o display_image=false -o display_link_number=true -text/*=fold -sw $WIDTH - -# -# Default: less -r -pager=less -r +# Default: less -R +pager=less -R # # If an email offers several versions (multipart), you can configure which @@ -89,6 +69,21 @@ pager=less -r # Default: text/plain,text/html alternatives=text/plain,text/html +[filters] +# +# Filters allow you to pipe an email body through a shell command to render +# certain emails differently, e.g. highlighting them with ANSI escape codes. +# +# The first filter which matches the email's mimetype will be used, so order +# them from most to least specific. +# +# You can also match on non-mimetypes, by prefixing with the header to match +# against (non-case-sensitive) and a colon, e.g. subject:text will match a +# subject which contains "text". Use header~:regex to match against a regex. +subject~:PATCH=contrib/hldiff.py +text/html=w3m -T text/html -cols $(tput cols) -dump -o display_image=false -o display_link_number=true +text/*=contrib/plaintext.py + [lbinds] # # Binds are of the form = diff --git a/config/config.go b/config/config.go index e5e332e..8d460ca 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,12 @@ type UIConfig struct { EmptyMessage string `ini:"empty-message"` } +const ( + FILTER_MIMETYPE = iota + FILTER_HEADER + FILTER_HEADER_REGEX +) + type AccountConfig struct { Default string Name string @@ -38,10 +44,23 @@ type BindingConfig struct { Terminal *KeyBindings } +type FilterConfig struct { + FilterType int + Filter string + Command string +} + +type ViewerConfig struct { + Pager string + Alternatives []string +} + type AercConfig struct { Bindings BindingConfig Ini *ini.File `ini:"-"` Accounts []AccountConfig `ini:"-"` + Filters []FilterConfig `ini:"-"` + Viewer ViewerConfig `ini:"-"` Ui UIConfig } @@ -135,6 +154,34 @@ func LoadConfig(root *string) (*AercConfig, error) { EmptyMessage: "(no messages)", }, } + if filters, err := file.GetSection("filters"); err == nil { + // TODO: Parse the filter more finely, e.g. parse the regex + for match, cmd := range filters.KeysHash() { + filter := FilterConfig{ + Command: cmd, + Filter: match, + } + if strings.Contains(match, "~:") { + filter.FilterType = FILTER_HEADER_REGEX + } else if strings.ContainsRune(match, ':') { + filter.FilterType = FILTER_HEADER + } else { + filter.FilterType = FILTER_MIMETYPE + } + config.Filters = append(config.Filters, filter) + } + } + if viewer, err := file.GetSection("viewer"); err == nil { + if err := viewer.MapTo(&config.Viewer); err != nil { + return nil, err + } + for key, val := range viewer.KeysHash() { + switch key { + case "alternatives": + config.Viewer.Alternatives = strings.Split(val, ",") + } + } + } if ui, err := file.GetSection("ui"); err == nil { if err := ui.MapTo(&config.Ui); err != nil { return nil, err diff --git a/go.mod b/go.mod index 0492447..1a29797 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module git.sr.ht/~sircmpwn/aerc2 require ( git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9 + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 github.com/emersion/go-imap v1.0.0-beta.1 github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b + github.com/emersion/go-message v0.9.2 github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect + github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe // indirect github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 github.com/gdamore/tcell v1.0.0 github.com/go-ini/ini v1.42.0 diff --git a/go.sum b/go.sum index 7d2cf96..9eab1ee 100644 --- a/go.sum +++ b/go.sum @@ -22,14 +22,20 @@ git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a h1:ktjo0NVokh git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190322002230-17c9f17a421a/go.mod h1:hT88+cTemwwESbMptwC7O33qrJfQX0SgRWbXlndUS2c= git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9 h1:WWPN5lf6KzXp3xWRrPQZ4MLR3yrFEI4Ysz7HSQ1G/yo= git.sr.ht/~sircmpwn/pty v0.0.0-20190330154901-3a43678975a9/go.mod h1:8Jmcax8M9nYoEwBhVBhv2ixLRCoUqlbQPE95VpPu43I= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-imap v1.0.0-beta.1 h1:bTCaVlUnb5mKoW9lEukusxguSYYZPer+q0g5t+vw5X0= github.com/emersion/go-imap v1.0.0-beta.1/go.mod h1:oydmHwiyv92ZOiNfQY9BDax5heePWN8P2+W1B2T6qjc= github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b h1:q4qkNe/W10qFGD3RWd4meQTkD0+Zrz0L4ekMvlptg60= github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= +github.com/emersion/go-message v0.9.2 h1:rJmtGZO1Z71PJDQXbC31EwzlJCsA/8kya6GnebSGp6I= +github.com/emersion/go-message v0.9.2/go.mod h1:m3cK90skCWxm5sIMs1sXxly4Tn9Plvcf6eayHZJ1NzM= github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 h1:rDJPbyliyym8ZL/Wt71kdolp6yaD4fLIQz638E6JEt0= github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI= github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ= github.com/gdamore/tcell v1.0.0 h1:oaly4AkxvDT5ffKHV/n4L8iy6FxG2QkAVl0M6cjryuE= diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index ab42ee0..3a3e962 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -6,24 +6,30 @@ import ( "io" "os/exec" + "github.com/danwakefield/fnmatch" "github.com/emersion/go-imap" "github.com/emersion/go-message" "github.com/emersion/go-message/mail" "github.com/gdamore/tcell" + "github.com/google/shlex" "github.com/mattn/go-runewidth" + "git.sr.ht/~sircmpwn/aerc2/config" "git.sr.ht/~sircmpwn/aerc2/lib" "git.sr.ht/~sircmpwn/aerc2/lib/ui" "git.sr.ht/~sircmpwn/aerc2/worker/types" ) type MessageViewer struct { - cmd *exec.Cmd - msg *types.MessageInfo - source io.Reader - sink io.WriteCloser - grid *ui.Grid - term *Terminal + conf *config.AercConfig + filter *exec.Cmd + msg *types.MessageInfo + pager *exec.Cmd + source io.Reader + pagerin io.WriteCloser + sink io.WriteCloser + grid *ui.Grid + term *Terminal } func formatAddresses(addrs []*imap.Address) string { @@ -43,7 +49,7 @@ func formatAddresses(addrs []*imap.Address) string { return val.String() } -func NewMessageViewer(store *lib.MessageStore, +func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer { grid := ui.NewGrid().Rows([]ui.GridSpec{ @@ -86,9 +92,40 @@ func NewMessageViewer(store *lib.MessageStore, {ui.SIZE_EXACT, 20}, }) - cmd := exec.Command("less") - pipe, _ := cmd.StdinPipe() - term, _ := NewTerminal(cmd) + var ( + filter *exec.Cmd + pager *exec.Cmd + pipe io.WriteCloser + pagerin io.WriteCloser + ) + cmd, err := shlex.Split(conf.Viewer.Pager) + if err != nil { + panic(err) // TODO: something useful + } + pager = exec.Command(cmd[0], cmd[1:]...) + + for _, f := range conf.Filters { + cmd, err := shlex.Split(f.Command) + if err != nil { + panic(err) // TODO: Something useful + } + mime := msg.BodyStructure.MIMEType + "/" + msg.BodyStructure.MIMESubType + switch f.FilterType { + case config.FILTER_MIMETYPE: + if fnmatch.Match(f.Filter, mime, 0) { + filter = exec.Command(cmd[0], cmd[1:]...) + fmt.Printf("Using filter for %s: %s\n", mime, f.Command) + } + } + } + if filter != nil { + pipe, _ = filter.StdinPipe() + pagerin, _ = pager.StdinPipe() + } else { + pipe, _ = pager.StdinPipe() + } + + term, _ := NewTerminal(pager) // TODO: configure multipart view. I left a spot for it in the grid body.AddChild(term).At(0, 0).Span(1, 2) @@ -96,11 +133,13 @@ func NewMessageViewer(store *lib.MessageStore, grid.AddChild(body).At(1, 0) viewer := &MessageViewer{ - cmd: cmd, - grid: grid, - msg: msg, - sink: pipe, - term: term, + filter: filter, + grid: grid, + msg: msg, + pager: pager, + pagerin: pagerin, + sink: pipe, + term: term, } store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) { @@ -116,12 +155,22 @@ func NewMessageViewer(store *lib.MessageStore, } func (mv *MessageViewer) attemptCopy() { - if mv.source != nil && mv.cmd.Process != nil { + if mv.source != nil && mv.pager.Process != nil { header := make(message.Header) header.Set("Content-Transfer-Encoding", mv.msg.BodyStructure.Encoding) header.SetContentType( mv.msg.BodyStructure.MIMEType, mv.msg.BodyStructure.Params) header.SetContentDescription(mv.msg.BodyStructure.Description) + if mv.filter != nil { + stdout, _ := mv.filter.StdoutPipe() + mv.filter.Start() + go func() { + _, err := io.Copy(mv.pagerin, stdout) + if err != nil { + io.WriteString(mv.sink, err.Error()) + } + }() + } go func() { entity, err := message.New(header, mv.source) if err != nil {