vcom/internal/fs/entry.go
vrubelroman df4df6b8f6 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
2026-04-27 23:18:48 +03:00

150 lines
4.7 KiB
Go

package vfs
import (
"io/fs"
"path/filepath"
"strings"
"time"
)
var (
configExtensions = map[string]struct{}{
"toml": {}, "yaml": {}, "yml": {}, "json": {}, "jsonc": {}, "ini": {}, "conf": {},
"config": {}, "env": {}, "properties": {}, "xml": {}, "mod": {}, "sum": {}, "lock": {},
}
textExtensions = map[string]struct{}{
"txt": {}, "md": {}, "rst": {}, "go": {}, "rs": {}, "c": {}, "h": {}, "cpp": {}, "hpp": {},
"py": {}, "js": {}, "ts": {}, "tsx": {}, "jsx": {}, "java": {}, "kt": {}, "kts": {}, "swift": {},
"html": {}, "css": {}, "scss": {}, "sass": {}, "less": {}, "styl": {},
"sh": {}, "bash": {}, "zsh": {}, "fish": {}, "sql": {},
"log": {}, "csv": {}, "tsv": {},
"lua": {}, "rb": {}, "pl": {}, "pm": {}, "t": {}, "ps1": {}, "bat": {}, "cmd": {},
"vue": {}, "svelte": {}, "astro": {}, "ejs": {}, "hbs": {}, "pug": {}, "haml": {}, "php": {}, "twig": {},
"scala": {}, "groovy": {}, "clj": {}, "ex": {}, "exs": {}, "elm": {}, "hs": {}, "lisp": {}, "cl": {}, "rkt": {}, "scm": {}, "dart": {},
"tex": {}, "bib": {}, "sty": {}, "cls": {},
"gradle": {}, "cmake": {}, "mk": {}, "mak": {},
"asm": {}, "s": {}, "inc": {},
"patch": {}, "diff": {},
"proto": {}, "graphql": {}, "gql": {},
"tf": {}, "hcl": {},
"r": {}, "m": {}, "mm": {},
"nim": {}, "zig": {}, "odin": {}, "v": {}, "nix": {},
"cr": {}, "jl": {},
"erl": {}, "hrl": {},
}
// textFilenames lists common text files without a meaningful extension
// (like Makefile, Dockerfile, etc.) so they open in the editor.
textFilenames = map[string]struct{}{
"makefile": {}, "dockerfile": {}, "containerfile": {},
"readme": {}, "license": {}, "licence": {}, "copying": {}, "changelog": {}, "changes": {},
"todo": {}, "notes": {}, "authors": {}, "contributors": {}, "maintainers": {},
"procfile": {}, "gemfile": {}, "rakefile": {}, "snapfile": {}, "fastfile": {},
"cmakelists": {}, "justfile": {}, "taskfile": {},
"gitignore": {}, "gitattributes": {}, "gitmodules": {}, "gitkeep": {},
"gitconfig": {}, "git-blame-ignore-revs": {},
"editorconfig": {}, "envrc": {}, "hushlogin": {},
"xsession": {}, "xresources": {}, "xinitrc": {},
"bashrc": {}, "bash_profile": {}, "bash_logout": {},
"zshrc": {}, "zprofile": {}, "zlogin": {}, "zlogout": {},
"profile": {}, "inputrc": {}, "tmux.conf": {},
"npmrc": {}, "yarnrc": {}, "pnpmrc": {},
"eslintrc": {}, "prettierrc": {}, "babelrc": {},
"stylelintrc": {}, "commitlintrc": {},
"htaccess": {}, "htpasswd": {},
}
imageExtensions = map[string]struct{}{
"png": {}, "jpg": {}, "jpeg": {}, "gif": {}, "webp": {}, "bmp": {}, "svg": {}, "ico": {},
"avif": {}, "heic": {}, "heif": {}, "tiff": {}, "tif": {},
}
pdfExtensions = map[string]struct{}{
"pdf": {},
}
audioExtensions = map[string]struct{}{
"mp3": {}, "flac": {}, "ogg": {}, "opus": {}, "wav": {},
"aac": {}, "m4a": {}, "wma": {}, "dsf": {}, "ape": {},
}
videoExtensions = map[string]struct{}{
"mp4": {}, "mkv": {}, "mov": {}, "avi": {}, "webm": {},
"m4v": {}, "wmv": {}, "flv": {}, "ts": {}, "mts": {},
}
archiveExtensions = map[string]struct{}{
"zip": {}, "tar": {}, "gz": {}, "tgz": {}, "xz": {}, "bz2": {}, "7z": {}, "rar": {},
"zst": {}, "lz": {}, "lz4": {}, "lzma": {},
}
)
type Entry struct {
Name string
Path string
Extension string
Mode fs.FileMode
Size int64
ModifiedAt time.Time
CreatedAt time.Time
CreatedKnown bool
IsDir bool
IsParent bool
IsHidden bool
DirSizeKnown bool
}
func (e Entry) DisplayName() string {
if e.IsParent {
return ".."
}
if e.IsDir {
return e.Name + "/"
}
return e.Name
}
func (e Entry) IsFile() bool {
return !e.IsDir && !e.IsParent
}
func (e Entry) MatchKey() string {
return strings.ToLower(e.Name)
}
func (e Entry) IsExecutable() bool {
return !e.IsDir && !e.IsParent && e.Mode&0o111 != 0
}
func (e Entry) Category() string {
switch {
case e.IsParent:
return "parent"
case e.IsDir:
return "directory"
case hasExt(configExtensions, e.Extension):
return "config"
case hasExt(textExtensions, e.Extension):
return "text"
case hasExt(textFilenames, strings.ToLower(e.Name)):
return "text"
case hasExt(imageExtensions, e.Extension):
return "image"
case hasExt(pdfExtensions, e.Extension):
return "pdf"
case hasExt(audioExtensions, e.Extension):
return "audio"
case hasExt(videoExtensions, e.Extension):
return "video"
case hasExt(archiveExtensions, e.Extension):
return "archive"
case e.IsExecutable():
return "executable"
default:
return "binary"
}
}
func ext(name string) string {
value := strings.TrimPrefix(filepath.Ext(name), ".")
return strings.ToLower(value)
}
func hasExt(set map[string]struct{}, ext string) bool {
_, ok := set[strings.ToLower(ext)]
return ok
}