tui-file-explorer
A keyboard-driven, two-pane file manager widget for Ratatui.
Use it as an embeddable library widget or run it as the standalone tfe CLI tool.
Preview
Navigation · Search · Sort — cargo run --example basic

File Operations — copy, cut, paste, delete across two panes · cargo run --bin tfe

27 Live Themes — cargo run --example theme_switcher

Features
- 🗂️ Two-pane layout — independent left and right explorer panes,
Tabto switch focus - 📋 File operations — copy (
y), cut (x), paste (p), and delete (d) between panes - 🔍 Incremental search — press
/to filter entries live as you type - 🔃 Sort modes — cycle
Name → Size ↓ → Extensionwiths - 🎛️ Extension filter — only matching files are selectable; dirs are always navigable
- 👁️ Toggle hidden dot-file visibility with
. - ⌨️ Full keyboard navigation: Miller-columns
←/→(ascend/descend), vim keys (h/j/k/l),↑/↓/j/k,PgUp/PgDn,g/G—→on a file moves down, never exits the TUI - 🎨 27 named themes — Catppuccin, Dracula, Nord, Tokyo Night, Kanagawa, Gruvbox, and more
- 🎛️ Live theme panel — press
tto open a side panel,[/]to cycle themes - 🔧 Fluent builder API for ergonomic embedding — both
FileExplorerandDualPane - 📦
DualPanelibrary widget — drop a full two-pane explorer into any Ratatui app with one struct - 🖥️
cdon exit — dismiss withEsc/qand your terminal jumps to the directory you were browsing; one-time setup withtfe --init <shell>(bash, zsh, fish, powershell) - ✅ Lean library — only
ratatui+crosstermrequired (clapis opt-out)
Stats
| Metric | Value |
|---|---|
| Library dependencies | 2 (ratatui, crossterm) |
| Named colour themes | 27 |
| Sort modes | 3 (Name, Size ↓, Extension) |
| File operations | 4 (copy, cut, paste, delete) |
| Key bindings | 20+ |
| File-type icons | 50+ extensions mapped |
| Public API surface | 10 types, 6 free functions |
| Unit tests | 263 |
Installation
As a library
[]
= "0.2"
= "0.30"
Library-only (no clap-powered CLI binary):
[]
= { = "0.2", = false }
As a CLI tool
Installs the tfe binary onto your PATH.
Quick Start
Single-pane
use ;
use ;
// 1. Create once — e.g. in your App::new
let mut explorer = builder
.allow_extension
.allow_extension
.sort_mode // largest files first
.show_hidden
.build;
// 2. Inside Terminal::draw:
// render(&mut explorer, frame, frame.area());
// 3. Inside your key-handler — ↑/↓/←/→ scroll the list, Enter confirms:
let key = new;
match explorer.handle_key
Dual-pane
use ;
use ;
use PathBuf;
// 1. Create once — left pane defaults to cwd; right pane can differ.
let mut dual = builder
.right_dir
.show_hidden
.build;
let theme = default;
// 2. Inside Terminal::draw:
// render_dual_pane_themed(&mut dual, frame, frame.area(), &theme);
// 3. Inside your key-handler:
let key = new;
match dual.handle_key
Key Bindings
Navigation
| Key | Action |
|---|---|
↑ / k |
Move cursor up |
↓ / j |
Move cursor down |
PgUp |
Jump up 10 rows |
PgDn |
Jump down 10 rows |
g / Home |
Jump to top |
G / End |
Jump to bottom |
→ / l / Enter |
Descend into directory; on a file → moves cursor down, l/Enter confirm (exits TUI) |
← / h / Backspace |
Ascend to parent directory |
Tab |
Switch active pane (left ↔ right) |
w |
Toggle two-pane ↔ single-pane layout |
Explorer actions
| Key | Action |
|---|---|
/ |
Activate incremental search |
s |
Cycle sort mode (Name → Size ↓ → Extension) |
. |
Toggle hidden (dot-file) entries |
Esc |
Clear search (if active), then dismiss |
q |
Dismiss when search is not active |
File operations
| Key | Action |
|---|---|
y |
Yank — mark highlighted entry for copy |
x |
Cut — mark highlighted entry for move |
p |
Paste — copy/move clipboard into the other pane's directory |
d |
Delete — remove highlighted entry (asks for confirmation) |
Layout & theme controls
| Key | Action |
|---|---|
w |
Toggle two-pane ↔ single-pane layout |
t |
Next theme |
T |
Toggle theme panel (right sidebar) |
[ |
Previous theme |
Search mode (after pressing /)
| Key | Action |
|---|---|
| Any character | Append to query — list filters live |
Backspace |
Remove last character; empty query exits search |
Esc |
Clear query and exit search |
↑ / ↓ / j / k |
Scroll the filtered results |
→ / Enter / l |
Descend into directory or confirm/navigate entry |
← / Backspace / h |
Ascend to parent directory |
Two-Pane File Manager
The tfe binary opens a split-screen file manager with a left and right pane.
┌─── Left Pane (active) ──────────┬─── Right Pane ───────────────────┐
│ 📁 ~/projects/tui-file-explorer │ 📁 ~/projects/tui-file-explorer │
├─────────────────────────────────┤──────────────────────────────────┤
│ ▶ 📁 src/ │ 📁 src/ │
│ 📁 examples/ │ 📁 examples/ │
│ 📄 Cargo.toml │ 📄 Cargo.toml │
│ 📄 README.md │ 📄 README.md │
├─────────────────────────────────┴──────────────────────────────────┤
│ 📋 Copy: main.rs Tab pane y copy x cut p paste d del │
└────────────────────────────────────────────────────────────────────┘
- The active pane renders with your full theme accent; the inactive pane dims its borders so focus is always clear at a glance
- Press
Tabto switch which pane has keyboard focus - Arrow keys (
←/→) scroll the cursor up/down without entering or exiting a directory — useEnter/lto descend andBackspace/hto ascend - Press
wto collapse to a single pane — the hidden pane keeps its state and reappears when you presswagain - Press
t/[to cycle themes forward / backward; pressTto open the theme panel - Each pane navigates independently — scroll to different directories and use one as source, one as destination
File Operations (Copy / Cut / Paste / Delete)
The classic Midnight Commander source-to-destination workflow:
- Navigate to the file you want in the active pane
yto yank (copy) orxto cut it — the status bar confirms what is in the clipboardTabto switch to the other pane and navigate to the destination directorypto paste — the file is copied (or moved for cut) into that pane's directory
Active pane: ~/projects/src/ Other pane: ~/backup/
▶ main.rs ← press y ← press p here → main.rs appears
If the destination file already exists a confirmation modal appears asking whether to overwrite. Delete (d) also shows a modal before any data is removed.
All file operations support directories — copy and delete both recurse automatically.
Incremental Search
Press / to activate search mode. The footer transforms into a live input bar showing your query. Entries are filtered in real-time using a case-insensitive substring match on the file name.
// Inspect search state programmatically:
println!;
println!;
Behaviour details:
Backspaceon a non-empty query pops the last characterBackspaceon an empty query deactivates search without dismissingEscclears the query and deactivates search — a secondEsc(when search is already inactive) dismisses the explorer entirely- Search is automatically cleared when navigating into a subdirectory or ascending to a parent
Sort Modes
Press s to cycle through three sort modes, or set one directly:
use ;
let mut explorer = new;
explorer.set_sort_mode; // largest files first
println!; // "size ↓"
| Mode | Trigger | Description |
|---|---|---|
SortMode::Name |
s (1st press) |
Alphabetical A → Z — the default |
SortMode::SizeDesc |
s (2nd press) |
Largest files first |
SortMode::Extension |
s (3rd press) |
Grouped by extension, then by name |
Directories always sort alphabetically among themselves regardless of the active mode. The current sort mode is shown in the footer status panel at all times.
Extension Filtering
Only files whose extension matches the filter are selectable. Directories are always shown and always navigable, regardless of the filter.
use FileExplorer;
// Builder API (preferred)
let explorer = builder
.allow_extension
.allow_extension
.build;
// Or replace the filter at runtime — reloads the listing immediately
explorer.set_extension_filter;
// Pass an empty filter to allow all files
explorer.set_extension_filter;
Non-matching files are shown dimmed so the directory structure remains visible. Attempting to confirm a non-matching file shows a status message in the footer.
Builder API
FileExplorer::builder
FileExplorer::builder gives a fluent, chainable construction API:
use ;
let explorer = builder
.allow_extension // add one extension at a time
.allow_extension
.show_hidden // show dot-files on startup
.sort_mode // initial sort order
.build;
Or pass the full filter list at once:
let explorer = builder
.extension_filter
.build;
The classic FileExplorer::new(dir, filter) constructor is still available and fully backwards-compatible.
DualPane::builder
DualPane::builder mirrors the single-pane builder and adds dual-pane-specific options:
use ;
use PathBuf;
let dual = builder
.right_dir // independent right-pane directory
.allow_extension // applied to both panes
.allow_extension
.show_hidden // both panes
.sort_mode // both panes
.single_pane // start in dual-pane mode (default)
.build;
Once built, pane directories, sort mode, and hidden-file visibility can still be changed independently on dual.left and dual.right at runtime.
| Builder method | Effect |
|---|---|
.right_dir(path) |
Independent starting directory for the right pane |
.allow_extension(ext) |
Append one extension to the shared filter |
.extension_filter(vec) |
Replace the shared filter entirely |
.show_hidden(bool) |
Hidden-file visibility for both panes |
.sort_mode(mode) |
Initial sort order for both panes |
.single_pane(bool) |
Start in single-pane mode (default false) |
Theming
Every colour used by the widget is overridable through the Theme struct. Pass a Theme to render_themed instead of render:
use ;
use Color;
let theme = default
.brand // widget title
.accent // borders & current path
.dir // directory names & icons
.sel_bg // highlighted-row background
.success // status bar & selectable files
.match_file;
terminal.draw?;
Named presets (27 included)
All presets are available as associated constructors on Theme:
use Theme;
let t = dracula;
let t = nord;
let t = catppuccin_mocha;
let t = catppuccin_latte;
let t = tokyo_night;
let t = tokyo_night_storm;
let t = gruvbox_dark;
let t = kanagawa_wave;
let t = kanagawa_dragon;
let t = moonfly;
let t = oxocarbon;
let t = grape;
let t = ocean;
let t = neon;
// Iterate the full catalogue (name, description, theme):
for in all_presets
Full preset list:
Default · Dracula · Nord · Solarized Dark · Solarized Light · Gruvbox Dark · Gruvbox Light · Catppuccin Latte · Catppuccin Frappé · Catppuccin Macchiato · Catppuccin Mocha · Tokyo Night · Tokyo Night Storm · Tokyo Night Light · Kanagawa Wave · Kanagawa Dragon · Kanagawa Lotus · Moonfly · Nightfly · Oxocarbon · Grape · Ocean · Sunset · Forest · Rose · Mono · Neon
Palette constants
The default colours are exported as pub const values for use alongside complementary widgets:
| Constant | Default | Used for |
|---|---|---|
C_BRAND |
Rgb(255, 100, 30) |
Widget title |
C_ACCENT |
Rgb(80, 200, 255) |
Borders, current-path text |
C_SUCCESS |
Rgb(80, 220, 120) |
Selectable files, status bar |
C_DIM |
Rgb(120, 120, 130) |
Hints, non-matching files |
C_FG |
White |
Default foreground |
C_SEL_BG |
Rgb(40, 60, 80) |
Selected-row background |
C_DIR |
Rgb(255, 210, 80) |
Directory names & icons |
C_MATCH |
Rgb(80, 220, 120) |
Extension-matched file names |
Examples
basic
examples/basic.rs — a fully self-contained Ratatui app demonstrating:
- Builder API and full event loop
- All
ExplorerOutcomevariants - Custom
Theme - Optional CLI extension-filter arguments
# Browse everything
# Only .rs and .toml files are selectable
dual_pane
examples/dual_pane.rs — a fully self-contained dual-pane Ratatui app built entirely on the library API (no binary code):
DualPane::builderwith an optional independent right-pane directoryrender_dual_pane_themedfor rendering both panes in one call- All
DualPaneOutcomevariants - Status bar showing active pane and current layout mode
| Key | Action |
|---|---|
Tab |
Switch focus left ↔ right |
w |
Toggle single-pane / dual-pane mode |
↑ / ↓ / j / k |
Move cursor up / down |
← / → |
Scroll cursor up / down (no navigation side-effects) |
Enter / l |
Descend into directory or confirm file |
Backspace / h |
Ascend to parent directory |
. |
Toggle hidden files |
/ |
Incremental search |
s |
Cycle sort mode |
Esc / q |
Quit |
# Both panes start in the current directory
# Left pane starts in cwd, right pane starts in /tmp
theme_switcher
examples/theme_switcher.rs — live theme switching without restarting, with a sidebar showing the full theme catalogue.
| Key | Action |
|---|---|
Tab |
Next theme |
Shift+Tab |
Previous theme |
↑ / ↓ / j / k |
Move cursor up / down |
← / → |
Scroll cursor up / down (no navigation side-effects) |
Enter / l |
Descend into directory or confirm file |
Backspace / h |
Ascend to parent directory |
. |
Toggle hidden files |
/ |
Search |
s |
Cycle sort mode |
Esc / q |
Quit |
Example Demos
Navigation & Search
Run: cargo run --example basic

Demonstrates directory navigation, incremental search (/), sort mode cycling (s), hidden-file toggle (.), directory descent and ascent, and file selection.
Incremental Search
Run: cargo run --example basic then press /

Shows the full search lifecycle: activate with /, type to filter live, use Backspace to narrow or clear, and Esc to cancel without dismissing the explorer.
Sort Modes
Run: cargo run --example basic then press s

Demonstrates Name → Size ↓ → Extension → Name cycling, combined with search, and sort persistence across directory navigation.
Extension Filter
Run: cargo run --example basic -- rs toml

Only .rs and .toml files are selectable; all other files appear dimmed. The footer reflects the active filter at all times.
File Operations
Run: cargo run --bin tfe

Demonstrates the full copy then paste and cut (move) then paste workflows across both panes, followed by a delete with confirmation modal. The clipboard status bar updates live after each y / x / p keystroke, and an overwrite-prompt appears when the destination file already exists.
Theme Switcher
Run: cargo run --example theme_switcher

Live theme cycling through all 27 named palettes with a real-time sidebar showing the catalogue and the active theme's description.
Pane Toggle
Run: cargo run --bin tfe

Demonstrates the three layout controls in sequence:
Tab— switch keyboard focus between the left and right pane; each pane navigates independently so you can be in different directories at the same timew— collapse to single-pane (the active pane expands to full width) and back to two-pane (the hidden pane reappears with its cursor position preserved)T— open and close the theme-picker sidebar; uset/[to cycle themes while the panel is open; both panes remain fully navigable with the panel visible
Demo Quick Reference
| Demo | Command | Highlights |
|---|---|---|
| Navigation + Search | cargo run --example basic |
All key bindings, search, sort, selection |
| Extension filter | cargo run --example basic -- rs toml |
Dimmed non-matching files, footer status |
| Incremental search | cargo run --example basic → / |
Live filtering, backspace, Esc behaviour |
| Sort modes | cargo run --example basic → s |
Three modes, combined with search |
| Dual-pane (library) | cargo run --example dual_pane |
DualPane widget, Tab focus, w toggle, status bar |
| Dual-pane (right dir) | cargo run --example dual_pane -- /tmp |
Independent left/right starting directories |
| File operations | cargo run --bin tfe |
Copy, cut, paste, delete, overwrite modal |
| Theme switcher | cargo run --example theme_switcher |
27 live themes, sidebar catalogue |
| Pane toggle | cargo run --bin tfe |
Tab focus-switch, w single/two-pane, T theme panel |
CLI Usage
| Flag | Description |
|---|---|
[PATH] |
Starting directory (default: current directory) |
-e, --ext <EXT> |
Only select files with this extension (repeatable) |
-H, --hidden |
Show hidden dot-files on startup |
-t, --theme <THEME> |
Colour theme — see --list-themes |
--list-themes |
Print all 27 available themes and exit |
--show-themes |
Open the theme panel on startup (T toggles it at runtime) |
--single-pane |
Start in single-pane mode (default is two-pane; toggle at runtime with w) |
--print-dir |
Print the parent directory of the selected file instead of the full path |
-0, --null |
Terminate output with a NUL byte (for xargs -0) |
--cd |
Enable cd-on-exit: on dismiss, print the active pane's directory to stdout (persisted) |
--no-cd |
Disable cd-on-exit (persisted) |
--init <SHELL> |
Install the shell wrapper for cd-on-exit and exit. Shells: bash, zsh, fish, powershell |
-h, --help |
Show help |
-V, --version |
Show version |
Exit codes
| Code | Meaning |
|---|---|
0 |
File selected — path printed to stdout |
0 |
Dismissed with --cd enabled — active pane's directory printed to stdout |
1 |
Dismissed without --cd — nothing printed |
2 |
Bad arguments or I/O error |
Shell integration (cd on exit)
The killer feature: press Esc or q to dismiss tfe and your terminal
automatically cds to whichever directory you were browsing.
Works on macOS, Linux, and Windows.
This is opt-in — two one-time steps to enable:
Step 1 — enable the feature (persisted across sessions):
Run tfe --no-cd at any time to turn it off again.
Step 2 — install the shell wrapper (once per shell):
--init appends the wrapper to your rc file (creating it and any missing
parent directories if needed), is idempotent — running it twice will not
duplicate the snippet — and tells you where it wrote. Then restart your
shell or source the rc file as instructed.
How it works:
| Situation | stdout | exit code |
|---|---|---|
File selected (Enter / l) |
selected file path | 0 |
Dismissed (Esc / q) + --cd enabled |
active pane's directory | 0 |
Dismissed + --cd disabled (default) |
(nothing) | 1 |
The TUI renders on stderr so it is never swallowed by the shell's
$() capture — the path on stdout is all the wrapper ever sees.
The installed wrapper captures whichever path was printed and calls cd on it.
# Open the selected file in $EDITOR (bypasses the wrapper)
|
# Select a Rust source file and edit it
|
# Start with the Catppuccin Mocha theme and the theme panel open
# Start in single-pane mode (useful for narrow terminals)
# List all available themes
# NUL-delimited output (safe for filenames with spaces or newlines)
|
Theme names are case-insensitive and hyphens/spaces are interchangeable:
catppuccin-mocha,Catppuccin Mocha, andcatppuccin mochaall resolve to the same preset.
Public API
The public surface is intentionally narrow for stability:
Single-pane
| Item | Kind | Description |
|---|---|---|
FileExplorer |
struct |
Core state machine — cursor, entries, search, sort state |
FileExplorerBuilder |
struct |
Fluent builder for FileExplorer |
ExplorerOutcome |
enum |
Result of handle_key — Selected, Dismissed, Pending, Unhandled |
FsEntry |
struct |
A single directory entry (name, path, size, extension, is_dir) |
SortMode |
enum |
Name | SizeDesc | Extension |
Theme |
struct |
Colour palette with builder methods and 27 named presets |
render |
fn |
Render one pane using the default theme |
render_themed |
fn |
Render one pane with a custom Theme |
entry_icon |
fn |
Map an FsEntry to its Unicode icon |
fmt_size |
fn |
Format a byte count as a human-readable string (1.5 KB) |
Dual-pane
| Item | Kind | Description |
|---|---|---|
DualPane |
struct |
Owns left + right: FileExplorer; routes keys; manages focus and single-pane mode |
DualPaneBuilder |
struct |
Fluent builder for DualPane — independent dirs, shared filter/sort/hidden |
DualPaneActive |
enum |
Left | Right — which pane has focus; .other() flips it |
DualPaneOutcome |
enum |
Result of DualPane::handle_key — Selected, Dismissed, Pending, Unhandled |
render_dual_pane |
fn |
Render both panes using the default theme |
render_dual_pane_themed |
fn |
Render both panes with a custom Theme |
Module Layout
Library (src/lib.rs re-exports)
| Module | Contents |
|---|---|
types |
FsEntry, ExplorerOutcome, SortMode — data types only, no I/O |
palette |
Palette constants + Theme builder + 27 named presets |
explorer |
FileExplorer, FileExplorerBuilder, entry_icon, fmt_size |
dual_pane |
DualPane, DualPaneBuilder, DualPaneActive, DualPaneOutcome |
render |
render, render_themed, render_dual_pane, render_dual_pane_themed — pure rendering, no state |
Because rendering is fully decoupled from state, you can slot either widget into any Ratatui layout, render it conditionally as an overlay, or build a completely custom renderer by reading FileExplorer's public fields directly.
Binary (tfe CLI, not part of the public library API)
| Module | Contents |
|---|---|
main |
Cli struct (argument parsing), run(), run_loop() — thin entry-point only |
app |
App state, Pane, ClipOp, ClipboardItem, Modal, handle_event |
ui |
draw(), render_theme_panel(), render_action_bar(), render_modal() |
fs |
copy_dir_all(), emit_path(), resolve_output_path() |
persistence |
AppState, load_state(), save_state(), resolve_theme_idx() |
Generating Demo GIFs
Install VHS then run:
# Generate all GIFs at once (requires just)
# Or individually
GIFs are written to examples/vhs/generated/ and tracked with Git LFS.
Development
Prerequisites
- Rust 1.75.0 or later
just— task runnergit-cliff— changelog generatorvhs— GIF recorder (optional, for demos)
Common tasks
License
MIT — see LICENSE for details.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Acknowledgments
Built for the Ratatui ecosystem.
Special thanks to the Ratatui team for an outstanding TUI framework.