From 9fd718b2ae8f13a4aff2c429de5a5bded0d67bac Mon Sep 17 00:00:00 2001 From: Mortdecai Date: Tue, 7 Apr 2026 20:58:45 -0400 Subject: [PATCH] fix(msgviewer): use pager for all text, replace image markers with readable placeholders The composite renderer bypassed the pager (less) for any email with images, losing colors, scrolling, and search. Now always forward to the pager with [image: alt] text placeholders. Image refs are still extracted and stored for future inline rendering. --- app/msgviewer.go | 30 +++++++++++++----------------- lib/parse/extract_images.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/msgviewer.go b/app/msgviewer.go index d0f771a..e62aae5 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -634,16 +634,16 @@ func (pv *PartViewer) attemptCopy() { cleaned, images := parse.ExtractImages(&filterBuf) if len(images) > 0 { pv.imageRefs = images - cleanedBytes, _ := io.ReadAll(cleaned) - pv.composite = newCompositeContent( - string(cleanedBytes), images, 80) - pv.Invalidate() - } else { - // No images — forward to pager as normal - _, copyErr := io.Copy(pv.pagerin, cleaned) - if copyErr != nil { - log.Errorf("io.Copy: %s", copyErr) - } + log.Debugf("extracted %d image refs from filter output", len(images)) + } + // Always forward to pager — placeholders are readable text + // Image markers become \x00IMG:N\x00 which we replace + // with human-readable [image: alt] text for the pager. + cleanedBytes, _ := io.ReadAll(cleaned) + output := parse.ReplacePlaceholders(cleanedBytes, images) + _, copyErr := io.Copy(pv.pagerin, bytes.NewReader(output)) + if copyErr != nil { + log.Errorf("io.Copy: %s", copyErr) } err = pv.pagerin.Close() if err != nil { @@ -838,13 +838,9 @@ func (pv *PartViewer) Draw(ctx *ui.Context) { if pv.term != nil { pv.term.Draw(ctx) } - // Composite mode: text + images from filter output - if pv.composite != nil { - pv.composite.width = ctx.Width() - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', pv.uiConfig.GetStyle(config.STYLE_DEFAULT)) - pv.composite.Draw(ctx, pv.scroll) - return - } + // NOTE: Composite image rendering (pv.composite) is reserved for + // future use. Currently all filter output goes through the pager + // with [image: alt] text placeholders for extracted images. if pv.image != nil && (pv.resized(ctx) || pv.graphic == nil) { // This path should only occur on resizes or the first pass // after the image is downloaded and could be slow due to diff --git a/lib/parse/extract_images.go b/lib/parse/extract_images.go index 9db1774..74b147b 100644 --- a/lib/parse/extract_images.go +++ b/lib/parse/extract_images.go @@ -39,3 +39,33 @@ func ExtractImages(r io.Reader) (io.Reader, []ImageRef) { } return buf, images } + +// ReplacePlaceholders converts \x00IMG:N\x00 placeholders into human-readable +// [image: alt] text suitable for display in a pager. +func ReplacePlaceholders(data []byte, images []ImageRef) []byte { + result := bytes.NewBuffer(nil) + for _, line := range bytes.Split(data, []byte("\n")) { + trimmed := bytes.TrimSpace(line) + if len(trimmed) > 6 && trimmed[0] == 0 && + bytes.HasPrefix(trimmed, []byte("\x00IMG:")) && + trimmed[len(trimmed)-1] == 0 { + // Extract index + numStr := string(trimmed[5 : len(trimmed)-1]) + idx := 0 + for _, c := range numStr { + if c >= '0' && c <= '9' { + idx = idx*10 + int(c-'0') + } + } + if idx < len(images) && images[idx].Alt != "" { + fmt.Fprintf(result, "[image: %s]\n", images[idx].Alt) + } else { + result.WriteString("[image]\n") + } + } else { + result.Write(line) + result.WriteByte('\n') + } + } + return result.Bytes() +}