feat(msgviewer): wire image extraction and composite rendering into PartViewer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+55
-22
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user