fix: file type handling on Enter - extensions checked before executable bit

- Reorder Category() to check known extensions (text, config, image,
  pdf, audio, video, archive) before the executable bit check.
  Fixes video/audio/image files with executable bit being opened in
  editor instead of system default application.
- Remove 'executable' from isEditableEntry() - executables are now
  launched via handleExecute() instead of opened in Neovim.
- Add handleExecute() method that runs executable files in the
  terminal via tea.ExecProcess.
- Update handleOpenSelected() to route: text/config -> editor,
  executable -> launch, everything else -> system default (xdg-open).
- Bump version to v0.2.1
This commit is contained in:
vrubelroman 2026-04-27 23:18:48 +03:00
parent c0df75c57e
commit df4df6b8f6
3 changed files with 35 additions and 17 deletions

View file

@ -36,13 +36,13 @@ 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.2.0 nix run github:vrubelroman/vcom?ref=v0.2.1
``` ```
Install into user profile: Install into user profile:
```bash ```bash
nix profile add github:vrubelroman/vcom?ref=v0.2.0 nix profile add github:vrubelroman/vcom?ref=v0.2.1
``` ```
The Nix package wraps `vcom` with `ueberzugpp` in `PATH`, so image preview works in non-`kitty` terminals out of the box. The Nix package wraps `vcom` with `ueberzugpp` in `PATH`, so image preview works in non-`kitty` terminals out of the box.
@ -52,7 +52,7 @@ The Nix package wraps `vcom` with `ueberzugpp` in `PATH`, so image preview works
Download and install the latest release: Download and install the latest release:
```bash ```bash
curl -sL https://github.com/vrubelroman/vcom/releases/download/v0.2.0/vcom_0.2.0_amd64.deb -o /tmp/vcom_0.2.0_amd64.deb curl -sL https://github.com/vrubelroman/vcom/releases/download/v0.2.1/vcom_0.2.1_amd64.deb -o /tmp/vcom_0.2.1_amd64.deb
sudo apt install /tmp/vcom_0.2.0_amd64.deb sudo apt install /tmp/vcom_0.2.0_amd64.deb
``` ```
@ -183,7 +183,7 @@ Built-in themes (use `T` to cycle or set `ui.theme` in config):
## Releases ## Releases
Pushing a tag like `v0.2.0` triggers GitHub Actions release workflow (`.github/workflows/release.yml`) which: Pushing a tag like `v0.2.1` triggers GitHub Actions release workflow (`.github/workflows/release.yml`) which:
- runs tests - runs tests
- vendors Go modules - vendors Go modules
@ -193,9 +193,9 @@ Pushing a tag like `v0.2.0` triggers GitHub Actions release workflow (`.github/w
Release artifacts: Release artifacts:
- `vcom-v0.2.0-x86_64-unknown-linux-gnu.tar.gz` - `vcom-v0.2.1-x86_64-unknown-linux-gnu.tar.gz`
- `vcom_0.2.0_amd64.deb` - `vcom_0.2.1_amd64.deb`
- `vcom-v0.2.0-checksums.txt` - `vcom-v0.2.1-checksums.txt`
## Notes ## Notes

View file

@ -116,14 +116,14 @@ func (e Entry) Category() string {
return "parent" return "parent"
case e.IsDir: case e.IsDir:
return "directory" return "directory"
case e.IsExecutable():
return "executable"
case hasExt(configExtensions, e.Extension): case hasExt(configExtensions, e.Extension):
return "config" return "config"
case hasExt(imageExtensions, e.Extension):
return "image"
case hasExt(textExtensions, e.Extension): case hasExt(textExtensions, e.Extension):
return "text" return "text"
case hasExt(textFilenames, strings.ToLower(e.Name)):
return "text"
case hasExt(imageExtensions, e.Extension):
return "image"
case hasExt(pdfExtensions, e.Extension): case hasExt(pdfExtensions, e.Extension):
return "pdf" return "pdf"
case hasExt(audioExtensions, e.Extension): case hasExt(audioExtensions, e.Extension):
@ -132,8 +132,8 @@ func (e Entry) Category() string {
return "video" return "video"
case hasExt(archiveExtensions, e.Extension): case hasExt(archiveExtensions, e.Extension):
return "archive" return "archive"
case hasExt(textFilenames, strings.ToLower(e.Name)): case e.IsExecutable():
return "text" return "executable"
default: default:
return "binary" return "binary"
} }

View file

@ -23,7 +23,7 @@ import (
"vcom/internal/theme" "vcom/internal/theme"
) )
const version = "v0.2.0" const version = "v0.2.1"
type modalKind int type modalKind int
@ -51,6 +51,7 @@ const (
opEdit opEdit
opView opView
opArchive opArchive
opExecute
) )
type pendingOperation struct { type pendingOperation struct {
@ -353,6 +354,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opView: case opView:
m.status = "Viewer closed" m.status = "Viewer closed"
return m, enableMouseCmd() return m, enableMouseCmd()
case opExecute:
m.status = "Executable closed"
return m, tea.Batch(m.loadPreviewCmd(), enableMouseCmd())
} }
leftSelection := selectedName(&m.left) leftSelection := selectedName(&m.left)
@ -1440,10 +1444,14 @@ func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
return m, m.loadPreviewCmd() return m, m.loadPreviewCmd()
} }
if isEditableEntry(selected) { switch selected.Category() {
case "text", "config":
return m.handleEdit() return m.handleEdit()
case "executable":
return m.handleExecute(selected)
default:
return m.handleOpenExternal()
} }
return m.handleOpenExternal()
} }
func (m *Model) goParent() error { func (m *Model) goParent() error {
@ -1769,6 +1777,16 @@ func (m *Model) handleEdit() (tea.Model, tea.Cmd) {
}) })
} }
func (m *Model) handleExecute(entry vfs.Entry) (tea.Model, tea.Cmd) {
m.cleanupImageOverlay()
cmd := exec.Command(entry.Path)
cmd.Dir = filepath.Dir(entry.Path)
m.status = fmt.Sprintf("Executing %s", entry.DisplayName())
return m, tea.ExecProcess(cmd, func(err error) tea.Msg {
return opMsg{kind: opExecute, sourcePath: entry.Path, err: err}
})
}
func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
if m.viewMode { if m.viewMode {
switch { switch {
@ -4242,7 +4260,7 @@ func paneIndexFromMouse(localY int, height int, pane *BrowserPane) (int, bool) {
func isEditableEntry(entry vfs.Entry) bool { func isEditableEntry(entry vfs.Entry) bool {
switch entry.Category() { switch entry.Category() {
case "text", "config", "executable": case "text", "config":
return true return true
default: default:
return false return false