Trail
A terminal-first workspace for navigating, inspecting and acting on the filesystem — without leaving your shell.
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
cdandls, not your shell. Quit withqand your shell is in the directory you were looking at.Ctrl-cand 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-msvcbuilds, 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
jthrough 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
(
Mmodified,Aadded,Ddeleted,?untracked,Rrenamed), computed off the UI thread. - Never blocks — git status, filesystem watching, syntax highlighting and image decoding all
run on a
tokioworker pool. The UI thread only renders, so navigation never stutters. - Live refresh — the current directory is watched with
notifyand refreshed automatically, debounced so that agit checkouttriggers 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
qand your shell follows you to the directory you were browsing; cancel withCtrl-cand it stays put. - Strictly validated config — TOML for theme, keybindings and editor, type-checked at load,
with the same validation applied to runtime
:setchanges. - Lua plugins — an embedded Lua 5.4 runtime (
mlua) withon_select,on_enter_dirand custom:pluginactions. 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 |
| From source | any | cargo install --git … |
Every method except the from-source one ships the shell wrappers needed for cd-on-exit.
Homebrew
AUR
# or: yay -S trail
Scoop
scoop bucket add trail https://github.com/WeedonSctt/trail-scoop-bucket
scoop install trail
Install script
Linux and macOS:
|
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.
From source
Requires Rust 1.80 or newer:
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:
Zsh — add to ~/.zshrc:
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; raise [preview] max_lines to load more of it.
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 and move down. 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 |
Cancel and restore the full listing |
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:
[]
= "nvim" # command used to open files
= "trash" # `dd` goes to the recycle bin; "permanent" to unlink
= false # show Trail's version on the preview border
= true
= 256 # larger text files are highlighted off-thread
= 200
[]
= "name" # name | size | modified | extension — seeds the first tab
= false
= true # group directories ahead of files
= "none" # none | size | modified | both
= 3 # rows kept between the selection and the panel edge
[]
= "#1a1b26"
= "blue"
= "dark_gray"
[]
= "n" # rebind anything
= "ctrl-q"
[]
= ["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.
trail.
-- 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
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:
CI runs the same four gates on every push and pull request.
License
MIT — see LICENSE.