From 912de45e19d64c1d3a98b33eea14e46551ff8571 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Fri, 24 Apr 2026 22:09:54 +0300 Subject: [PATCH] Refine image preview integration --- README.md | 1 - flake.nix | 5 ---- internal/fs/preview.go | 36 ++------------------------- internal/ui/image_overlay.go | 2 +- internal/ui/model.go | 47 ++++++++++++++++++++++++++++++------ 5 files changed, 43 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 40e2c01..256a739 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,5 @@ Release artifacts: ## Notes - File creation time depends on filesystem/OS support; unavailable values are shown as `n/a`. -- Image preview in info pane (`F9`) and image full-screen view (`F3`) use `chafa`. Architecture notes: [docs/architecture.md](/home/vrubel/projects/vcom/docs/architecture.md) diff --git a/flake.nix b/flake.nix index 512477e..5c38d98 100644 --- a/flake.nix +++ b/flake.nix @@ -35,11 +35,6 @@ package = pkgs.symlinkJoin { name = "vcom"; paths = [ packageBase ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram "$out/bin/vcom" \ - --prefix PATH : "${lib.makeBinPath [ pkgs.chafa ]}" - ''; }; in { packages.default = package; diff --git a/internal/fs/preview.go b/internal/fs/preview.go index df62ff2..caa32d4 100644 --- a/internal/fs/preview.go +++ b/internal/fs/preview.go @@ -9,7 +9,6 @@ import ( _ "image/png" "io" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -123,9 +122,7 @@ func BuildPreview(entry Entry, options PreviewOptions) Preview { preview.Metadata.ImageFormat = format preview.Metadata.ImageSize = dimensions inline := renderImageInlinePreview(entry.Path, options.ImagePreviewWidth, options.ImagePreviewHeight) - if inline == "" { - preview.Body = "Image preview unavailable.\n\nInstall `chafa` for inline preview in info pane." - } else { + if inline != "" { preview.Body = inline } preview.PlainBody = preview.Body @@ -381,36 +378,7 @@ func previewIcon(entry Entry, useNerdIcons bool) string { } func renderImageInlinePreview(path string, width int, height int) string { - if width < 20 { - width = 20 - } - if height < 8 { - height = 8 - } - - if _, err := exec.LookPath("chafa"); err != nil { - return "" - } - - cmd := exec.Command( - "chafa", - "--format=symbols", - "--symbols=vhalf", - "--animate=off", - "--fg-only", - "--size", fmt.Sprintf("%dx%d", width, height), - path, - ) - out, err := cmd.Output() - if err != nil { - return "" - } - - view := strings.TrimSpace(string(out)) - if view == "" { - return "" - } - return view + return "" } func detectImage(data []byte) (string, string, bool) { diff --git a/internal/ui/image_overlay.go b/internal/ui/image_overlay.go index d25699b..5b271b4 100644 --- a/internal/ui/image_overlay.go +++ b/internal/ui/image_overlay.go @@ -89,7 +89,7 @@ func (m *imageOverlayManager) backendOutput() string { case os.Getenv("DISPLAY") != "": order = append(order, "x11") } - order = append(order, "wayland", "x11", "sixel", "kitty", "chafa") + order = append(order, "wayland", "x11", "sixel", "kitty") unique := make([]string, 0, len(order)) for _, backend := range order { diff --git a/internal/ui/model.go b/internal/ui/model.go index efc6aab..e75de28 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -3,6 +3,7 @@ package ui import ( "context" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -111,6 +112,10 @@ type copyDoneMsg struct { } type dismissNoticeMsg struct{} +type externalOpenMsg struct { + path string + err error +} type copyJobState struct { id int @@ -412,6 +417,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil + case externalOpenMsg: + if msg.err != nil { + m.status = fmt.Sprintf("Open failed: %v", msg.err) + return m, nil + } + m.status = fmt.Sprintf("Opened %s", filepath.Base(msg.path)) + return m, nil + case tea.KeyMsg: if m.modal.kind != modalNone { return m.handleModalKey(msg) @@ -1026,9 +1039,7 @@ func (m *Model) handleOpenExternal() (tea.Model, tea.Cmd) { m.cleanupImageOverlay() m.status = fmt.Sprintf("Opening %s with %s", selected.DisplayName(), name) - return m, tea.ExecProcess(command, func(err error) tea.Msg { - return opMsg{kind: opView, sourcePath: selected.Path, err: err} - }) + return m, startExternalOpenCmd(command, selected.Path) } func (m *Model) handleEdit() (tea.Model, tea.Cmd) { @@ -2598,6 +2609,18 @@ func externalCommandFromEnv(envVars []string, fallbacks []string, path string) ( return exec.Command(parts[0], args...), filepath.Base(parts[0]), nil } +func startExternalOpenCmd(command *exec.Cmd, path string) tea.Cmd { + return func() tea.Msg { + command.Stdin = nil + command.Stdout = io.Discard + command.Stderr = io.Discard + if err := command.Start(); err != nil { + return externalOpenMsg{path: path, err: err} + } + return externalOpenMsg{path: path} + } +} + func enableMouseCmd() tea.Cmd { return func() tea.Msg { return tea.EnableMouseCellMotion() @@ -2753,11 +2776,21 @@ func (m Model) syncImageOverlay(leftWidth int, previewWidth int, bodyHeight int) if m.active == PaneLeft { startX = leftWidth + m.cfg.UI.PaneGap } + innerWidth := max(previewWidth-2, 1) + metaHeight := 0 + if m.cfg.Preview.ShowMetadata { + metaHeight = lipgloss.Height(renderMetadata(m.previewData.Metadata, m.palette, innerWidth)) + } + titleHeight := 1 + topInset := 1 + contentBorder := 1 + safetyGap := 1 + contentTop := topInset + titleHeight + metaHeight + contentBorder + safetyGap rect = overlayRect{ - x: startX + 2, - y: 9, - width: max(previewWidth-4, 1), - height: max(bodyHeight-11, 1), + x: startX + 3, + y: contentTop, + width: max(previewWidth-6, 1), + height: max(bodyHeight-contentTop-2, 1), } } else { m.overlay.hide()