Add terminal image preview via chafa and release v0.1.3 updates

This commit is contained in:
vrubelroman 2026-04-24 15:35:11 +03:00
parent 6a518896b8
commit 6b23717572
4 changed files with 82 additions and 18 deletions

View file

@ -23,6 +23,13 @@ You can force behavior in config:
- `ui.icon_mode = "nerd"`: always use Nerd icons - `ui.icon_mode = "nerd"`: always use Nerd icons
- `ui.icon_mode = "ascii"`: always use ASCII 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: Preview mode (`F9` / `i`) temporarily replaces the inactive pane and shows:
- directory listing preview - directory listing preview
@ -57,21 +64,21 @@ go build -o vcom ./cmd/vcom
Run directly from the flake: Run directly from the flake:
```bash ```bash
nix run github:vrubelroman/vcom?ref=v0.1.2 nix run github:vrubelroman/vcom?ref=v0.1.3
``` ```
Install into user profile: Install into user profile:
```bash ```bash
nix profile add github:vrubelroman/vcom?ref=v0.1.2 nix profile add github:vrubelroman/vcom?ref=v0.1.3
``` ```
### Debian / Ubuntu ### Debian / Ubuntu
Download the release `.deb` for `v0.1.2`, then install: Download the release `.deb` for `v0.1.3`, then install:
```bash ```bash
sudo apt install ./vcom_0.1.2_amd64.deb sudo apt install ./vcom_0.1.3_amd64.deb
``` ```
Install a Nerd Font (example): Install a Nerd Font (example):
@ -123,7 +130,7 @@ Built-in themes:
## Releases ## 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 - runs tests
- vendors Go modules - vendors Go modules
@ -133,13 +140,13 @@ Pushing a tag like `v0.1.2` triggers GitHub Actions release workflow (`.github/w
Release artifacts: Release artifacts:
- `vcom-v0.1.2-x86_64-unknown-linux-gnu.tar.gz` - `vcom-v0.1.3-x86_64-unknown-linux-gnu.tar.gz`
- `vcom_0.1.2_amd64.deb` - `vcom_0.1.3_amd64.deb`
- `vcom-v0.1.2-checksums.txt` - `vcom-v0.1.3-checksums.txt`
## Notes ## Notes
- File creation time depends on filesystem/OS support; unavailable values are shown as `n/a`. - 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) Architecture notes: [docs/architecture.md](/home/vrubel/projects/vcom/docs/architecture.md)

View file

@ -11,9 +11,9 @@
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib; lib = pkgs.lib;
package = pkgs.buildGoModule { packageBase = pkgs.buildGoModule {
pname = "vcom"; pname = "vcom";
version = "0.1.0"; version = "0.1.3";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -32,6 +32,15 @@
platforms = platforms.linux; 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 { in {
packages.default = package; packages.default = package;

View file

@ -9,6 +9,7 @@ import (
_ "image/png" _ "image/png"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
@ -65,6 +66,8 @@ type PreviewOptions struct {
HumanReadableSize bool HumanReadableSize bool
ThemeName string ThemeName string
UseNerdIcons bool UseNerdIcons bool
ImagePreviewWidth int
ImagePreviewHeight int
} }
func BuildPreview(entry Entry, options PreviewOptions) Preview { func BuildPreview(entry Entry, options PreviewOptions) Preview {
@ -119,12 +122,12 @@ func BuildPreview(entry Entry, options PreviewOptions) Preview {
preview.Kind = PreviewKindImage preview.Kind = PreviewKindImage
preview.Metadata.ImageFormat = format preview.Metadata.ImageFormat = format
preview.Metadata.ImageSize = dimensions preview.Metadata.ImageSize = dimensions
preview.Body = fmt.Sprintf( inline := renderImageInlinePreview(entry.Path, options.ImagePreviewWidth, options.ImagePreviewHeight)
"Image preview is metadata-only for now.\n\nFormat: %s\nDimensions: %s\nPath: %s", if inline == "" {
format, preview.Body = "Image preview unavailable.\n\nInstall `chafa` for inline preview in info pane."
dimensions, } else {
entry.Path, preview.Body = inline
) }
preview.PlainBody = preview.Body preview.PlainBody = preview.Body
return preview 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) { func detectImage(data []byte) (string, string, bool) {
cfg, format, err := image.DecodeConfig(bytes.NewReader(data)) cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil { if err != nil {

View file

@ -869,6 +869,12 @@ func (m Model) loadPreviewCmd() tea.Cmd {
HumanReadableSize: m.cfg.Browser.HumanReadableSize, HumanReadableSize: m.cfg.Browser.HumanReadableSize,
ThemeName: m.cfg.UI.Theme, ThemeName: m.cfg.UI.Theme,
UseNerdIcons: m.nerdIcons, 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 { return func() tea.Msg {
@ -970,6 +976,12 @@ func (m *Model) handleView() (tea.Model, tea.Cmd) {
m.status = "Select a file to view" m.status = "Select a file to view"
return m, nil 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 { if m.viewMode {
return m.exitViewMode() return m.exitViewMode()
} }
@ -1287,7 +1299,7 @@ func (m *Model) openHelpModal() {
"", "",
"View and Panels", "View and Panels",
" F9 / i toggle preview/info pane", " 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", " F3 / Esc / q close view mode",
" Ctrl+t toggle text selection mode in text preview", " Ctrl+t toggle text selection mode in text preview",
" Space calculate selected directory size", " Space calculate selected directory size",