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:
@@ -186,13 +186,16 @@ func (cc *compositeContent) CleanupFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw renders the composite content into the given UI context.
|
// Draw renders the composite content into the given UI context.
|
||||||
// scroll is the row offset from the top.
|
// scroll is the row offset from the top. style is the default text style.
|
||||||
func (cc *compositeContent) Draw(ctx *ui.Context, scroll int) {
|
func (cc *compositeContent) Draw(ctx *ui.Context, scroll int, style ...vaxis.Style) {
|
||||||
width := ctx.Width()
|
width := ctx.Width()
|
||||||
height := ctx.Height()
|
height := ctx.Height()
|
||||||
row := -scroll // start above viewport if scrolled
|
row := -scroll // start above viewport if scrolled
|
||||||
|
|
||||||
defaultStyle := vaxis.Style{}
|
defaultStyle := vaxis.Style{}
|
||||||
|
if len(style) > 0 {
|
||||||
|
defaultStyle = style[0]
|
||||||
|
}
|
||||||
|
|
||||||
for _, b := range cc.blocks {
|
for _, b := range cc.blocks {
|
||||||
switch b.blockType {
|
switch b.blockType {
|
||||||
|
|||||||
+73
-11
@@ -632,19 +632,19 @@ func (pv *PartViewer) attemptCopy() {
|
|||||||
}
|
}
|
||||||
// Extract image markers from filter output
|
// Extract image markers from filter output
|
||||||
cleaned, images := parse.ExtractImages(&filterBuf)
|
cleaned, images := parse.ExtractImages(&filterBuf)
|
||||||
|
cleanedBytes, _ := io.ReadAll(cleaned)
|
||||||
if len(images) > 0 {
|
if len(images) > 0 {
|
||||||
pv.imageRefs = images
|
pv.imageRefs = images
|
||||||
log.Debugf("extracted %d image refs from filter output", len(images))
|
pv.composite = newCompositeContent(
|
||||||
}
|
string(cleanedBytes), images, 80)
|
||||||
// Always forward to pager — placeholders are readable text
|
pv.Invalidate()
|
||||||
// Image markers become \x00IMG:N\x00 which we replace
|
} else {
|
||||||
// with human-readable [image: alt] text for the pager.
|
// No images — forward to pager as normal
|
||||||
cleanedBytes, _ := io.ReadAll(cleaned)
|
_, copyErr := io.Copy(pv.pagerin, bytes.NewReader(cleanedBytes))
|
||||||
output := parse.ReplacePlaceholders(cleanedBytes, images)
|
|
||||||
_, copyErr := io.Copy(pv.pagerin, bytes.NewReader(output))
|
|
||||||
if copyErr != nil {
|
if copyErr != nil {
|
||||||
log.Errorf("io.Copy: %s", copyErr)
|
log.Errorf("io.Copy: %s", copyErr)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
err = pv.pagerin.Close()
|
err = pv.pagerin.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error closing pager pipe: %v", err)
|
log.Errorf("error closing pager pipe: %v", err)
|
||||||
@@ -838,9 +838,14 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
|
|||||||
if pv.term != nil {
|
if pv.term != nil {
|
||||||
pv.term.Draw(ctx)
|
pv.term.Draw(ctx)
|
||||||
}
|
}
|
||||||
// NOTE: Composite image rendering (pv.composite) is reserved for
|
// Composite mode: text + inline images from filter output
|
||||||
// future use. Currently all filter output goes through the pager
|
if pv.composite != nil {
|
||||||
// with [image: alt] text placeholders for extracted images.
|
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) {
|
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
|
||||||
@@ -890,12 +895,69 @@ func (pv *PartViewer) resized(ctx *ui.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pv *PartViewer) Event(event vaxis.Event) 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 {
|
if pv.term != nil {
|
||||||
return pv.term.Event(event)
|
return pv.term.Event(event)
|
||||||
}
|
}
|
||||||
return false
|
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 {
|
type HeaderView struct {
|
||||||
Name string
|
Name string
|
||||||
Value string
|
Value string
|
||||||
|
|||||||
Reference in New Issue
Block a user