# tui-file-explorer
[](https://crates.io/crates/tui-file-explorer)
[](https://docs.rs/tui-file-explorer)
[](https://opensource.org/licenses/MIT)
[](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/release.yml)
[](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/ci.yml)
[](https://crates.io/crates/tui-file-explorer)
A keyboard-driven, two-pane file manager widget for [Ratatui](https://ratatui.rs).
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, `Tab` to 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 ↓ → Extension` with `s`
- 🎛️ **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 `t` to open a side panel, `[`/`]` to cycle themes
- 🔧 Fluent builder API for ergonomic embedding — both `FileExplorer` and `DualPane`
- 📦 **`DualPane` library widget** — drop a full two-pane explorer into any Ratatui app with one struct
- 🖥️ **`cd` on exit** — dismiss with `Esc`/`q` and your terminal jumps to the directory you were browsing; one-time setup with `tfe --init <shell>` (bash, zsh, fish, powershell)
- ✅ Lean library — only `ratatui` + `crossterm` required (`clap` is opt-out)
---
## Stats
| 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
```toml
[dependencies]
tui-file-explorer = "0.2"
ratatui = "0.30"
```
Library-only (no `clap`-powered CLI binary):
```toml
[dependencies]
tui-file-explorer = { version = "0.2", default-features = false }
```
### As a CLI tool
```bash
cargo install tui-file-explorer
```
Installs the `tfe` binary onto your `PATH`.
---
## Quick Start
### Single-pane
```rust
use tui_file_explorer::{FileExplorer, ExplorerOutcome, SortMode, render};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
// 1. Create once — e.g. in your App::new
let mut explorer = FileExplorer::builder(std::env::current_dir().unwrap())
.allow_extension("iso")
.allow_extension("img")
.sort_mode(SortMode::SizeDesc) // largest files first
.show_hidden(false)
.build();
// 2. Inside Terminal::draw:
// render(&mut explorer, frame, frame.area());
// 3. Inside your key-handler — ↑/↓/←/→ scroll the list, Enter confirms:
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
match explorer.handle_key(key) {
ExplorerOutcome::Selected(path) => println!("chosen: {}", path.display()),
ExplorerOutcome::Dismissed => { /* close the overlay */ }
_ => {}
}
```
### Dual-pane
```rust
use tui_file_explorer::{DualPane, DualPaneOutcome, render_dual_pane_themed, Theme};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::path::PathBuf;
// 1. Create once — left pane defaults to cwd; right pane can differ.
let mut dual = DualPane::builder(std::env::current_dir().unwrap())
.right_dir(PathBuf::from("/tmp"))
.show_hidden(false)
.build();
let theme = Theme::default();
// 2. Inside Terminal::draw:
// render_dual_pane_themed(&mut dual, frame, frame.area(), &theme);
// 3. Inside your key-handler:
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
match dual.handle_key(key) {
DualPaneOutcome::Selected(path) => println!("chosen: {}", path.display()),
DualPaneOutcome::Dismissed => { /* close the overlay */ }
_ => {}
}
```
---
## Key Bindings
### Navigation
| `↑` / `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
| `/` | 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
| `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
| `w` | Toggle two-pane ↔ single-pane layout |
| `t` | Next theme |
| `T` | Toggle theme panel (right sidebar) |
| `[` | Previous theme |
### Search mode (after pressing `/`)
| 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 `Tab` to switch which pane has keyboard focus
- Arrow keys (`←`/`→`) scroll the cursor up/down **without** entering or exiting a directory — use `Enter` / `l` to descend and `Backspace` / `h` to ascend
- Press `w` to **collapse to a single pane** — the hidden pane keeps its state and reappears when you press `w` again
- Press `t` / `[` to cycle themes forward / backward; press `T` to 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:
1. **Navigate** to the file you want in the active pane
2. **`y`** to yank (copy) or **`x`** to cut it — the status bar confirms what is in the clipboard
3. **`Tab`** to switch to the other pane and navigate to the destination directory
4. **`p`** to 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.
```rust
// Inspect search state programmatically:
println!("searching: {}", explorer.is_searching());
println!("query : {}", explorer.search_query());
```
**Behaviour details:**
- `Backspace` on a non-empty query pops the last character
- `Backspace` on an empty query deactivates search without dismissing
- `Esc` clears the query and deactivates search — a second `Esc` (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:
```rust
use tui_file_explorer::{FileExplorer, SortMode};
let mut explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
explorer.set_sort_mode(SortMode::SizeDesc); // largest files first
println!("{}", explorer.sort_mode().label()); // "size ↓"
```
| `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.
```rust
use tui_file_explorer::FileExplorer;
// Builder API (preferred)
let explorer = FileExplorer::builder(std::env::current_dir().unwrap())
.allow_extension("iso")
.allow_extension("img")
.build();
// Or replace the filter at runtime — reloads the listing immediately
explorer.set_extension_filter(["rs", "toml"]);
// Pass an empty filter to allow all files
explorer.set_extension_filter([] as [&str; 0]);
```
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:
```rust
use tui_file_explorer::{FileExplorer, SortMode};
let explorer = FileExplorer::builder(std::env::current_dir().unwrap())
.allow_extension("rs") // add one extension at a time
.allow_extension("toml")
.show_hidden(true) // show dot-files on startup
.sort_mode(SortMode::Extension) // initial sort order
.build();
```
Or pass the full filter list at once:
```rust
let explorer = FileExplorer::builder(std::env::current_dir().unwrap())
.extension_filter(vec!["iso".into(), "img".into()])
.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:
```rust
use tui_file_explorer::{DualPane, SortMode};
use std::path::PathBuf;
let dual = DualPane::builder(std::env::current_dir().unwrap())
.right_dir(PathBuf::from("/tmp")) // independent right-pane directory
.allow_extension("rs") // applied to both panes
.allow_extension("toml")
.show_hidden(false) // both panes
.sort_mode(SortMode::Name) // both panes
.single_pane(false) // 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.
| `.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`:
```rust
use tui_file_explorer::{FileExplorer, Theme, render_themed};
use ratatui::style::Color;
let theme = Theme::default()
.brand(Color::Magenta) // widget title
.accent(Color::Cyan) // borders & current path
.dir(Color::LightYellow) // directory names & icons
.sel_bg(Color::Rgb(50, 40, 80)) // highlighted-row background
.success(Color::Rgb(100, 230, 140)) // status bar & selectable files
.match_file(Color::Rgb(100, 230, 140));
})?;
```
### Named presets (27 included)
All presets are available as associated constructors on `Theme`:
```rust
use tui_file_explorer::Theme;
let t = Theme::dracula();
let t = Theme::nord();
let t = Theme::catppuccin_mocha();
let t = Theme::catppuccin_latte();
let t = Theme::tokyo_night();
let t = Theme::tokyo_night_storm();
let t = Theme::gruvbox_dark();
let t = Theme::kanagawa_wave();
let t = Theme::kanagawa_dragon();
let t = Theme::moonfly();
let t = Theme::oxocarbon();
let t = Theme::grape();
let t = Theme::ocean();
let t = Theme::neon();
// Iterate the full catalogue (name, description, theme):
for (name, desc, _theme) in Theme::all_presets() {
println!("{name} — {desc}");
}
```
**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:
| `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`](examples/basic.rs) — a fully self-contained Ratatui app demonstrating:
- Builder API and full event loop
- All `ExplorerOutcome` variants
- Custom `Theme`
- Optional CLI extension-filter arguments
```bash
# Browse everything
cargo run --example basic
# Only .rs and .toml files are selectable
cargo run --example basic -- rs toml md
```
---
### `dual_pane`
[`examples/dual_pane.rs`](examples/dual_pane.rs) — a fully self-contained dual-pane Ratatui app built entirely on the **library API** (no binary code):
- `DualPane::builder` with an optional independent right-pane directory
- `render_dual_pane_themed` for rendering both panes in one call
- All `DualPaneOutcome` variants
- Status bar showing active pane and current layout mode
| `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 |
```bash
# Both panes start in the current directory
cargo run --example dual_pane
# Left pane starts in cwd, right pane starts in /tmp
cargo run --example dual_pane -- /tmp
```
---
### `theme_switcher`
[`examples/theme_switcher.rs`](examples/theme_switcher.rs) — live theme switching without restarting, with a sidebar showing the full theme catalogue.
| `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 |
```bash
cargo run --example theme_switcher
```
---
## 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 time
- **`w`** — 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; use `t` / `[` to cycle themes while the panel is open; both panes remain fully navigable with the panel visible
---
## Demo Quick Reference
| 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
```bash
tfe [OPTIONS] [PATH]
```
| `[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 (on dismiss, the current directory is always printed regardless) |
| `-0, --null` | Terminate output with a NUL byte (for `xargs -0`) |
| `-h, --help` | Show help |
| `-V, --version` | Show version |
### Exit codes
| `0` | Path printed to stdout (file selected, or dismissed — always emits the active pane's directory) |
| `2` | Bad arguments or I/O error |
### Shell integration
The killer feature: press `Esc` or `q` to dismiss `tfe` and your terminal
**automatically `cd`s** to whichever directory you were browsing.
Works on **macOS, Linux, and Windows**.
Run the one-time setup command for your shell:
```bash
# bash
tfe --init bash # writes to ~/.bashrc
# zsh
tfe --init zsh # writes to ~/.zshrc
# fish
tfe --init fish # writes to ~/.config/fish/functions/tfe.fish
# PowerShell (Windows or cross-platform pwsh)
tfe --init powershell # writes to $PROFILE
```
`--init` appends the wrapper function to your rc file (creating it and any
missing parent directories if needed), tells you where it wrote, and is
idempotent — running it twice will not duplicate the snippet. Then restart
your shell or `source` the rc file as instructed.
How it works: `tfe` always prints a path to stdout on exit.
- **Dismiss** (`Esc` / `q`) → prints the active pane's current directory.
- **File selected** (`Enter` / `l`) → prints the selected file's path.
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.
```bash
# 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
tfe --theme catppuccin-mocha --show-themes
# Start in single-pane mode (useful for narrow terminals)
tfe --single-pane
# List all available themes
tfe --list-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`, and `catppuccin mocha` all resolve to the same preset.
---
## Public API
The public surface is intentionally narrow for stability:
### Single-pane
| `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
| `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)
| `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)
| `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](https://github.com/charmbracelet/vhs) then run:
```bash
# Generate all GIFs at once (requires just)
just vhs-all
# Or individually
vhs examples/vhs/basic.tape
vhs examples/vhs/search.tape
vhs examples/vhs/sort.tape
vhs examples/vhs/filter.tape
vhs examples/vhs/file_ops.tape
vhs examples/vhs/theme_switcher.tape
```
GIFs are written to `examples/vhs/generated/` and tracked with **Git LFS**.
---
## Development
### Prerequisites
- Rust 1.75.0 or later
- [`just`](https://github.com/casey/just) — task runner
- [`git-cliff`](https://github.com/orhun/git-cliff) — changelog generator
- [`vhs`](https://github.com/charmbracelet/vhs) — GIF recorder (optional, for demos)
```bash
just install-tools
```
### Common tasks
```bash
just fmt # format code
just clippy # run linter (zero warnings enforced)
just test # run tests
just check-all # fmt + clippy + test in one shot
just doc # build and open docs
just bump 0.2.0 # interactive version bump + tag
just release 0.2.0 # non-interactive: bump + commit + tag + push (triggers CI release)
just --list # see all available commands
```
---
## License
MIT — see [LICENSE](LICENSE) for details.
---
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
---
## Acknowledgments
Built for the [Ratatui](https://github.com/ratatui/ratatui) ecosystem.
Special thanks to the Ratatui team for an outstanding TUI framework.