From 6b23717572fd297d32ee37989968c728a520962e Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Fri, 24 Apr 2026 15:35:11 +0300 Subject: [PATCH] Add terminal image preview via chafa and release v0.1.3 updates --- README.md | 25 ++++++++++++++-------- flake.nix | 13 ++++++++++-- internal/fs/preview.go | 48 ++++++++++++++++++++++++++++++++++++------ internal/ui/model.go | 14 +++++++++++- 4 files changed, 82 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b80defb..fc4ad18 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,13 @@ You can force behavior in config: - `ui.icon_mode = "nerd"`: always use Nerd icons - `ui.icon_mode = "ascii"`: always use ASCII icons +How to make terminal use the installed Nerd Font: + +- GNOME Terminal: `Preferences -> Profile -> Text -> Custom font` -> choose `JetBrainsMono Nerd Font` +- Konsole: `Settings -> Edit Current Profile -> Appearance` -> choose a Nerd Font profile +- Alacritty: set `font.normal.family: "JetBrainsMono Nerd Font"` in `~/.config/alacritty/alacritty.yml` +- Kitty: set `font_family JetBrainsMono Nerd Font` in `~/.config/kitty/kitty.conf` + Preview mode (`F9` / `i`) temporarily replaces the inactive pane and shows: - directory listing preview @@ -57,21 +64,21 @@ go build -o vcom ./cmd/vcom Run directly from the flake: ```bash -nix run github:vrubelroman/vcom?ref=v0.1.2 +nix run github:vrubelroman/vcom?ref=v0.1.3 ``` Install into user profile: ```bash -nix profile add github:vrubelroman/vcom?ref=v0.1.2 +nix profile add github:vrubelroman/vcom?ref=v0.1.3 ``` ### Debian / Ubuntu -Download the release `.deb` for `v0.1.2`, then install: +Download the release `.deb` for `v0.1.3`, then install: ```bash -sudo apt install ./vcom_0.1.2_amd64.deb +sudo apt install ./vcom_0.1.3_amd64.deb ``` Install a Nerd Font (example): @@ -123,7 +130,7 @@ Built-in themes: ## Releases -Pushing a tag like `v0.1.2` triggers GitHub Actions release workflow (`.github/workflows/release.yml`) which: +Pushing a tag like `v0.1.3` triggers GitHub Actions release workflow (`.github/workflows/release.yml`) which: - runs tests - vendors Go modules @@ -133,13 +140,13 @@ Pushing a tag like `v0.1.2` triggers GitHub Actions release workflow (`.github/w Release artifacts: -- `vcom-v0.1.2-x86_64-unknown-linux-gnu.tar.gz` -- `vcom_0.1.2_amd64.deb` -- `vcom-v0.1.2-checksums.txt` +- `vcom-v0.1.3-x86_64-unknown-linux-gnu.tar.gz` +- `vcom_0.1.3_amd64.deb` +- `vcom-v0.1.3-checksums.txt` ## Notes - File creation time depends on filesystem/OS support; unavailable values are shown as `n/a`. -- Inline image rendering is intentionally disabled for now due to terminal compatibility differences. +- 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 0c87f73..ba292c2 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,9 @@ let pkgs = import nixpkgs { inherit system; }; lib = pkgs.lib; - package = pkgs.buildGoModule { + packageBase = pkgs.buildGoModule { pname = "vcom"; - version = "0.1.0"; + version = "0.1.3"; src = ./.; vendorHash = null; @@ -32,6 +32,15 @@ platforms = platforms.linux; }; }; + 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 66db83e..df62ff2 100644 --- a/internal/fs/preview.go +++ b/internal/fs/preview.go @@ -9,6 +9,7 @@ import ( _ "image/png" "io" "os" + "os/exec" "path/filepath" "regexp" "strconv" @@ -65,6 +66,8 @@ type PreviewOptions struct { HumanReadableSize bool ThemeName string UseNerdIcons bool + ImagePreviewWidth int + ImagePreviewHeight int } func BuildPreview(entry Entry, options PreviewOptions) Preview { @@ -119,12 +122,12 @@ func BuildPreview(entry Entry, options PreviewOptions) Preview { preview.Kind = PreviewKindImage preview.Metadata.ImageFormat = format preview.Metadata.ImageSize = dimensions - preview.Body = fmt.Sprintf( - "Image preview is metadata-only for now.\n\nFormat: %s\nDimensions: %s\nPath: %s", - format, - dimensions, - entry.Path, - ) + 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 { + preview.Body = inline + } preview.PlainBody = preview.Body return preview } @@ -377,6 +380,39 @@ 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 +} + func detectImage(data []byte) (string, string, bool) { cfg, format, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { diff --git a/internal/ui/model.go b/internal/ui/model.go index 18ef872..f985ccb 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -869,6 +869,12 @@ func (m Model) loadPreviewCmd() tea.Cmd { HumanReadableSize: m.cfg.Browser.HumanReadableSize, ThemeName: m.cfg.UI.Theme, UseNerdIcons: m.nerdIcons, + ImagePreviewWidth: max(m.previewModel.Width-2, 20), + ImagePreviewHeight: max(m.previewModel.Height-6, 8), + } + if m.viewMode { + options.ImagePreviewWidth = max(m.width-8, 20) + options.ImagePreviewHeight = max(m.bodyHeight()-8, 8) } return func() tea.Msg { @@ -970,6 +976,12 @@ func (m *Model) handleView() (tea.Model, tea.Cmd) { m.status = "Select a file to view" return m, nil } + if selected.Category() == "image" { + if _, err := exec.LookPath("chafa"); err != nil { + m.status = "Install `chafa` to view images in terminal" + return m, nil + } + } if m.viewMode { return m.exitViewMode() } @@ -1287,7 +1299,7 @@ func (m *Model) openHelpModal() { "", "View and Panels", " F9 / i toggle preview/info pane", - " F3 / v open read-only view mode", + " F3 / v text view mode or fullscreen image viewer", " F3 / Esc / q close view mode", " Ctrl+t toggle text selection mode in text preview", " Space calculate selected directory size",