@@ -21,12 +21,13 @@ const (
2121type PreviewPane struct {
2222 childColumn * Column
2323 content string
24- imageContent string // inline image sequences — excluded from yank
24+ imagePaths [] string // source image file paths for lazy rendering
2525 contentLines []string // split content for scrolling
2626 highlighted string
2727 mode PreviewMode
2828 loading bool
2929 scrollOffset int
30+
3031}
3132
3233// navEntry stores one level of the navigation stack for drill-in / go-back.
@@ -114,7 +115,7 @@ func (m MillerView) Update(msg tea.Msg) (MillerView, tea.Cmd) {
114115 Trace ("miller: PreviewReady key=%q highlight=%q len=%d" , msg .NodeKey , msg .HighlightType , len (msg .Content ))
115116 m .preview .loading = false
116117 m .preview .content = msg .Content
117- m .preview .imageContent = msg .ImageContent
118+ m .preview .imagePaths = msg .ImagePaths
118119 m .preview .contentLines = strings .Split (msg .Content , "\n " )
119120 m .preview .highlighted = msg .HighlightType
120121 m .preview .childColumn = nil
@@ -183,7 +184,7 @@ func (m MillerView) handleCursorChanged(msg CursorChangedMsg) (MillerView, tea.C
183184 col .SetItems (treeNodesToItems (node .Children ))
184185 m .preview .childColumn = & col
185186 m .preview .content = ""
186- m .preview .imageContent = ""
187+ m .preview .imagePaths = nil
187188 m .preview .contentLines = nil
188189 m .preview .loading = false
189190 m .preview .scrollOffset = 0
@@ -199,7 +200,7 @@ func (m MillerView) handleCursorChanged(msg CursorChangedMsg) (MillerView, tea.C
199200 return m , cmd
200201 }
201202 m .preview .content = ""
202- m .preview .imageContent = ""
203+ m .preview .imagePaths = nil
203204 m .preview .contentLines = nil
204205 m .preview .loading = false
205206 return m , nil
@@ -393,8 +394,14 @@ func (m MillerView) renderPreview(previewWidth int) string {
393394 if m .preview .mode == PreviewNDSL {
394395 modeLabel = "NDSL"
395396 }
397+ if len (m .preview .imagePaths ) > 0 {
398+ modeLabel += " 🖼 click path to view"
399+ }
396400
397401 contentHeight := m .height - 1 // reserve 1 line for header
402+ if contentHeight < 1 {
403+ contentHeight = 1
404+ }
398405 srcLines := m .preview .contentLines
399406 totalSrc := len (srcLines )
400407
@@ -461,14 +468,10 @@ func (m MillerView) renderPreview(previewWidth int) string {
461468 out .WriteByte ('\n' )
462469 }
463470
464- rendered := lipgloss .NewStyle ().
471+ return lipgloss .NewStyle ().
465472 Width (previewWidth ).
466473 MaxHeight (m .height ).
467474 Render (out .String ())
468- if m .preview .imageContent != "" {
469- rendered += "\n " + m .preview .imageContent
470- }
471- return rendered
472475 }
473476
474477 return lipgloss .NewStyle ().
@@ -628,6 +631,19 @@ func (m MillerView) handleMouse(msg tea.MouseMsg) (MillerView, tea.Cmd) { //noli
628631 return m , nil
629632
630633 case zonePreview :
634+ // If imagecollection, click a FROM FILE line → open that image in overlay
635+ if m .preview .childColumn == nil && len (m .preview .imagePaths ) > 0 {
636+ // Y=0 tabbar, Y=1 MDL header, Y=2+ content (0-indexed visual lines)
637+ clickedVLine := msg .Y - 2
638+ path := findImagePathAtClick (m .preview .contentLines , m .preview .imagePaths ,
639+ clickedVLine , m .preview .scrollOffset )
640+ if path != "" {
641+ return m , func () tea.Msg {
642+ return OpenImageOverlayMsg {Title : "Image Preview" , Paths : []string {path }}
643+ }
644+ }
645+ return m , nil
646+ }
631647 // Click preview child item → drill in, then select the clicked item
632648 if m .preview .childColumn != nil {
633649 clickedIdx := m .preview .childColumn .HitTestIndex (msg .Y )
@@ -748,7 +764,7 @@ func (m *MillerView) updateFocusStyles() {
748764func (m * MillerView ) clearPreview () {
749765 m .preview .childColumn = nil
750766 m .preview .content = ""
751- m .preview .imageContent = ""
767+ m .preview .imagePaths = nil
752768 m .preview .contentLines = nil
753769 m .preview .loading = false
754770 m .preview .scrollOffset = 0
@@ -811,3 +827,37 @@ func cloneItems(items []ColumnItem) []ColumnItem {
811827 copy (cloned , items )
812828 return cloned
813829}
830+
831+ // findImagePathAtClick maps a clicked visual line (0-indexed relative to content area)
832+ // plus the current scroll offset to an image file path in imagePaths.
833+ // Returns "" if no FROM FILE line is found near the click.
834+ func findImagePathAtClick (contentLines , imagePaths []string , clickedVLine , scrollOffset int ) string {
835+ // Approximate source line: each long FROM FILE line typically wraps to ~2 visual lines.
836+ // Search a window of ±3 source lines around the estimate to handle wrapping.
837+ approx := clickedVLine + scrollOffset
838+ for delta := 0 ; delta <= 3 ; delta ++ {
839+ for _ , sign := range []int {0 , 1 , - 1 } {
840+ srcIdx := approx + sign * delta
841+ if srcIdx < 0 || srcIdx >= len (contentLines ) {
842+ continue
843+ }
844+ plain := stripANSI (contentLines [srcIdx ])
845+ i := strings .Index (plain , "FROM FILE '" )
846+ if i == - 1 {
847+ continue
848+ }
849+ rest := plain [i + len ("FROM FILE '" ):]
850+ end := strings .Index (rest , "'" )
851+ if end == - 1 {
852+ continue
853+ }
854+ foundPath := rest [:end ]
855+ for _ , p := range imagePaths {
856+ if p == foundPath {
857+ return p
858+ }
859+ }
860+ }
861+ }
862+ return ""
863+ }
0 commit comments