trail-fm 1.12.1

A terminal-first workspace for navigating, inspecting and acting on the filesystem
Documentation

Trail

A terminal-first workspace for navigating, inspecting and acting on the filesystem — without leaving your shell.

CI Release License: MIT Rust 1.80+ Platforms

Trail is a keyboard-driven file manager for the terminal. It gives you a directory listing, a live preview of whatever is selected, and a command line for filesystem work — then leaves your shell in the directory you finished in.

It is not a fuzzy finder and not a file picker. Trail is built around one principle:

Navigation is the primary task. Every other feature exists to support it without interrupting the flow.

Why Trail?

There are good terminal file managers already. Trail is for you if these matter:

  • It replaces cd and ls, not your shell. Quit with q and your shell is in the directory you were looking at. Ctrl-c and it stays where it was. Wrappers ship for Bash, Zsh, Fish and PowerShell in every package.
  • Windows is a first-class platform, not a port. Native x86_64-pc-windows-msvc builds, PowerShell integration, the recycle bin for deletes, and keys that work on layouts where @ and \ need AltGr.
  • It never stutters. Git status, syntax highlighting, image decoding and filesystem watching all run off the UI thread, so holding j through a large repository stays smooth.
  • A real shell is one key away. The built-in terminal panel runs interactive shells under the file list, so you can browse and run commands side by side without leaving.
  • Strict, predictable config. One TOML file, validated on load: a typo is an error you see, not a setting that silently does nothing.

Contents — Features · Installation · Shell integration · Usage · Configuration · Plugins · Documentation · Development


Features

  • Three-panel interface — directory listing on the left, type-aware preview on the right, and a status bar reflecting mode, active filter, entry count and git branch. The panel borders carry the rest: the directory you are in and the sort order above the listing, the full path below it, so none of it can be covered by the command line.
  • Sort the way you are looking — by name, size, modification time or extension, reversed or not, directories grouped or mixed in. Per tab, so a downloads tab can sit in most-recent-first while the tab beside it keeps a source tree alphabetical, and the order in use is always shown on the panel border.
  • Details in the listing — optional size and modification time beside each name ([navigation] entry_details), budgeted so a narrow panel keeps readable names instead.
  • Type-aware previews — syntax-highlighted text (via syntect), directory summaries with file and directory counts, image metadata and dimensions, and binary metadata. Images render inline when the terminal speaks Kitty, iTerm2 or Sixel.
  • Git aware — current branch in the status bar and per-entry status badges (M modified, A added, D deleted, ? untracked, R renamed), computed off the UI thread.
  • Never blocks — git status, filesystem watching, syntax highlighting and image decoding all run on a tokio worker pool. The UI thread only renders, so navigation never stutters.
  • Live refresh — the current directory is watched with notify and refreshed automatically, debounced so that a git checkout triggers one repaint instead of a hundred.
  • Fuzzy search — incremental filtering of the current directory with nucleo, results reordered by match score as you type.
  • Command mode — mkdir, touch, rename, mv, cp, git, set, sort, bookmark, jump, plus arbitrary shell commands with !. Includes history and Tab completion for command verbs and destination paths, and the current directory stays visible while you type.
  • Tabs — several directories open at once, each with its own selection and history.
  • cd-on-exit — quit with q and your shell follows you to the directory you were browsing; cancel with Ctrl-c and it stays put.
  • Strictly validated config — TOML for theme, keybindings and editor, type-checked at load, with the same validation applied to runtime :set changes.
  • Lua plugins — an embedded Lua 5.4 runtime (mlua) with on_select, on_enter_dir and custom :plugin actions. A misbehaving plugin is caught and skipped, never taking down the TUI.

Platform support

Prebuilt binaries are published for every release:

Platform Targets
Linux x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu
macOS x86_64-apple-darwin, aarch64-apple-darwin (Apple Silicon)
Windows x86_64-pc-windows-msvc

Installation

Full guide: docs/installation.md. Quick paths:

Method Platform Command
Homebrew macOS brew tap WeedonSctt/trail && brew install trail
AUR Arch Linux paru -S trail
Scoop Windows scoop bucket add trail …
Install script Linux / macOS curl -fsSL … | sh
Install script Windows irm … | iex
crates.io any cargo install trail-fm --locked
From source any cargo install --git …

Every method except crates.io and from-source ships the shell wrappers needed for cd-on-exit.

Homebrew

brew tap WeedonSctt/trail
brew install trail

AUR

paru -S trail
# or: yay -S trail

Scoop

scoop bucket add trail https://github.com/WeedonSctt/trail-scoop-bucket
scoop install trail

Install script

Linux and macOS:

curl -fsSL https://github.com/WeedonSctt/trail/releases/latest/download/install.sh | sh

Windows:

irm https://github.com/WeedonSctt/trail/releases/latest/download/install.ps1 | iex

Both scripts verify the downloaded archive against its published SHA-256 digest before installing.

crates.io

Requires Rust 1.80 or newer. The crate is trail-fm — trail on crates.io is an unrelated library — and the command it installs is trail:

cargo install trail-fm --locked

--locked builds against the exact dependency versions each release was tested with.

Note: like every source install, this installs only the binary. Take the shell wrappers for cd-on-exit from the Releases page or from shell/.

From source

Requires Rust 1.80 or newer:

cargo install --git https://github.com/WeedonSctt/trail

Note: this installs only the binary. The shell wrappers that provide cd-on-exit are not included — take them from the Releases page or from shell/ in this repository.


Shell integration

Trail's cd-on-exit behaviour — your shell moving to the last directory Trail was browsing — requires a shell wrapper function. A child process cannot change its parent shell's working directory, so the wrapper is what actually calls cd; the binary alone cannot do it.

The wrapper ships in every release archive and in every package manager formula. Source it once per shell:

Bash — add to ~/.bashrc:

source /path/to/trail/shell/trail.bash

Zsh — add to ~/.zshrc:

source /path/to/trail/shell/trail.zsh

Fish — copy into fish's functions directory, which autoloads it:

cp /path/to/trail/shell/trail.fish ~/.config/fish/functions/trail.fish

PowerShell — add to your $PROFILE:

. "/path/to/trail/shell/trail.ps1"

Reload your shell afterwards (source ~/.bashrc, . $PROFILE, and so on).

How it works

The wrapper creates a temporary file and passes it to the binary via --cwd-file. On a normal exit (q), Trail writes its current directory to that file and the wrapper cds into it. On cancellation (Ctrl-c, or Esc-quit), Trail writes nothing, so your shell stays exactly where it started.


Usage

trail [<start-path>]
Flag Description
<start-path> Directory to open. Defaults to the current directory.
--config <path> Load a TOML config file. The path is remembered for later runs.
--no-config Ignore the remembered config for this run only.
--cwd-file <path> Write the final directory here on normal exit (used by the shell wrappers).
--version, --help Version and usage information.

Navigation mode

The default mode. Vim-style bindings, with arrow keys and standard keys as fallbacks.

Key Action
j / ↓ Move selection down
k / ↑ Move selection up
l / Enter / → Enter directory, or open file in your configured editor
h / Backspace / ← Go to parent directory
gg Jump to top
G Jump to bottom
u Back in navigation history
Ctrl-r Forward in navigation history
U Return from the last :jump, skipping every directory visited since
R Refresh the current directory
. Toggle hidden files
m Cycle the details column: none → size → modified → both

Sorting

s is a prefix; the second key picks the order. Size and time run largest-first and newest-first, the way ls -S and ls -t do, and sr flips whichever is active.

Key Action
sn Sort by name (A–Z)
ss Sort by size, largest first
st Sort by modification time, most recent first
se Sort by extension, then name
sr Flip the current order
sd Toggle grouping directories ahead of files

The order in use is shown on the right of the panel's top border, so it is never a guess, and the full path sits on the bottom border where the command line cannot cover it:

╭ my-project ─────────── size↓ ╮
│>  src/                      —│
│   README.md          17.06 kB│
│   Cargo.toml          3.22 kB│
╰ …\proj\it\util\trail ────────╯

The arrow is the direction values run as you read down the list, and mixed appears when directories are not grouped first. A path too deep for the panel is cut from the front — the end is the part that answers "where am I".

Sorting is per tab — a downloads tab can sit in st while the tab beside it keeps a source tree in sn — and a new tab inherits the order it was opened from. Set [navigation] sort_by in trail.toml for the order every session starts in, and [navigation] entry_details to show size or modification time in the listing. The details column stands down on a narrow panel rather than crushing names. [navigation] scroll_margin keeps a few rows between the selection and the panel's edges while you move, like vim's scrolloff (default 3; 0 for none).

Preview pane

The preview scrolls without moving the selection, so you can read down a file and keep browsing from where you were. Moving to another entry starts its preview at the top again.

Key Action
J / Shift-↓ Scroll preview down one line
K / Shift-↑ Scroll preview up one line
Ctrl-f Scroll preview down one page
Ctrl-b Scroll preview up one page
Shift-Home Jump to the start of the preview
Shift-End Jump to the end of the loaded preview

When the content is longer than the pane, the pane's bottom-right corner shows the visible range — 41–61/200. A trailing + means the file continues past what was loaded, and the bottom-left corner names the limit that stopped it — preview limit: first 2000 lines · max_lines. Raise that key to load more. An empty file shows (empty file).

File operations

Key Action
o Open selection with the OS default application
ya Copy absolute path to clipboard
yr Copy path relative to the launch directory to clipboard
yn Copy filename to clipboard
yc Copy content to clipboard — file text, or directory listing
dd Delete selection to the recycle bin — confirm with y/Enter, cancel with n/Esc. [general] delete_mode = "permanent" unlinks instead
Space Mark / unmark the selection (it stays put). Marks survive navigation and are shared by every tab
Esc Clear every mark (:unmark)
pp Copy the marked entries here — the marks stay
pm Move the marked entries here
v Visual mode: extend a range with j/k/G/gg, then Space or v to mark it, h/l to mark it and go, dd to delete it, Esc to abandon it

While anything is marked, :mv, :cp and dd act on the marks instead of the selection.

Tabs

Key Action
Ctrl-t New tab
Ctrl-w Close current tab
Tab Next tab
Shift-Tab Previous tab

Modes and exit

Key Action
/ Enter Search mode
: Enter Command mode
@ Repeat the last file operation — a :/! command, dd, pp or pm — on the current selection or marks
q Quit — your shell cd's here if the wrapper is sourced
Ctrl-c Quit without changing directory

Search mode

Press / and type to fuzzy-filter the current directory. Results reorder by match score as you type.

Key Action
any character Typed into the query - every letter is text here, including j and k
↓ / Ctrl-n, ↑ / Ctrl-p Move through results
Enter / → Open the match — enter a directory, or open a file in your editor
Esc Restore the full listing, keeping the cursor on the selected match
Backspace / Ctrl-h Delete the last character

Command mode

Press : for operations that take arguments.

Command Description
:mkdir <name> Create a directory in the current directory
:touch <name> Create an empty file
:rename <new-name> / :ren Rename the selected entry
:mv <dest> Move the selection (relative or absolute destination)
:cp <dest> Copy the selection
:mv <pattern> <dir> Move everything matching */? into a directory, e.g. :mv *.md notes
:cp <pattern> <dir> Copy everything matching */? into a directory
:mv @name\route\ A destination starting with @ is a bookmark, e.g. :mv @docs\next\; Tab completes the names
:unmark Clear every mark
:git <subcommand> Run a git subcommand, e.g. :git status --short
:set <key> <value> Change a setting at runtime — see Configuration
:config write Save this session's :set changes into trail.toml, comments intact
:bookmark [name] / :bm Bookmark the current directory (defaults to its base name)
:jump <name> / :j Jump to a saved bookmark
:plugin <name> [arg] Invoke an action registered by a Lua plugin
:term / :term new [profile] Show or hide the terminal panel / start another shell in it
:term close / :term <n> / :term max End the current shell / switch to shell n / fill the screen
!<command> Run any shell command in the current directory, e.g. !ls -la

Command mode keeps a history (↑ / ↓) and offers Tab completion for command verbs and for destination paths in :mv and :cp.

Shell commands and your editor take over the terminal while they run; Trail restores the interface and your full navigation state when they exit.

Terminal panel

Real, interactive shells in a panel under the file list, like VS Code's terminal — browse and use a shell at the same time. Each shell starts in the folder Trail is showing; after that, neither moves the other.

Key Action
Ctrl-. Show / hide the panel (starts a shell the first time)
F12 Move the keyboard between the file list and the shell
Alt-Shift-PageDown / Alt-Shift-PageUp Next / previous shell
Alt-PageUp / Alt-PageDown Scroll back through the shell's output

While the shell has the keyboard, every other key goes to it. Shells come from [[terminal.profile]] entries (PowerShell by default on Windows); quitting Trail asks first if one is still running a command. See the user guide.


Configuration

Trail runs on built-in defaults and does not scan ~/.config/trail/ on its own. You point it at a config file once, and it remembers:

trail --config ~/dotfiles/trail.toml   # loads it — and remembers the path
trail                                  # reloads ~/dotfiles/trail.toml
trail --no-config                      # built-in defaults, this run only
[general]
editor = "nvim"                # command used to open files
delete_mode = "trash"          # `dd` goes to the recycle bin; "permanent" to unlink
show_version = false           # show Trail's version on the preview border
terminal_title = "dir"         # tab title: "trail <dir>"; "path" for the full path, "off"
git_status_enabled = true
text_sync_threshold_kb = 256   # larger text files are highlighted off-thread
fs_watch_debounce_ms = 200

[navigation]
sort_by = "name"               # name | size | modified | extension — seeds the first tab
sort_reverse = false
dirs_first = true              # group directories ahead of files
entry_details = "none"         # none | size | modified | both
scroll_margin = 3              # rows kept between the selection and the panel edge

[theme]
background   = "#1a1b26"
directory    = "blue"
selection_bg = "dark_gray"

[keymap.navigation]
move_down = "n"                # rebind anything
quit = "ctrl-q"

[plugins]
enabled = ["bookmarks"]

The schema is strictly validated — an unknown key or a malformed colour is an error, not a warning. Settings can also be changed live with :set editor nvim, :set theme.directory blue or :set keymap.navigation.move_down n, using the same validation. Runtime changes apply immediately but are not written back to your file.

Full schema and config-resolution rules: docs/configuration_guide.md.


Plugins

Trail embeds a Lua 5.4 runtime. Plugins are loaded from your OS config directory (~/.config/trail/<name>.lua, or %APPDATA%\trail\config\<name>.lua on Windows) and enabled by name in trail.toml.

-- ~/.config/trail/activity_logger.lua
trail.on_enter_dir(function(dir)
  trail.log("entered " .. dir)
end)

trail.register_action("note", function(arg)
  local f = io.open(os.getenv("HOME") .. "/notes.txt", "a")
  f:write(os.date("%F %T ") .. arg .. "\n")
  f:close()
end)
-- invoke with:  :plugin note some text

The API surface is trail.log, trail.on_select, trail.on_enter_dir and trail.register_action. Runtime errors in plugin code are caught and logged — they never take down the TUI.

Full guide with a worked end-to-end example: docs/plugin_guide.md.


Documentation

Document Contents
User guide Every mode, key and command in detail
Installation All install methods, per-platform shell wrapper setup
Configuration guide Full TOML schema, :set keys, config resolution
Plugin guide Lua API, plugin load order, worked example
Product spec What Trail is, and the design philosophy behind it
Architecture Tech stack, UI thread / worker pool split, program logic
Coding standard Conventions every PR is held to
Release process · checklist How a release is cut and verified

Development

git clone https://github.com/WeedonSctt/trail
cd trail
cargo run                       # opens Trail in the current directory
cargo run -- ~/some/path        # or somewhere else

Environment setup, including optional tooling: docs/trail_development_environment_setup_guide.md.

Architecture at a glance

Trail is a single process split into two concurrency domains. The UI thread owns the terminal exclusively and must never block. An async worker pool handles everything with variable latency — git status, filesystem watching, syntax highlighting, image decoding — and reports back over an mpsc channel drained once per tick. Results are tagged with the selection they belong to, so navigating quickly past several entries never renders a stale preview.

Preview types are pluggable through the PreviewProvider trait: a new type (PDF, archive, …) is a new implementation registered at startup, not a change to the core loop. See docs/trail_architecture.md.

Contributing

The project follows the coding standard. Every PR must pass:

cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo doc --workspace --no-deps --all-features

CI runs the same four gates on every push and pull request.


License

MIT — see LICENSE.