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
|
width int
|
||||||
height 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
|
const copying int32 = 1
|
||||||
@@ -611,37 +614,56 @@ func (pv *PartViewer) attemptCopy() {
|
|||||||
pv.source = parse.StripAnsi(pv.hyperlinks(pv.source))
|
pv.source = parse.StripAnsi(pv.hyperlinks(pv.source))
|
||||||
}
|
}
|
||||||
if pv.filter != pv.pager {
|
if pv.filter != pv.pager {
|
||||||
// Filter is a separate process that needs to output to the pager.
|
|
||||||
pv.filter.Stdin = pv.source
|
pv.filter.Stdin = pv.source
|
||||||
pv.filter.Stdout = pv.pagerin
|
var filterBuf bytes.Buffer
|
||||||
|
pv.filter.Stdout = &filterBuf
|
||||||
pv.filter.Stderr = pv.pagerin
|
pv.filter.Stderr = pv.pagerin
|
||||||
err := pv.filter.Start()
|
err := pv.filter.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error running filter: %v", err)
|
log.Errorf("error running filter: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
go func() {
|
||||||
go func() {
|
defer log.PanicHandler()
|
||||||
defer log.PanicHandler()
|
defer atomic.StoreInt32(&pv.copying, 0)
|
||||||
defer atomic.StoreInt32(&pv.copying, 0)
|
err := pv.filter.Wait()
|
||||||
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 {
|
if err != nil {
|
||||||
log.Errorf("filter.Wait: %v", err)
|
log.Errorf("filter.Wait: %v", err)
|
||||||
}
|
}
|
||||||
}
|
// Extract image markers from filter output
|
||||||
err = pv.pagerin.Close()
|
cleaned, images := parse.ExtractImages(&filterBuf)
|
||||||
if err != nil {
|
if len(images) > 0 {
|
||||||
log.Errorf("error closing pager pipe: %v", err)
|
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() {
|
func (pv *PartViewer) writeMailHeaders() {
|
||||||
@@ -816,6 +838,13 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
|
|||||||
if pv.term != nil {
|
if pv.term != nil {
|
||||||
pv.term.Draw(ctx)
|
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) {
|
if pv.image != nil && (pv.resized(ctx) || pv.graphic == nil) {
|
||||||
// This path should only occur on resizes or the first pass
|
// This path should only occur on resizes or the first pass
|
||||||
// after the image is downloaded and could be slow due to
|
// after the image is downloaded and could be slow due to
|
||||||
@@ -847,6 +876,10 @@ func (pv *PartViewer) Cleanup() {
|
|||||||
if pv.graphic != nil {
|
if pv.graphic != nil {
|
||||||
pv.graphic.Destroy()
|
pv.graphic.Destroy()
|
||||||
}
|
}
|
||||||
|
if pv.composite != nil {
|
||||||
|
pv.composite.Destroy()
|
||||||
|
pv.composite.CleanupFiles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pv *PartViewer) resized(ctx *ui.Context) bool {
|
func (pv *PartViewer) resized(ctx *ui.Context) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user