From 6b0c1b0539afe8ebabb0ba8ca68316204d5ddecb Mon Sep 17 00:00:00 2001 From: Yorukot Date: Sat, 13 Sep 2025 02:36:34 +0800 Subject: [PATCH 1/9] feat: update inline image protocol basically implementing --- src/pkg/file_preview/image_preview.go | 40 +++++++++- src/pkg/file_preview/inline.go | 109 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/pkg/file_preview/inline.go diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index 328341028..73320c51f 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -23,6 +23,7 @@ type ImageRenderer int const ( RendererANSI ImageRenderer = iota RendererKitty + RendererInline ) // ImagePreviewCache stores cached image previews @@ -244,8 +245,33 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, return preview, nil } - // Fall through to ANSI if Kitty fails - slog.Error("Kitty renderer failed, falling back to ANSI", "error", err) + // Fall through to next renderer if Kitty fails + slog.Error("Kitty renderer failed, trying other renderers", "error", err) + } + + // Try inline renderer (iTerm2, WezTerm, etc.) + if p.IsInlineCapable() { + // Check cache for Inline renderer + if preview, found := p.cache.Get(path, dimensions, RendererInline); found { + return preview, nil + } + + preview, err := p.ImagePreviewWithRenderer( + path, + maxWidth, + maxHeight, + defaultBGColor, + RendererInline, + sideAreaWidth, + ) + if err == nil { + // Cache the successful result + p.cache.Set(path, dimensions, preview, RendererInline) + return preview, nil + } + + // Fall through to ANSI if Inline fails + slog.Error("Inline renderer failed, falling back to ANSI", "error", err) } // Check cache for ANSI renderer @@ -296,6 +322,16 @@ func (p *ImagePreviewer) ImagePreviewWithRenderer(path string, maxWidth int, max } return result, nil + case RendererInline: + result, err := p.renderWithInlineUsingTermCap(img, path, originalWidth, + originalHeight, maxWidth, maxHeight, sideAreaWidth) + if err != nil { + // If inline fails, fall back to ANSI renderer + slog.Error("Inline renderer failed, falling back to ANSI", "error", err) + return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight) + } + return result, nil + case RendererANSI: return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight) default: diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go new file mode 100644 index 000000000..b93cec74f --- /dev/null +++ b/src/pkg/file_preview/inline.go @@ -0,0 +1,109 @@ +package filepreview + +import ( + "bytes" + "fmt" + "image" + "log/slog" + "os" + "strconv" + "strings" + + "github.com/BourgeoisBear/rasterm" +) + +// isInlineCapable checks if the terminal supports inline image protocol (iTerm2, WezTerm, etc.) +func isInlineCapable() bool { + isCapable := rasterm.IsItermCapable() + + // Additional detection for terminals that might not be detected by rasterm + if !isCapable { + termProgram := os.Getenv("TERM_PROGRAM") + term := os.Getenv("TERM") + + // List of known terminal identifiers that support inline image protocol + knownTerminals := []string{ + "iTerm2", + "iTerm.app", + "WezTerm", + "Hyper", + "Terminus", + "Tabby", + } + + for _, knownTerm := range knownTerminals { + if strings.EqualFold(termProgram, knownTerm) || strings.EqualFold(term, knownTerm) { + isCapable = true + break + } + } + + // Additional check for iTerm2 specific environment variables + if !isCapable && (os.Getenv("ITERM_SESSION_ID") != "" || os.Getenv("ITERM_PROFILE") != "") { + isCapable = true + } + } + + return isCapable +} + + + + +// renderWithInlineUsingTermCap renders an image using inline image protocol +func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path string, + originalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int) (string, error) { + + // Validate dimensions + if maxWidth <= 0 || maxHeight <= 0 { + return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) + } + + var buf bytes.Buffer + + slog.Debug("inline renderer starting", "path", path, "maxWidth", maxWidth, "maxHeight", maxHeight) + + // Calculate display dimensions in character cells + imgRatio := float64(originalWidth) / float64(originalHeight) + termRatio := float64(maxWidth) / float64(maxHeight) + + var displayWidthCells, displayHeightCells int + + if imgRatio > termRatio { + // Image is wider, constrain by width + displayWidthCells = maxWidth + displayHeightCells = int(float64(maxWidth) / imgRatio) + } else { + // Image is taller, constrain by height + displayHeightCells = maxHeight + displayWidthCells = int(float64(maxHeight) * imgRatio) + } + + // Ensure minimum dimensions + if displayWidthCells < 1 { + displayWidthCells = 1 + } + if displayHeightCells < 1 { + displayHeightCells = 1 + } + + slog.Debug("inline display dimensions", "widthCells", displayWidthCells, "heightCells", displayHeightCells) + + // Use rasterm to write the image using iTerm2/WezTerm protocol + if err := rasterm.ItermWriteImage(&buf, img); err != nil { + return "", fmt.Errorf("failed to write image using rasterm: %w", err) + } + + // Position cursor after the image output (following kitty.go pattern) + buf.WriteString("\x1b[2;" + strconv.Itoa(sideAreaWidth) + "H") + + // Add a newline to ensure proper display + buf.WriteString("\n") + + return buf.String(), nil +} + +// IsInlineCapable checks if the terminal supports inline image protocol +func (p *ImagePreviewer) IsInlineCapable() bool { + return isInlineCapable() +} \ No newline at end of file From 1790cd40a661386a57e768e04f86d89f3c332599 Mon Sep 17 00:00:00 2001 From: Yorukot Date: Sat, 13 Sep 2025 15:34:34 +0800 Subject: [PATCH 2/9] fix: try to fix that line image do not be able to clear --- src/internal/ui/preview/model.go | 19 ++++++------- src/pkg/file_preview/image_preview.go | 39 +++++++++++++++++++++------ src/pkg/file_preview/inline.go | 19 +++++++------ 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/internal/ui/preview/model.go b/src/internal/ui/preview/model.go index a6ab9aabd..e7b03f44f 100644 --- a/src/internal/ui/preview/model.go +++ b/src/internal/ui/preview/model.go @@ -14,18 +14,15 @@ import ( "sort" "strings" - "github.com/yorukot/superfile/src/internal/ui" - "github.com/yorukot/superfile/src/internal/ui/rendering" - - "github.com/yorukot/superfile/src/internal/common" - "github.com/yorukot/superfile/src/internal/utils" - "github.com/alecthomas/chroma/v2/lexers" "github.com/charmbracelet/lipgloss" - "github.com/yorukot/ansichroma" "github.com/yorukot/superfile/src/config/icon" + "github.com/yorukot/superfile/src/internal/common" + "github.com/yorukot/superfile/src/internal/ui" + "github.com/yorukot/superfile/src/internal/ui/rendering" + "github.com/yorukot/superfile/src/internal/utils" filepreview "github.com/yorukot/superfile/src/pkg/file_preview" ) @@ -79,7 +76,7 @@ func (m *Model) Close() { func (m *Model) RenderText(text string) string { return ui.FilePreviewPanelRenderer(m.height, m.width). AddLines(text). - Render() + m.imagePreviewer.ClearKittyImages() + Render() + m.imagePreviewer.ClearAllImages() } func (m *Model) SetContentWithRenderText(text string) { @@ -194,7 +191,7 @@ func (m *Model) renderImagePreview(box lipgloss.Style, itemPath string, previewW } // Use the new auto-detection function to choose the best renderer - imageRender, err := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight, + imageRender, err, renderedType := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight, common.Theme.FilePanelBG, sideAreaWidth) if errors.Is(err, image.ErrFormat) { return box.Render("\n --- " + icon.Error + " Unsupported image formats ---") @@ -207,7 +204,7 @@ func (m *Model) renderImagePreview(box lipgloss.Style, itemPath string, previewW // Check if this looks like Kitty protocol output (starts with escape sequences) // For Kitty protocol, avoid using lipgloss alignment to prevent layout drift - if strings.HasPrefix(imageRender, "\x1b_G") { + if renderedType != filepreview.RendererANSI { rendered := common.FilePreviewBox(previewHeight, previewWidth).Render(imageRender) return rendered } @@ -271,7 +268,7 @@ func (m *Model) RenderWithPath(itemPath string, fullModelWidth int) string { box := common.FilePreviewBox(previewHeight, previewWidth) r := ui.FilePreviewPanelRenderer(previewHeight, previewWidth) - clearCmd := m.imagePreviewer.ClearKittyImages() + clearCmd := m.imagePreviewer.ClearAllImages() fileInfo, infoErr := os.Stat(itemPath) if infoErr != nil { diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index 73320c51f..ed620c83f 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -11,6 +11,7 @@ import ( "log/slog" "os" "strconv" + "strings" "sync" "time" @@ -215,10 +216,10 @@ func ConvertImageToANSI(img image.Image, defaultBGColor color.Color) string { // ImagePreview generates a preview of an image file func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, - defaultBGColor string, sideAreaWidth int) (string, error) { + defaultBGColor string, sideAreaWidth int) (string, error, ImageRenderer) { // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { - return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) + return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight), RendererANSI } // Create dimensions string for cache key @@ -228,7 +229,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if p.IsKittyCapable() { // Check cache for Kitty renderer if preview, found := p.cache.Get(path, dimensions, RendererKitty); found { - return preview, nil + return preview, nil, RendererKitty } preview, err := p.ImagePreviewWithRenderer( @@ -242,7 +243,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if err == nil { // Cache the successful result p.cache.Set(path, dimensions, preview, RendererKitty) - return preview, nil + return preview, nil, RendererKitty } // Fall through to next renderer if Kitty fails @@ -253,7 +254,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if p.IsInlineCapable() { // Check cache for Inline renderer if preview, found := p.cache.Get(path, dimensions, RendererInline); found { - return preview, nil + return preview, nil, RendererInline } preview, err := p.ImagePreviewWithRenderer( @@ -267,7 +268,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if err == nil { // Cache the successful result p.cache.Set(path, dimensions, preview, RendererInline) - return preview, nil + return preview, nil, RendererInline } // Fall through to ANSI if Inline fails @@ -276,7 +277,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, // Check cache for ANSI renderer if preview, found := p.cache.Get(path, dimensions, RendererANSI); found { - return preview, nil + return preview, nil, RendererANSI } // Fall back to ANSI @@ -285,7 +286,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, // Cache the successful result p.cache.Set(path, dimensions, preview, RendererANSI) } - return preview, err + return preview, err, RendererANSI } // ImagePreviewWithRenderer generates an image preview using the specified renderer @@ -377,3 +378,25 @@ func colorToHex(color color.Color) string { uint8(b>>rgbShift8), //nolint:gosec // RGBA() returns 16-bit values, shifting by 8 gives 8-bit ) } + +// ClearAllImages clears all images from the terminal using the appropriate protocol +// This method intelligently detects terminal capabilities and clears images accordingly +func (p *ImagePreviewer) ClearAllImages() string { + var result strings.Builder + + // Clear Kitty protocol images if supported + if p.IsKittyCapable() { + if clearCmd := p.ClearKittyImages(); clearCmd != "" { + result.WriteString(clearCmd) + } + } + + // Clear inline protocol images if supported + if p.IsInlineCapable() { + if clearCmd := p.ClearInlineImage(); clearCmd != "" { + result.WriteString(clearCmd) + } + } + + return result.String() +} diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index b93cec74f..8aef41044 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -48,12 +48,19 @@ func isInlineCapable() bool { } +// ClearInlineImage clears all inline image protocol images from the terminal +func (p *ImagePreviewer) ClearInlineImage() string { + if !p.IsInlineCapable() { + return "" // No need to clear if terminal doesn't support inline protocol + } + return strings.Repeat(" ", 9999) +} // renderWithInlineUsingTermCap renders an image using inline image protocol func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path string, originalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int) (string, error) { - + // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) @@ -74,7 +81,7 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path stri displayWidthCells = maxWidth displayHeightCells = int(float64(maxWidth) / imgRatio) } else { - // Image is taller, constrain by height + // Image is taller, constrain by height displayHeightCells = maxHeight displayWidthCells = int(float64(maxHeight) * imgRatio) } @@ -94,11 +101,7 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path stri return "", fmt.Errorf("failed to write image using rasterm: %w", err) } - // Position cursor after the image output (following kitty.go pattern) - buf.WriteString("\x1b[2;" + strconv.Itoa(sideAreaWidth) + "H") - - // Add a newline to ensure proper display - buf.WriteString("\n") + buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") return buf.String(), nil } @@ -106,4 +109,4 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path stri // IsInlineCapable checks if the terminal supports inline image protocol func (p *ImagePreviewer) IsInlineCapable() bool { return isInlineCapable() -} \ No newline at end of file +} From fea4eee734d2e11035de74b6dc5ed995c324ea75 Mon Sep 17 00:00:00 2001 From: lazysegtree <59679977+lazysegtree@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:21:36 +0530 Subject: [PATCH 3/9] fix: temp --- src/internal/ui/preview/model.go | 1 + src/pkg/file_preview/image_preview.go | 109 ------------------- src/pkg/file_preview/image_preview_cache.go | 114 ++++++++++++++++++++ src/pkg/file_preview/inline.go | 2 +- src/pkg/file_preview/kitty.go | 2 + 5 files changed, 118 insertions(+), 110 deletions(-) create mode 100644 src/pkg/file_preview/image_preview_cache.go diff --git a/src/internal/ui/preview/model.go b/src/internal/ui/preview/model.go index e7b03f44f..0fed95e20 100644 --- a/src/internal/ui/preview/model.go +++ b/src/internal/ui/preview/model.go @@ -206,6 +206,7 @@ func (m *Model) renderImagePreview(box lipgloss.Style, itemPath string, previewW // For Kitty protocol, avoid using lipgloss alignment to prevent layout drift if renderedType != filepreview.RendererANSI { rendered := common.FilePreviewBox(previewHeight, previewWidth).Render(imageRender) + slog.Debug("[TEMP]", "final", rendered, "initial", imageRender) return rendered } diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index ed620c83f..7f8f4138c 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -12,7 +12,6 @@ import ( "os" "strconv" "strings" - "sync" "time" "github.com/muesli/termenv" @@ -27,22 +26,6 @@ const ( RendererInline ) -// ImagePreviewCache stores cached image previews -type ImagePreviewCache struct { - cache map[string]*CachedPreview - mutex sync.RWMutex - maxEntries int - expiration time.Duration -} - -// CachedPreview represents a cached image preview -type CachedPreview struct { - Preview string - Timestamp time.Time - Renderer ImageRenderer - Dimensions string // "width,height,bgColor,sideAreaWidth" -} - // ImagePreviewer encapsulates image preview functionality with caching type ImagePreviewer struct { cache *ImagePreviewCache @@ -67,98 +50,6 @@ func NewImagePreviewerWithConfig(maxEntries int, expiration time.Duration) *Imag return previewer } -// NewImagePreviewCache creates a new image preview cache -func NewImagePreviewCache(maxEntries int, expiration time.Duration) *ImagePreviewCache { - cache := &ImagePreviewCache{ - cache: make(map[string]*CachedPreview), - maxEntries: maxEntries, - expiration: expiration, - } - - // Start a cleanup goroutine - go cache.periodicCleanup() - - return cache -} - -// periodicCleanup removes expired entries periodically -func (c *ImagePreviewCache) periodicCleanup() { - //nolint:mnd // half of expiration for cleanup interval - ticker := time.NewTicker(c.expiration / 2) - defer ticker.Stop() - - for range ticker.C { - c.cleanupExpired() - } -} - -// cleanupExpired removes expired cache entries -func (c *ImagePreviewCache) cleanupExpired() { - now := time.Now() - c.mutex.Lock() - defer c.mutex.Unlock() - - for key, entry := range c.cache { - if now.Sub(entry.Timestamp) > c.expiration { - delete(c.cache, key) - } - } -} - -// Get retrieves a cached preview if available -func (c *ImagePreviewCache) Get(path, dimensions string, renderer ImageRenderer) (string, bool) { - c.mutex.RLock() - defer c.mutex.RUnlock() - - cacheKey := path + ":" + dimensions - - if entry, exists := c.cache[cacheKey]; exists { - if entry.Renderer == renderer && time.Since(entry.Timestamp) < c.expiration { - return entry.Preview, true - } - } - - return "", false -} - -// Set stores a preview in the cache -func (c *ImagePreviewCache) Set(path, dimensions, preview string, renderer ImageRenderer) { - c.mutex.Lock() - defer c.mutex.Unlock() - - // Check if we need to evict entries - if len(c.cache) >= c.maxEntries { - c.evictOldest() - } - - cacheKey := path + ":" + dimensions - c.cache[cacheKey] = &CachedPreview{ - Preview: preview, - Timestamp: time.Now(), - Renderer: renderer, - Dimensions: dimensions, - } -} - -// evictOldest removes the oldest entry from the cache -func (c *ImagePreviewCache) evictOldest() { - var oldestKey string - var oldestTime time.Time - - // Find the oldest entry - for key, entry := range c.cache { - if oldestKey == "" || entry.Timestamp.Before(oldestTime) { - oldestKey = key - oldestTime = entry.Timestamp - } - } - - // Remove the oldest entry - if oldestKey != "" { - delete(c.cache, oldestKey) - } -} - type colorCache struct { rgbaToTermenv map[color.RGBA]termenv.RGBColor } diff --git a/src/pkg/file_preview/image_preview_cache.go b/src/pkg/file_preview/image_preview_cache.go new file mode 100644 index 000000000..35a15bdf1 --- /dev/null +++ b/src/pkg/file_preview/image_preview_cache.go @@ -0,0 +1,114 @@ +package filepreview + +import ( + "sync" + "time" +) + +// ImagePreviewCache stores cached image previews +type ImagePreviewCache struct { + cache map[string]*CachedPreview + mutex sync.RWMutex + maxEntries int + expiration time.Duration +} + +// CachedPreview represents a cached image preview +type CachedPreview struct { + Preview string + Timestamp time.Time + Renderer ImageRenderer + Dimensions string // "width,height,bgColor,sideAreaWidth" +} + +// NewImagePreviewCache creates a new image preview cache +func NewImagePreviewCache(maxEntries int, expiration time.Duration) *ImagePreviewCache { + cache := &ImagePreviewCache{ + cache: make(map[string]*CachedPreview), + maxEntries: maxEntries, + expiration: expiration, + } + + // Start a cleanup goroutine + go cache.periodicCleanup() + + return cache +} + +// periodicCleanup removes expired entries periodically +func (c *ImagePreviewCache) periodicCleanup() { + //nolint:mnd // half of expiration for cleanup interval + ticker := time.NewTicker(c.expiration / 2) + defer ticker.Stop() + + for range ticker.C { + c.cleanupExpired() + } +} + +// cleanupExpired removes expired cache entries +func (c *ImagePreviewCache) cleanupExpired() { + now := time.Now() + c.mutex.Lock() + defer c.mutex.Unlock() + + for key, entry := range c.cache { + if now.Sub(entry.Timestamp) > c.expiration { + delete(c.cache, key) + } + } +} + +// Get retrieves a cached preview if available +func (c *ImagePreviewCache) Get(path, dimensions string, renderer ImageRenderer) (string, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + cacheKey := path + ":" + dimensions + + if entry, exists := c.cache[cacheKey]; exists { + if entry.Renderer == renderer && time.Since(entry.Timestamp) < c.expiration { + return entry.Preview, true + } + } + + return "", false +} + +// Set stores a preview in the cache +func (c *ImagePreviewCache) Set(path, dimensions, preview string, renderer ImageRenderer) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // Check if we need to evict entries + if len(c.cache) >= c.maxEntries { + c.evictOldest() + } + + cacheKey := path + ":" + dimensions + c.cache[cacheKey] = &CachedPreview{ + Preview: preview, + Timestamp: time.Now(), + Renderer: renderer, + Dimensions: dimensions, + } +} + +// evictOldest removes the oldest entry from the cache +func (c *ImagePreviewCache) evictOldest() { + var oldestKey string + var oldestTime time.Time + + // Find the oldest entry + for key, entry := range c.cache { + if oldestKey == "" || entry.Timestamp.Before(oldestTime) { + oldestKey = key + oldestTime = entry.Timestamp + } + } + + // Remove the oldest entry + if oldestKey != "" { + delete(c.cache, oldestKey) + } +} diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index 8aef41044..3c4957120 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -43,11 +43,11 @@ func isInlineCapable() bool { isCapable = true } } + slog.Debug("[TEMP] ", "inlineCapabale", isCapable) return isCapable } - // ClearInlineImage clears all inline image protocol images from the terminal func (p *ImagePreviewer) ClearInlineImage() string { if !p.IsInlineCapable() { diff --git a/src/pkg/file_preview/kitty.go b/src/pkg/file_preview/kitty.go index 3a3f246c8..2253032e1 100644 --- a/src/pkg/file_preview/kitty.go +++ b/src/pkg/file_preview/kitty.go @@ -40,6 +40,8 @@ func isKittyCapable() bool { } } + slog.Debug("[TEMP] ", "kittyCapabale", isCapable) + return isCapable } From 6f8ab6a6cf0fc3f57840d7e89f6f4bbbbc9805f2 Mon Sep 17 00:00:00 2001 From: lazysegtree <59679977+lazysegtree@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:33:14 +0530 Subject: [PATCH 4/9] fix: rename box to filePreviewStyle --- go.mod | 2 +- go.sum | 4 +- src/internal/common/style_function.go | 2 +- src/internal/ui/preview/model.go | 60 ++++++++++++++++----------- src/pkg/file_preview/image_preview.go | 6 ++- src/pkg/file_preview/inline.go | 1 - 6 files changed, 45 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 8abaa25b1..8187e9249 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( ) require ( - github.com/BourgeoisBear/rasterm v1.1.1 + github.com/BourgeoisBear/rasterm v1.1.2 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index a1a9e0a1c..4ea85a051 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BourgeoisBear/rasterm v1.1.1 h1:J94gv2pRv+G0jXj9Pf3jUk2qQtWPCiTsiRGxlXoQvgo= -github.com/BourgeoisBear/rasterm v1.1.1/go.mod h1:Ifd+To5s/uyUiYx+B4fxhS8lUNwNLSxDBjskmC5pEyw= +github.com/BourgeoisBear/rasterm v1.1.2 h1:hWHZBZ45N366uNSqxWFYBV0y19q8fXRXADhPkoLF4Ss= +github.com/BourgeoisBear/rasterm v1.1.2/go.mod h1:Ifd+To5s/uyUiYx+B4fxhS8lUNwNLSxDBjskmC5pEyw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= diff --git a/src/internal/common/style_function.go b/src/internal/common/style_function.go index 72c4cdb2a..6152b0db1 100644 --- a/src/internal/common/style_function.go +++ b/src/internal/common/style_function.go @@ -36,7 +36,7 @@ func FilePanelBorderStyle(height int, width int, filePanelFocussed bool, borderB } // Generate filePreview Box -func FilePreviewBox(height int, width int) lipgloss.Style { +func FilePreviewStyle(height int, width int) lipgloss.Style { return lipgloss.NewStyle(). Width(width). Height(height). diff --git a/src/internal/ui/preview/model.go b/src/internal/ui/preview/model.go index 0fed95e20..440eec845 100644 --- a/src/internal/ui/preview/model.go +++ b/src/internal/ui/preview/model.go @@ -129,8 +129,8 @@ func renderFileInfoError(r *rendering.Renderer, err error) string { return r.Render() } -func renderUnsupportedFormat(box lipgloss.Style) string { - return box.Render(common.FilePreviewUnsupportedFormatText) +func renderUnsupportedFormat(filePreviewStyle lipgloss.Style) string { + return filePreviewStyle.Render(common.FilePreviewUnsupportedFormatText) } func renderUnsupportedFileMode(r *rendering.Renderer) string { @@ -138,8 +138,8 @@ func renderUnsupportedFileMode(r *rendering.Renderer) string { return r.Render() } -func renderThumbnailGenerationError(box lipgloss.Style) string { - return box.Render(common.FilePreviewThumbnailGenerationErrorText) +func renderThumbnailGenerationError(filePreviewStyle lipgloss.Style) string { + return filePreviewStyle.Render(common.FilePreviewThumbnailGenerationErrorText) } func renderDirectoryPreview(r *rendering.Renderer, itemPath string, previewHeight int) string { @@ -179,42 +179,42 @@ func renderDirectoryPreview(r *rendering.Renderer, itemPath string, previewHeigh return r.Render() } -func (m *Model) renderImagePreview(box lipgloss.Style, itemPath string, previewWidth, +func (m *Model) renderImagePreview(filePreviewStyle lipgloss.Style, itemPath string, previewWidth, previewHeight int, sideAreaWidth int, ) string { if !m.open { - return box.Render("\n --- Preview panel is closed ---") + return filePreviewStyle.Render("\n --- Preview panel is closed ---") } if !common.Config.ShowImagePreview { - return box.Render("\n --- Image preview is disabled ---") + return filePreviewStyle.Render("\n --- Image preview is disabled ---") } // Use the new auto-detection function to choose the best renderer imageRender, err, renderedType := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight, common.Theme.FilePanelBG, sideAreaWidth) if errors.Is(err, image.ErrFormat) { - return box.Render("\n --- " + icon.Error + " Unsupported image formats ---") + return filePreviewStyle.Render("\n --- " + icon.Error + " Unsupported image formats ---") } if err != nil { slog.Error("Error convert image to ansi", "error", err) - return box.Render("\n --- " + icon.Error + " Error convert image to ansi ---") + return filePreviewStyle.Render("\n --- " + icon.Error + " Error convert image to ansi ---") } // Check if this looks like Kitty protocol output (starts with escape sequences) // For Kitty protocol, avoid using lipgloss alignment to prevent layout drift if renderedType != filepreview.RendererANSI { - rendered := common.FilePreviewBox(previewHeight, previewWidth).Render(imageRender) + rendered := filePreviewStyle.Render(imageRender) slog.Debug("[TEMP]", "final", rendered, "initial", imageRender) return rendered } // For ANSI output, we can safely use vertical alignment - return box.AlignVertical(lipgloss.Center).AlignHorizontal(lipgloss.Center).Render(imageRender) + return filePreviewStyle.AlignVertical(lipgloss.Center).AlignHorizontal(lipgloss.Center).Render(imageRender) } -func (m *Model) renderTextPreview(r *rendering.Renderer, box lipgloss.Style, itemPath string, +func (m *Model) renderTextPreview(r *rendering.Renderer, filePreviewStyle lipgloss.Style, itemPath string, previewWidth, previewHeight int, ) string { format := lexers.Match(filepath.Base(itemPath)) @@ -222,20 +222,20 @@ func (m *Model) renderTextPreview(r *rendering.Renderer, box lipgloss.Style, ite isText, err := common.IsTextFile(itemPath) if err != nil { slog.Error("Error while checking text file", "error", err) - return box.Render(common.FilePreviewError) + return filePreviewStyle.Render(common.FilePreviewError) } else if !isText { - return box.Render(common.FilePreviewUnsupportedFormatText) + return filePreviewStyle.Render(common.FilePreviewUnsupportedFormatText) } } fileContent, err := utils.ReadFileContent(itemPath, previewWidth, previewHeight) if err != nil { slog.Error("Error open file", "error", err) - return box.Render(common.FilePreviewError) + return filePreviewStyle.Render(common.FilePreviewError) } if fileContent == "" { - return box.Render(common.FilePreviewEmptyText) + return filePreviewStyle.Render(common.FilePreviewEmptyText) } if format != nil { @@ -245,7 +245,7 @@ func (m *Model) renderTextPreview(r *rendering.Renderer, box lipgloss.Style, ite } if common.Config.CodePreviewer == "bat" { if m.batCmd == "" { - return box.Render("\n --- " + icon.Error + + return filePreviewStyle.Render("\n --- " + icon.Error + " 'bat' is not installed or not found. ---\n --- Cannot render file preview. ---") } fileContent, err = getBatSyntaxHighlightedContent(itemPath, previewHeight, background, m.batCmd) @@ -255,7 +255,7 @@ func (m *Model) renderTextPreview(r *rendering.Renderer, box lipgloss.Style, ite } if err != nil { slog.Error("Error render code highlight", "error", err) - return box.Render("\n" + common.FilePreviewError) + return filePreviewStyle.Render("\n" + common.FilePreviewError) } } @@ -267,7 +267,7 @@ func (m *Model) RenderWithPath(itemPath string, fullModelWidth int) string { previewHeight := m.height previewWidth := m.width - box := common.FilePreviewBox(previewHeight, previewWidth) + filePreviewStyle := common.FilePreviewStyle(previewHeight, previewWidth) r := ui.FilePreviewPanelRenderer(previewHeight, previewWidth) clearCmd := m.imagePreviewer.ClearAllImages() @@ -286,7 +286,7 @@ func (m *Model) RenderWithPath(itemPath string, fullModelWidth int) string { ext := filepath.Ext(itemPath) if slices.Contains(common.UnsupportedPreviewFormats, ext) { - return renderUnsupportedFormat(box) + clearCmd + return renderUnsupportedFormat(filePreviewStyle) + clearCmd } if fileInfo.IsDir() { @@ -297,16 +297,28 @@ func (m *Model) RenderWithPath(itemPath string, fullModelWidth int) string { thumbnailPath, err := m.thumbnailGenerator.GetThumbnailOrGenerate(itemPath) if err != nil { slog.Error("Error generating thumbnail", "error", err) - return renderThumbnailGenerationError(box) + clearCmd + return renderThumbnailGenerationError(filePreviewStyle) + clearCmd } - return m.renderImagePreview(box, thumbnailPath, previewWidth, previewHeight, fullModelWidth-previewWidth+1) + return m.renderImagePreview( + filePreviewStyle, + thumbnailPath, + previewWidth, + previewHeight, + fullModelWidth-previewWidth+1, + ) } if isImageFile(itemPath) { - return m.renderImagePreview(box, itemPath, previewWidth, previewHeight, fullModelWidth-previewWidth+1) + return m.renderImagePreview( + filePreviewStyle, + itemPath, + previewWidth, + previewHeight, + fullModelWidth-previewWidth+1, + ) } - return m.renderTextPreview(r, box, itemPath, previewWidth, previewHeight) + clearCmd + return m.renderTextPreview(r, filePreviewStyle, itemPath, previewWidth, previewHeight) + clearCmd } func getBatSyntaxHighlightedContent( diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index 7f8f4138c..4e226a78e 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -110,7 +110,11 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, defaultBGColor string, sideAreaWidth int) (string, error, ImageRenderer) { // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { - return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight), RendererANSI + return "", fmt.Errorf( + "dimensions must be positive (maxWidth=%d, maxHeight=%d)", + maxWidth, + maxHeight, + ), RendererANSI } // Create dimensions string for cache key diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index 3c4957120..feb6f3b6f 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -60,7 +60,6 @@ func (p *ImagePreviewer) ClearInlineImage() string { // renderWithInlineUsingTermCap renders an image using inline image protocol func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path string, originalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int) (string, error) { - // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) From 1c8e9b593ade89698f0d1b28162a029b83448fc7 Mon Sep 17 00:00:00 2001 From: lazysegtree <59679977+lazysegtree@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:37:31 +0530 Subject: [PATCH 5/9] fix: prevent run on draft PR --- .github/workflows/superfile-build-test.yml | 1 + .github/workflows/testsuite-run.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/superfile-build-test.yml b/.github/workflows/superfile-build-test.yml index fa0a92707..91eb49a6b 100644 --- a/.github/workflows/superfile-build-test.yml +++ b/.github/workflows/superfile-build-test.yml @@ -11,6 +11,7 @@ permissions: jobs: build: + if: github.event.pull_request.draft == false name: Build and Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/testsuite-run.yml b/.github/workflows/testsuite-run.yml index 56d927c09..8c9188921 100644 --- a/.github/workflows/testsuite-run.yml +++ b/.github/workflows/testsuite-run.yml @@ -11,6 +11,7 @@ permissions: jobs: test: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Checkout code From db197c30e9b38f46cef0dfdb613296719c5a1428 Mon Sep 17 00:00:00 2001 From: yorukot <107802416+yorukot@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:08:37 +0000 Subject: [PATCH 6/9] chore: update gomod2nix --- gomod2nix.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gomod2nix.toml b/gomod2nix.toml index c1e2f7191..2f2522417 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -2,8 +2,8 @@ schema = 3 [mod] [mod."github.com/BourgeoisBear/rasterm"] - version = "v1.1.1" - hash = "sha256-uCZauICgNHFVJvq1rsZ8nDwMgyNsJOOPFwXIgWcqmFE=" + version = "v1.1.2" + hash = "sha256-fYV85hVcIAT01xriEpWn0f/YyGgsc7W+0SW6iz9K/+A=" [mod."github.com/adrg/xdg"] version = "v0.5.3" hash = "sha256-bo6tBgHS+3sl6f4oWpmdFrZjfV6eA/3xAlysSW0bIEs=" From 6e617955cbb23f0471ca50c7880c58793f2d98e9 Mon Sep 17 00:00:00 2001 From: lazysegtree <59679977+lazysegtree@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:53:53 +0530 Subject: [PATCH 7/9] fix: Pass preview Style to renderers --- src/internal/ui/preview/model.go | 12 +++---- src/pkg/file_preview/image_preview.go | 49 +++++++++++++++++---------- src/pkg/file_preview/inline.go | 18 +++++----- src/pkg/file_preview/kitty.go | 5 +-- 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/internal/ui/preview/model.go b/src/internal/ui/preview/model.go index 440eec845..5e3c99bda 100644 --- a/src/internal/ui/preview/model.go +++ b/src/internal/ui/preview/model.go @@ -191,8 +191,8 @@ func (m *Model) renderImagePreview(filePreviewStyle lipgloss.Style, itemPath str } // Use the new auto-detection function to choose the best renderer - imageRender, err, renderedType := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight, - common.Theme.FilePanelBG, sideAreaWidth) + imageRender, err := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight, + common.Theme.FilePanelBG, sideAreaWidth, filePreviewStyle) if errors.Is(err, image.ErrFormat) { return filePreviewStyle.Render("\n --- " + icon.Error + " Unsupported image formats ---") } @@ -201,17 +201,13 @@ func (m *Model) renderImagePreview(filePreviewStyle lipgloss.Style, itemPath str slog.Error("Error convert image to ansi", "error", err) return filePreviewStyle.Render("\n --- " + icon.Error + " Error convert image to ansi ---") } + return imageRender // Check if this looks like Kitty protocol output (starts with escape sequences) // For Kitty protocol, avoid using lipgloss alignment to prevent layout drift - if renderedType != filepreview.RendererANSI { - rendered := filePreviewStyle.Render(imageRender) - slog.Debug("[TEMP]", "final", rendered, "initial", imageRender) - return rendered - } // For ANSI output, we can safely use vertical alignment - return filePreviewStyle.AlignVertical(lipgloss.Center).AlignHorizontal(lipgloss.Center).Render(imageRender) + } func (m *Model) renderTextPreview(r *rendering.Renderer, filePreviewStyle lipgloss.Style, itemPath string, diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index 4e226a78e..0634e1906 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" _ "golang.org/x/image/webp" // Register WebP decoder ) @@ -107,14 +108,14 @@ func ConvertImageToANSI(img image.Image, defaultBGColor color.Color) string { // ImagePreview generates a preview of an image file func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, - defaultBGColor string, sideAreaWidth int) (string, error, ImageRenderer) { + defaultBGColor string, sideAreaWidth int, filePreviewStyle lipgloss.Style) (string, error) { // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf( "dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight, - ), RendererANSI + ) } // Create dimensions string for cache key @@ -124,7 +125,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if p.IsKittyCapable() { // Check cache for Kitty renderer if preview, found := p.cache.Get(path, dimensions, RendererKitty); found { - return preview, nil, RendererKitty + return preview, nil } preview, err := p.ImagePreviewWithRenderer( @@ -134,11 +135,12 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, defaultBGColor, RendererKitty, sideAreaWidth, + filePreviewStyle, ) if err == nil { // Cache the successful result p.cache.Set(path, dimensions, preview, RendererKitty) - return preview, nil, RendererKitty + return preview, nil } // Fall through to next renderer if Kitty fails @@ -149,7 +151,7 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, if p.IsInlineCapable() { // Check cache for Inline renderer if preview, found := p.cache.Get(path, dimensions, RendererInline); found { - return preview, nil, RendererInline + return preview, nil } preview, err := p.ImagePreviewWithRenderer( @@ -159,11 +161,12 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, defaultBGColor, RendererInline, sideAreaWidth, + filePreviewStyle, ) if err == nil { // Cache the successful result p.cache.Set(path, dimensions, preview, RendererInline) - return preview, nil, RendererInline + return preview, nil } // Fall through to ANSI if Inline fails @@ -172,21 +175,29 @@ func (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int, // Check cache for ANSI renderer if preview, found := p.cache.Get(path, dimensions, RendererANSI); found { - return preview, nil, RendererANSI + return preview, nil } // Fall back to ANSI - preview, err := p.ImagePreviewWithRenderer(path, maxWidth, maxHeight, defaultBGColor, RendererANSI, sideAreaWidth) + preview, err := p.ImagePreviewWithRenderer( + path, + maxWidth, + maxHeight, + defaultBGColor, + RendererANSI, + sideAreaWidth, + filePreviewStyle, + ) if err == nil { // Cache the successful result p.cache.Set(path, dimensions, preview, RendererANSI) } - return preview, err, RendererANSI + return preview, err } // ImagePreviewWithRenderer generates an image preview using the specified renderer func (p *ImagePreviewer) ImagePreviewWithRenderer(path string, maxWidth int, maxHeight int, - defaultBGColor string, renderer ImageRenderer, sideAreaWidth int) (string, error) { + defaultBGColor string, renderer ImageRenderer, sideAreaWidth int, filePreviewStyle lipgloss.Style) (string, error) { info, err := os.Stat(path) if err != nil { return "", err @@ -210,26 +221,26 @@ func (p *ImagePreviewer) ImagePreviewWithRenderer(path string, maxWidth int, max switch renderer { case RendererKitty: result, err := p.renderWithKittyUsingTermCap(img, path, originalWidth, - originalHeight, maxWidth, maxHeight, sideAreaWidth) + originalHeight, maxWidth, maxHeight, sideAreaWidth, filePreviewStyle) if err != nil { // If kitty fails, fall back to ANSI renderer slog.Error("Kitty renderer failed, falling back to ANSI", "error", err) - return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight) + return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight, filePreviewStyle) } return result, nil case RendererInline: result, err := p.renderWithInlineUsingTermCap(img, path, originalWidth, - originalHeight, maxWidth, maxHeight, sideAreaWidth) + originalHeight, maxWidth, maxHeight, sideAreaWidth, filePreviewStyle) if err != nil { // If inline fails, fall back to ANSI renderer slog.Error("Inline renderer failed, falling back to ANSI", "error", err) - return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight) + return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight, filePreviewStyle) } return result, nil case RendererANSI: - return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight) + return p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight, filePreviewStyle) default: return "", fmt.Errorf("invalid renderer : %v", renderer) } @@ -237,7 +248,7 @@ func (p *ImagePreviewer) ImagePreviewWithRenderer(path string, maxWidth int, max // Convert image to ansi func (p *ImagePreviewer) ANSIRenderer(img image.Image, defaultBGColor string, - maxWidth int, maxHeight int) (string, error) { + maxWidth int, maxHeight int, filePreviewStyle lipgloss.Style) (string, error) { bgColor, err := hexToColor(defaultBGColor) if err != nil { return "", fmt.Errorf("invalid background color: %w", err) @@ -245,7 +256,11 @@ func (p *ImagePreviewer) ANSIRenderer(img image.Image, defaultBGColor string, // For ANSI rendering, resize image appropriately fittedImg := resizeForANSI(img, maxWidth, maxHeight) - return ConvertImageToANSI(fittedImg, bgColor), nil + res := ConvertImageToANSI(fittedImg, bgColor) + + return filePreviewStyle. + AlignVertical(lipgloss.Center). + AlignHorizontal(lipgloss.Center).Render(res), nil } func hexToColor(hex string) (color.RGBA, error) { diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index feb6f3b6f..07b1991f9 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/BourgeoisBear/rasterm" + "github.com/charmbracelet/lipgloss" ) // isInlineCapable checks if the terminal supports inline image protocol (iTerm2, WezTerm, etc.) @@ -50,16 +51,17 @@ func isInlineCapable() bool { // ClearInlineImage clears all inline image protocol images from the terminal func (p *ImagePreviewer) ClearInlineImage() string { - if !p.IsInlineCapable() { - return "" // No need to clear if terminal doesn't support inline protocol - } - - return strings.Repeat(" ", 9999) + return "" } // renderWithInlineUsingTermCap renders an image using inline image protocol -func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path string, - originalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int) (string, error) { +func (p *ImagePreviewer) renderWithInlineUsingTermCap( + img image.Image, + path string, + originalWidth, originalHeight, maxWidth, maxHeight int, + sideAreaWidth int, + filePreviewStyle lipgloss.Style, +) (string, error) { // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) @@ -102,7 +104,7 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap(img image.Image, path stri buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") - return buf.String(), nil + return filePreviewStyle.Render(buf.String()), nil } // IsInlineCapable checks if the terminal supports inline image protocol diff --git a/src/pkg/file_preview/kitty.go b/src/pkg/file_preview/kitty.go index 2253032e1..74f1ad244 100644 --- a/src/pkg/file_preview/kitty.go +++ b/src/pkg/file_preview/kitty.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/BourgeoisBear/rasterm" + "github.com/charmbracelet/lipgloss" ) // isKittyCapable checks if the terminal supports Kitty graphics protocol @@ -98,7 +99,7 @@ func generatePlacementID(path string) uint32 { // renderWithKittyUsingTermCap renders an image using Kitty graphics protocol with terminal capabilities func (p *ImagePreviewer) renderWithKittyUsingTermCap(img image.Image, path string, originalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int, -) (string, error) { + filePreviewStyle lipgloss.Style) (string, error) { // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) @@ -144,7 +145,7 @@ func (p *ImagePreviewer) renderWithKittyUsingTermCap(img image.Image, path strin buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") - return buf.String(), nil + return filePreviewStyle.Render(buf.String()), nil } // IsKittyCapable checks if the terminal supports Kitty graphics protocol From 60b82b593cf963fe7bbaaa4fd1e87f35ad60f367 Mon Sep 17 00:00:00 2001 From: lazysegtree <59679977+lazysegtree@users.noreply.github.com> Date: Sun, 21 Dec 2025 15:47:00 +0530 Subject: [PATCH 8/9] fix: Update inline protocol for rendering --- src/pkg/file_preview/inline.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index 07b1991f9..616e4c51e 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -67,8 +67,6 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap( return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) } - var buf bytes.Buffer - slog.Debug("inline renderer starting", "path", path, "maxWidth", maxWidth, "maxHeight", maxHeight) // Calculate display dimensions in character cells @@ -97,14 +95,26 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap( slog.Debug("inline display dimensions", "widthCells", displayWidthCells, "heightCells", displayHeightCells) + var buf bytes.Buffer + line := strings.Repeat("-", maxWidth) + block := strings.Repeat(line+"\n", maxHeight-1) + line + //block := strings.Repeat("------------------\n", 10) + buf.WriteString(filePreviewStyle.Render(block)) + buf.WriteString("\x1b[s") + buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") + opts := rasterm.ItermImgOpts{ + Width: strconv.FormatInt(int64(displayWidthCells), 10), + Height: strconv.FormatInt(int64(displayHeightCells), 10), + IgnoreAspectRatio: true, + DisplayInline: true, + } // Use rasterm to write the image using iTerm2/WezTerm protocol - if err := rasterm.ItermWriteImage(&buf, img); err != nil { + if err := rasterm.ItermWriteImageWithOptions(&buf, img, opts); err != nil { return "", fmt.Errorf("failed to write image using rasterm: %w", err) } - - buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") - - return filePreviewStyle.Render(buf.String()), nil + buf.WriteString("\x1b[u") + slog.Debug("[TEMP]", "res", buf.String()) + return buf.String(), nil } // IsInlineCapable checks if the terminal supports inline image protocol From aa49bd602d647a5fb1121404b51185133b69e6ce Mon Sep 17 00:00:00 2001 From: Yorukot Date: Wed, 14 Jan 2026 01:30:20 +0800 Subject: [PATCH 9/9] feat: fix inline issue and make it works --- src/pkg/file_preview/inline.go | 125 +++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/src/pkg/file_preview/inline.go b/src/pkg/file_preview/inline.go index 616e4c51e..31ae737a2 100644 --- a/src/pkg/file_preview/inline.go +++ b/src/pkg/file_preview/inline.go @@ -2,10 +2,12 @@ package filepreview import ( "bytes" + "encoding/base64" "fmt" "image" "log/slog" "os" + "path/filepath" "strconv" "strings" @@ -14,7 +16,7 @@ import ( ) // isInlineCapable checks if the terminal supports inline image protocol (iTerm2, WezTerm, etc.) -func isInlineCapable() bool { +func (p *ImagePreviewer) IsInlineCapable() bool { isCapable := rasterm.IsItermCapable() // Additional detection for terminals that might not be detected by rasterm @@ -54,6 +56,16 @@ func (p *ImagePreviewer) ClearInlineImage() string { return "" } +func itermInlineSeq(name string, data []byte, width, height string) string { + nameEnc := base64.StdEncoding.EncodeToString([]byte(name)) + dataB64 := base64.StdEncoding.EncodeToString(data) + + return fmt.Sprintf( + "\033]1337;File=name=%s;inline=1;width=%s;height=%s;preserveAspectRatio=1:%s\a", + nameEnc, width, height, dataB64, + ) +} + // renderWithInlineUsingTermCap renders an image using inline image protocol func (p *ImagePreviewer) renderWithInlineUsingTermCap( img image.Image, @@ -62,62 +74,97 @@ func (p *ImagePreviewer) renderWithInlineUsingTermCap( sideAreaWidth int, filePreviewStyle lipgloss.Style, ) (string, error) { - // Validate dimensions if maxWidth <= 0 || maxHeight <= 0 { return "", fmt.Errorf("dimensions must be positive (maxWidth=%d, maxHeight=%d)", maxWidth, maxHeight) } slog.Debug("inline renderer starting", "path", path, "maxWidth", maxWidth, "maxHeight", maxHeight) - // Calculate display dimensions in character cells - imgRatio := float64(originalWidth) / float64(originalHeight) - termRatio := float64(maxWidth) / float64(maxHeight) + w, h := originalWidth, originalHeight + if (w <= 0 || h <= 0) && img != nil { + b := img.Bounds() + w, h = b.Dx(), b.Dy() + } + if w <= 0 || h <= 0 { + return "", fmt.Errorf("invalid original dimensions (w=%d, h=%d)", w, h) + } + + imgRatio := float64(w) / float64(h) - var displayWidthCells, displayHeightCells int + cellSize := p.terminalCap.GetTerminalCellSize() + ppc := cellSize.PixelsPerColumn + ppr := cellSize.PixelsPerRow - if imgRatio > termRatio { - // Image is wider, constrain by width - displayWidthCells = maxWidth - displayHeightCells = int(float64(maxWidth) / imgRatio) + usePixelRatio := ppc > 0 && ppr > 0 + + displayW, displayH := 1, 1 + + if usePixelRatio { + termRatio := float64(maxWidth*ppc) / float64(maxHeight*ppr) + + if imgRatio > termRatio { + displayW = maxWidth + widthPx := float64(displayW * ppc) + heightPx := widthPx / imgRatio + displayH = int(heightPx / float64(ppr)) + } else { + displayH = maxHeight + heightPx := float64(displayH * ppr) + widthPx := heightPx * imgRatio + displayW = int(widthPx / float64(ppc)) + } } else { - // Image is taller, constrain by height - displayHeightCells = maxHeight - displayWidthCells = int(float64(maxHeight) * imgRatio) + termRatio := float64(maxWidth) / float64(maxHeight) + + if imgRatio > termRatio { + displayW = maxWidth + displayH = int(float64(maxWidth) / imgRatio) + } else { + displayH = maxHeight + displayW = int(float64(maxHeight) * imgRatio) + } } - // Ensure minimum dimensions - if displayWidthCells < 1 { - displayWidthCells = 1 + // clamp + if displayW < 1 { + displayW = 1 } - if displayHeightCells < 1 { - displayHeightCells = 1 + if displayH < 1 { + displayH = 1 + } + if displayW > maxWidth { + displayW = maxWidth + } + if displayH > maxHeight { + displayH = maxHeight } - - slog.Debug("inline display dimensions", "widthCells", displayWidthCells, "heightCells", displayHeightCells) var buf bytes.Buffer - line := strings.Repeat("-", maxWidth) + line := strings.Repeat(" ", maxWidth) block := strings.Repeat(line+"\n", maxHeight-1) + line - //block := strings.Repeat("------------------\n", 10) buf.WriteString(filePreviewStyle.Render(block)) - buf.WriteString("\x1b[s") - buf.WriteString("\x1b[1;" + strconv.Itoa(sideAreaWidth) + "H") - opts := rasterm.ItermImgOpts{ - Width: strconv.FormatInt(int64(displayWidthCells), 10), - Height: strconv.FormatInt(int64(displayHeightCells), 10), - IgnoreAspectRatio: true, - DisplayInline: true, - } - // Use rasterm to write the image using iTerm2/WezTerm protocol - if err := rasterm.ItermWriteImageWithOptions(&buf, img, opts); err != nil { - return "", fmt.Errorf("failed to write image using rasterm: %w", err) + + var data []byte + var name string + + b, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("read image file: %w", err) } + data = b + name = filepath.Base(path) + + seq := itermInlineSeq( + name, + data, + strconv.Itoa(displayW), + strconv.Itoa(displayH), + ) + + buf.WriteString("\x1b[s") + buf.WriteString(fmt.Sprintf("\x1b[1;%dH", sideAreaWidth)) + buf.WriteString(seq) buf.WriteString("\x1b[u") - slog.Debug("[TEMP]", "res", buf.String()) - return buf.String(), nil -} -// IsInlineCapable checks if the terminal supports inline image protocol -func (p *ImagePreviewer) IsInlineCapable() bool { - return isInlineCapable() + return buf.String(), nil }