Hey folks, this has been requested multiple times recently. I thought I would tackle it since nobody stepped up :) This is a follow up on https://todo.sr.ht/~rjarry/aerc/250. I changed my mind about the way it is configured in the filters section. The filter pattern was already pretty complex with all the headers and regular expression stuff. Since the pager thing was more related to the command itself, I chose to condition the paging based on an exclamation mark prefix in the filter command itself. For example: text/html = ! w3m -I UTF-8 -T text/html This will effectively run w3m as the main process running in the embedded terminal of the part viewer. It will provide interactive access to w3m and will effectively allow navigating using w3m paging and coloring. Robin Jarry (4): viewer: limit indentation in writeMailHeaders viewer: avoid potential race condition viewer: set term size environment vars before filter command starts viewer: allow filters to provide their own paging app/msgviewer.go | 192 +++++++++++++++++++++++------------------- config/aerc.conf | 1 + config/filters.go | 22 +++-- doc/aerc-config.5.scd | 13 +++ 4 files changed, 135 insertions(+), 93 deletions(-) -- 2.47.0
aerc/patches: SUCCESS in 2m4s [Allow filters to implement their own paging][0] from [Robin Jarry][1] [0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/56048 [1]: mailto:robin@jarry.cc ✓ #1373181 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1373181 ✓ #1373182 SUCCESS aerc/patches/openbsd.yml https://builds.sr.ht/~rjarry/job/1373182
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~rjarry/aerc-devel/patches/56048/mbox | git am -3Learn more about email & git
There is an extraneous level of indentation in writeMailHeaders(). Remove it. Signed-off-by: Robin Jarry <robin@jarry.cc> --- app/msgviewer.go | 104 ++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/app/msgviewer.go b/app/msgviewer.go index 0c2c1bef0f86..804bc6007348 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -635,69 +635,71 @@ func (pv *PartViewer) attemptCopy() { func (pv *PartViewer) writeMailHeaders() { info := pv.msg.MessageInfo() - if config.Viewer.ShowHeaders && info.RFC822Headers != nil { - var file io.WriteCloser + if !config.Viewer.ShowHeaders || info.RFC822Headers == nil { + return + } + var file io.WriteCloser - for _, f := range config.Filters { - if f.Type != config.FILTER_HEADERS { - continue - } - log.Debugf("<%s> piping headers in filter: %s", - info.Envelope.MessageId, f.Command) - filter := exec.Command("sh", "-c", f.Command) - if pv.filter != nil { - // inherit from filter env - filter.Env = pv.filter.Env - } + for _, f := range config.Filters { + if f.Type != config.FILTER_HEADERS { + continue + } + log.Debugf("<%s> piping headers in filter: %s", + info.Envelope.MessageId, f.Command) + filter := exec.Command("sh", "-c", f.Command) + if pv.filter != nil { + // inherit from filter env + filter.Env = pv.filter.Env + } - stdin, err := filter.StdinPipe() + stdin, err := filter.StdinPipe() + if err == nil { + filter.Stdout = pv.pagerin + filter.Stderr = pv.pagerin + err := filter.Start() if err == nil { - filter.Stdout = pv.pagerin - filter.Stderr = pv.pagerin - err := filter.Start() - if err == nil { - //nolint:errcheck // who cares? - defer filter.Wait() - file = stdin - } else { - log.Errorf( - "failed to start header filter: %v", - err) - } + //nolint:errcheck // who cares? + defer filter.Wait() + file = stdin } else { - log.Errorf("failed to create pipe: %v", err) + log.Errorf( + "failed to start header filter: %v", + err) } - break - } - if file == nil { - file = pv.pagerin } else { - defer file.Close() + log.Errorf("failed to create pipe: %v", err) } + break + } - var buf bytes.Buffer - err := textproto.WriteHeader(&buf, info.RFC822Headers.Header.Header) - if err != nil { - log.Errorf("failed to format headers: %v", err) - } - _, err = file.Write(bytes.TrimRight(buf.Bytes(), "\r\n")) - if err != nil { - log.Errorf("failed to write headers: %v", err) - } + if file == nil { + file = pv.pagerin + } else { + defer file.Close() + } - // virtual header - if len(info.Labels) != 0 { - labels := fmtHeader(info, "Labels", "", "", "", "") - _, err := file.Write([]byte(fmt.Sprintf("\r\nLabels: %s", labels))) - if err != nil { - log.Errorf("failed to write to labels: %v", err) - } - } - _, err = file.Write([]byte{'\r', '\n', '\r', '\n'}) + var buf bytes.Buffer + err := textproto.WriteHeader(&buf, info.RFC822Headers.Header.Header) + if err != nil { + log.Errorf("failed to format headers: %v", err) + } + _, err = file.Write(bytes.TrimRight(buf.Bytes(), "\r\n")) + if err != nil { + log.Errorf("failed to write headers: %v", err) + } + + // virtual header + if len(info.Labels) != 0 { + labels := fmtHeader(info, "Labels", "", "", "", "") + _, err := file.Write([]byte(fmt.Sprintf("\r\nLabels: %s", labels))) if err != nil { - log.Errorf("failed to write empty line: %v", err) + log.Errorf("failed to write to labels: %v", err) } } + _, err = file.Write([]byte{'\r', '\n', '\r', '\n'}) + if err != nil { + log.Errorf("failed to write empty line: %v", err) + } } func (pv *PartViewer) hyperlinks(r io.Reader) (reader io.Reader) { -- 2.47.0
Make sure to do a test and set atomically on the copying boolean. Signed-off-by: Robin Jarry <robin@jarry.cc> --- app/msgviewer.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/msgviewer.go b/app/msgviewer.go index 804bc6007348..f4bc5801b0c2 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -601,10 +601,9 @@ func (pv *PartViewer) decodeImage() { func (pv *PartViewer) attemptCopy() { if pv.source == nil || pv.filter == nil || - atomic.LoadInt32(&pv.copying) == copying { + atomic.SwapInt32(&pv.copying, copying) == copying { return } - atomic.StoreInt32(&pv.copying, copying) pv.writeMailHeaders() if strings.EqualFold(pv.part.MIMEType, "text") { pv.source = parse.StripAnsi(pv.hyperlinks(pv.source)) -- 2.47.0
When the part viewer embedded terminal starts we take the opportunity to initialize the COLUMNS and LINES environment variables of the filter command before starting the terminal. Unfortunately, the part viewer Draw() method will start the terminal after fetching the body part which will (when complete) start the filter command in attemptCopy(). There is a race which can lead to the env variables being updated after the filter command has been started. Set the environment variables before triggering the part fetch. This will guarantee that they are present when the filter command starts. Signed-off-by: Robin Jarry <robin@jarry.cc> --- app/msgviewer.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/msgviewer.go b/app/msgviewer.go index f4bc5801b0c2..2785d03cb7a4 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -560,16 +560,6 @@ func NewPartViewer( uiConfig: acct.UiConfig(), } - if term != nil { - term.OnStart = func() { - if term.ctx != nil { - filter.Env = append(filter.Env, fmt.Sprintf("COLUMNS=%d", term.ctx.Window().Width)) - filter.Env = append(filter.Env, fmt.Sprintf("LINES=%d", term.ctx.Window().Height)) - } - pv.attemptCopy() - } - } - return pv, nil } @@ -782,6 +772,10 @@ func (pv *PartViewer) Draw(ctx *ui.Context) { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) pv.noFilter.Draw(ctx) return + case !pv.fetched: + w, h := ctx.Window().Size() + pv.filter.Env = append(pv.filter.Env, fmt.Sprintf("COLUMNS=%d", w)) + pv.filter.Env = append(pv.filter.Env, fmt.Sprintf("LINES=%d", h)) } if !pv.fetched { pv.msg.FetchBodyPart(pv.index, pv.SetSource) -- 2.47.0
Allow filter commands to declare that they will provide their own paging functionality. To do so, a filter command must start with an exclamation mark "!". For example: text/html = ! w3m -I UTF-8 -T text/html This will effectively run w3m as the main process running in the embedded terminal of the part viewer. It will provide interactive access to w3m and will effectively allow navigating using w3m paging and coloring. Implements: https://todo.sr.ht/~rjarry/aerc/250 Signed-off-by: Robin Jarry <robin@jarry.cc> --- app/msgviewer.go | 71 +++++++++++++++++++++++++++++-------------- config/aerc.conf | 1 + config/filters.go | 22 +++++++++----- doc/aerc-config.5.scd | 13 ++++++++ 4 files changed, 77 insertions(+), 30 deletions(-) diff --git a/app/msgviewer.go b/app/msgviewer.go index 2785d03cb7a4..22cc1f0ae06a 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -452,14 +452,6 @@ func NewPartViewer( pagerin io.WriteCloser term *Terminal ) - pagerCmd, err := CmdFallbackSearch(config.PagerCmds(), false) - if err != nil { - acct.PushError(fmt.Errorf("could not start pager: %w", err)) - return nil, err - } - cmd := opt.SplitArgs(pagerCmd) - pager = exec.Command(cmd[0], cmd[1:]...) - info := msg.MessageInfo() mime := part.FullMIMEType() @@ -492,9 +484,21 @@ func NewPartViewer( log.Tracef("command %v", f.Command) } } - if filter != nil { + if filter == nil { + continue + } + if !f.NeedsPager { + pager = filter break } + pagerCmd, err := CmdFallbackSearch(config.PagerCmds(), false) + if err != nil { + acct.PushError(fmt.Errorf("could not start pager: %w", err)) + return nil, err + } + cmd := opt.SplitArgs(pagerCmd) + pager = exec.Command(cmd[0], cmd[1:]...) + break } var noFilter *ui.Grid if filter != nil { @@ -524,8 +528,14 @@ func NewPartViewer( if config.General.EnableOSC8 { filter.Env = append(filter.Env, "AERC_OSC8_URLS=1") } - log.Debugf("<%s> part=%v %s: %v | %v", - info.Envelope.MessageId, curindex, mime, filter, pager) + if pager == filter { + log.Debugf("<%s> part=%v %s: %v", + info.Envelope.MessageId, curindex, mime, filter) + } else { + log.Debugf("<%s> part=%v %s: %v | %v", + info.Envelope.MessageId, curindex, mime, filter, pager) + } + var err error if pagerin, err = pager.StdinPipe(); err != nil { return nil, err } @@ -598,26 +608,36 @@ func (pv *PartViewer) attemptCopy() { if strings.EqualFold(pv.part.MIMEType, "text") { pv.source = parse.StripAnsi(pv.hyperlinks(pv.source)) } - pv.filter.Stdin = pv.source - pv.filter.Stdout = pv.pagerin - pv.filter.Stderr = pv.pagerin - err := pv.filter.Start() - if err != nil { - log.Errorf("error running filter: %v", err) - return + if pv.filter != pv.pager { + // Filter is a separate process that needs to output to the pager. + pv.filter.Stdin = pv.source + pv.filter.Stdout = pv.pagerin + pv.filter.Stderr = pv.pagerin + err := pv.filter.Start() + if err != nil { + log.Errorf("error running filter: %v", err) + return + } } go func() { defer log.PanicHandler() defer atomic.StoreInt32(&pv.copying, 0) - err = pv.filter.Wait() - if err != nil { - log.Errorf("error waiting for filter: %v", err) - return + var err error + if pv.filter == pv.pager { + // Filter already implements its own paging. + _, err = io.Copy(pv.pagerin, pv.source) + if err != nil { + log.Errorf("io.Copy: %s", err) + } + } else { + err = pv.filter.Wait() + if err != nil { + log.Errorf("filter.Wait: %v", err) + } } err = pv.pagerin.Close() if err != nil { log.Errorf("error closing pager pipe: %v", err) - return } }() } @@ -627,6 +647,11 @@ func (pv *PartViewer) writeMailHeaders() { if !config.Viewer.ShowHeaders || info.RFC822Headers == nil { return } + if pv.filter == pv.pager { + // Filter already implements its own paging. + // Piping another filter into it will cause mayhem. + return + } var file io.WriteCloser for _, f := range config.Filters { diff --git a/config/aerc.conf b/config/aerc.conf index 9070bcf3f4a8..3a6cf25159fc 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -768,6 +768,7 @@ message/delivery-status=colorize message/rfc822=colorize #text/html=pandoc -f html -t plain | colorize #text/html=html | colorize +#text/html=! w3m -T text/html -I UTF-8 #text/*=bat -fP --file-name="$AERC_FILENAME" #application/x-sh=bat -fP -l sh #image/*=catimg -w $(tput cols) - diff --git a/config/filters.go b/config/filters.go index e843c5faa1d4..60372664e8fc 100644 --- a/config/filters.go +++ b/config/filters.go @@ -18,11 +18,12 @@ const ( ) type FilterConfig struct { - Type FilterType - Filter string - Command string - Header string - Regex *regexp.Regexp + Type FilterType + Filter string + Command string + NeedsPager bool + Header string + Regex *regexp.Regexp } var Filters []*FilterConfig @@ -34,9 +35,16 @@ func parseFilters(file *ini.File) error { } for _, key := range filters.Keys() { + pager := true + cmd := key.Value() + if strings.HasPrefix(cmd, "!") { + cmd = strings.TrimLeft(cmd, "! \t") + pager = false + } filter := FilterConfig{ - Command: key.Value(), - Filter: key.Name(), + Command: cmd, + NeedsPager: pager, + Filter: key.Name(), } switch { diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 56c4e2099fa0..1d49f11a3a47 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -1011,6 +1011,13 @@ filters need to be able to read from standard input. Many programs support reading from stdin by putting _-_ instead of a path to a file. You can also chain together multiple filters by piping with _|_. +Some filter commands may require interactive user input. If a filter command +starts with an exclamation mark _!_, the configured *pager* will *not* be used. +Instead, the filter command will be executed as the main process in the embedded +terminal of the part viewer. The filter command standard input, output and error +will be set to the terminal TTY. The filter is expected to implement its own +paging. + aerc ships with some default filters installed in the libexec directory (usually _/usr/libexec/aerc/filters_). Note that these may have additional dependencies that aerc does not have alone. @@ -1100,6 +1107,12 @@ _text/html_ text/html=pandoc -f html -t plain ``` + Use w3m internal pager to interactively view an HTML part with coloring: + + ``` + text/html=! w3m -I UTF-8 -T text/html + ``` + _text/calendar_ Parse calendar invites: -- 2.47.0
builds.sr.ht <builds@sr.ht>aerc/patches: SUCCESS in 2m4s [Allow filters to implement their own paging][0] from [Robin Jarry][1] [0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/56048 [1]: mailto:robin@jarry.cc ✓ #1373181 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1373181 ✓ #1373182 SUCCESS aerc/patches/openbsd.yml https://builds.sr.ht/~rjarry/job/1373182