<div align="center">
# Trail
**A terminal-first workspace for navigating, inspecting and acting on the filesystem — without leaving your shell.**
[](https://github.com/WeedonSctt/trail/actions/workflows/ci.yml)
[](https://github.com/WeedonSctt/trail/releases/latest)
[](LICENSE)
[](https://www.rust-lang.org)
[](#platform-support)
</div>
<p align="center"><img src="docs/assets/demo.gif" alt="Trail browsing a project: previews follow the selection, a search jumps into a directory, a command runs in place, and quitting leaves the shell in that directory" width="800"></p>
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](#features) · [Installation](#installation) · [Shell integration](#shell-integration) ·
[Usage](#usage) · [Configuration](#configuration) · [Plugins](#plugins) · [Documentation](#documentation) ·
[Development](#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:
| 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](docs/installation.md). Quick paths:
| [Homebrew](#homebrew) | macOS | `brew tap WeedonSctt/trail && brew install trail` |
| [AUR](#aur) | Arch Linux | `paru -S trail` |
| [Scoop](#scoop) | Windows | `scoop bucket add trail …` |
| [Install script](#install-script) | Linux / macOS | `curl -fsSL … \| sh` |
| [Install script](#install-script) | Windows | `irm … \| iex` |
| [crates.io](#cratesio) | any | `cargo install trail-fm --locked` |
| [From source](#from-source) | any | `cargo install --git …` |
Every method except crates.io and from-source ships the shell wrappers needed for
[cd-on-exit](#shell-integration).
### Homebrew
```sh
brew tap WeedonSctt/trail
brew install trail
```
### AUR
```sh
paru -S trail
# or: yay -S trail
```
### Scoop
```pwsh
scoop bucket add trail https://github.com/WeedonSctt/trail-scoop-bucket
scoop install trail
```
### Install script
Linux and macOS:
```sh
Windows:
```pwsh
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`:
```sh
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](#shell-integration) from the
> [Releases page](https://github.com/WeedonSctt/trail/releases/latest) or from [`shell/`](shell/).
### From source
Requires Rust 1.80 or newer:
```sh
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](https://github.com/WeedonSctt/trail/releases/latest)
> or from [`shell/`](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`:
```bash
source /path/to/trail/shell/trail.bash
```
**Zsh** — add to `~/.zshrc`:
```zsh
source /path/to/trail/shell/trail.zsh
```
**Fish** — copy into fish's functions directory, which autoloads it:
```fish
cp /path/to/trail/shell/trail.fish ~/.config/fish/functions/trail.fish
```
**PowerShell** — add to your `$PROFILE`:
```pwsh
. "/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
`cd`s into it. On **cancellation** (`Ctrl-c`, or `Esc`-quit), Trail writes nothing, so your
shell stays exactly where it started.
---
## Usage
```
trail [<start-path>]
```
| `<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.
| `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.
| `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.
| `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**
| `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**
| `Ctrl-t` | New tab |
| `Ctrl-w` | Close current tab |
| `Tab` | Next tab |
| `Shift-Tab` | Previous tab |
**Modes and exit**
| `/` | 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.
| *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.
| `: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](#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.
| `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](docs/user_guide.md#5-terminal-panel).
---
## 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:
```sh
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
```
```toml
[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](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`.
```lua
-- ~/.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](docs/plugin_guide.md)**.
---
## Documentation
| [User guide](docs/user_guide.md) | Every mode, key and command in detail |
| [Installation](docs/installation.md) | All install methods, per-platform shell wrapper setup |
| [Configuration guide](docs/configuration_guide.md) | Full TOML schema, `:set` keys, config resolution |
| [Plugin guide](docs/plugin_guide.md) | Lua API, plugin load order, worked example |
| [Product spec](docs/trail.md) | What Trail is, and the design philosophy behind it |
| [Architecture](docs/trail_architecture.md) | Tech stack, UI thread / worker pool split, program logic |
| [Coding standard](docs/coding_standard.md) | Conventions every PR is held to |
| [Release process](docs/release_process.md) · [checklist](docs/release_checklist.md) | How a release is cut and verified |
---
## Development
```sh
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](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](docs/trail_architecture.md).
### Contributing
The project follows the [coding standard](docs/coding_standard.md). Every PR must pass:
```sh
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](LICENSE).