diff --git a/app/msgviewer.go b/app/msgviewer.go index d093b11..d0f771a 100644 --- a/app/msgviewer.go +++ b/app/msgviewer.go @@ -439,7 +439,10 @@ type PartViewer struct { width int height int - links []string + links []string + imageRefs []parse.ImageRef // extracted image references from filter output + composite *compositeContent // composite renderer (non-nil when images present) + scroll int // scroll offset for composite mode } const copying int32 = 1 @@ -611,37 +614,56 @@ func (pv *PartViewer) attemptCopy() { pv.source = parse.StripAnsi(pv.hyperlinks(pv.source)) } 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 + var filterBuf bytes.Buffer + pv.filter.Stdout = &filterBuf 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) - 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() + go func() { + defer log.PanicHandler() + defer atomic.StoreInt32(&pv.copying, 0) + 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) - } - }() + // Extract image markers from filter output + 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) + } + } + err = pv.pagerin.Close() + if err != nil { + log.Errorf("error closing pager pipe: %v", err) + } + }() + } else { + go func() { + defer log.PanicHandler() + defer atomic.StoreInt32(&pv.copying, 0) + _, err := io.Copy(pv.pagerin, pv.source) + if err != nil { + log.Errorf("io.Copy: %s", err) + } + err = pv.pagerin.Close() + if err != nil { + log.Errorf("error closing pager pipe: %v", err) + } + }() + } } func (pv *PartViewer) writeMailHeaders() { @@ -816,6 +838,13 @@ 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 + } 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 @@ -847,6 +876,10 @@ func (pv *PartViewer) Cleanup() { if pv.graphic != nil { pv.graphic.Destroy() } + if pv.composite != nil { + pv.composite.Destroy() + pv.composite.CleanupFiles() + } } func (pv *PartViewer) resized(ctx *ui.Context) bool {