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.
This commit is contained in:
Mortdecai
2026-04-07 20:58:45 -04:00
parent abf8feb229
commit 9fd718b2ae
2 changed files with 43 additions and 17 deletions
+13 -17
View File
@@ -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
+30
View File
@@ -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()
}