trail-fm 1.12.2

A terminal-first workspace for navigating, inspecting and acting on the filesystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
<div align="center">

# Trail

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

[![CI](https://github.com/WeedonSctt/trail/actions/workflows/ci.yml/badge.svg)](https://github.com/WeedonSctt/trail/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/WeedonSctt/trail?label=release)](https://github.com/WeedonSctt/trail/releases/latest)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust 1.80+](https://img.shields.io/badge/rust-1.80%2B-orange.svg)](https://www.rust-lang.org)
[![Platforms](https://img.shields.io/badge/platforms-linux%20%7C%20macos%20%7C%20windows-lightgrey.svg)](#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:

| 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](docs/installation.md). Quick paths:

| Method | Platform | Command |
|---|---|---|
| [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
curl -fsSL https://github.com/WeedonSctt/trail/releases/latest/download/install.sh | sh
```

Windows:

```pwsh
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`:

```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>]
```

| 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]#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](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

| Document | Contents |
|---|---|
| [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).