feat(msgviewer): re-enable composite renderer with scroll support and inline images

Composite mode now handles j/k/g/G/PgUp/PgDown/mouse wheel for scrolling.
Text uses the configured default style. Images render inline via Vaxis.
Emails without images still use the standard pager path.
This commit is contained in:
Mortdecai
2026-04-07 21:04:27 -04:00
parent 9fd718b2ae
commit 20161fedeb
2 changed files with 80 additions and 15 deletions
+5 -2
View File
@@ -186,13 +186,16 @@ func (cc *compositeContent) CleanupFiles() {
}
// Draw renders the composite content into the given UI context.
// scroll is the row offset from the top.
func (cc *compositeContent) Draw(ctx *ui.Context, scroll int) {
// scroll is the row offset from the top. style is the default text style.
func (cc *compositeContent) Draw(ctx *ui.Context, scroll int, style ...vaxis.Style) {
width := ctx.Width()
height := ctx.Height()
row := -scroll // start above viewport if scrolled
defaultStyle := vaxis.Style{}
if len(style) > 0 {
defaultStyle = style[0]
}
for _, b := range cc.blocks {
switch b.blockType {
+73 -11
View File
@@ -632,19 +632,19 @@ func (pv *PartViewer) attemptCopy() {
}
// Extract image markers from filter output
cleaned, images := parse.ExtractImages(&filterBuf)
cleanedBytes, _ := io.ReadAll(cleaned)
if len(images) > 0 {
pv.imageRefs = images
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))
pv.composite = newCompositeContent(
string(cleanedBytes), images, 80)
pv.Invalidate()
} else {
// No images — forward to pager as normal
_, copyErr := io.Copy(pv.pagerin, bytes.NewReader(cleanedBytes))
if copyErr != nil {
log.Errorf("io.Copy: %s", copyErr)
}
}
err = pv.pagerin.Close()
if err != nil {
log.Errorf("error closing pager pipe: %v", err)
@@ -838,9 +838,14 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
if pv.term != nil {
pv.term.Draw(ctx)
}
// 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.
// Composite mode: text + inline images from filter output
if pv.composite != nil {
pv.composite.width = ctx.Width()
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
pv.composite.Draw(ctx, pv.scroll, style)
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
@@ -890,12 +895,69 @@ func (pv *PartViewer) resized(ctx *ui.Context) bool {
}
func (pv *PartViewer) Event(event vaxis.Event) bool {
if pv.composite != nil {
switch ev := event.(type) {
case vaxis.Mouse:
switch ev.Button {
case vaxis.MouseWheelUp:
pv.scrollComposite(-3)
return true
case vaxis.MouseWheelDown:
pv.scrollComposite(3)
return true
}
case vaxis.Key:
switch {
case ev.Matches('j'), ev.Matches(vaxis.KeyDown):
pv.scrollComposite(1)
return true
case ev.Matches('k'), ev.Matches(vaxis.KeyUp):
pv.scrollComposite(-1)
return true
case ev.Matches(vaxis.KeyPgDown), ev.Matches(' '):
pv.scrollComposite(pv.height / 2)
return true
case ev.Matches(vaxis.KeyPgUp):
pv.scrollComposite(-pv.height / 2)
return true
case ev.Matches('g'), ev.Matches(vaxis.KeyHome):
pv.scroll = 0
pv.Invalidate()
return true
case ev.Matches('G'), ev.Matches(vaxis.KeyEnd):
maxScroll := pv.composite.totalHeight() - pv.height
if maxScroll > 0 {
pv.scroll = maxScroll
}
pv.Invalidate()
return true
}
}
return false
}
if pv.term != nil {
return pv.term.Event(event)
}
return false
}
func (pv *PartViewer) scrollComposite(delta int) {
pv.scroll += delta
if pv.scroll < 0 {
pv.scroll = 0
}
if pv.composite != nil {
maxScroll := pv.composite.totalHeight() - pv.height
if maxScroll < 0 {
maxScroll = 0
}
if pv.scroll > maxScroll {
pv.scroll = maxScroll
}
}
pv.Invalidate()
}
type HeaderView struct {
Name string
Value string