tui-file-explorer 0.3.4

A self-contained, keyboard-driven file-browser widget for Ratatui
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
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# tui-file-explorer

[![Crates.io](https://img.shields.io/crates/v/tui-file-explorer)](https://crates.io/crates/tui-file-explorer)
[![Documentation](https://docs.rs/tui-file-explorer/badge.svg)](https://docs.rs/tui-file-explorer)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Release](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/release.yml/badge.svg)](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/release.yml)
[![CI](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/ci.yml/badge.svg)](https://github.com/sorinirimies/tui-file-explorer/actions/workflows/ci.yml)
[![Downloads](https://img.shields.io/crates/d/tui-file-explorer)](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`

![Navigation, search and sort](examples/vhs/generated/basic.gif)

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

![Copy, Cut, Paste, Delete](examples/vhs/generated/file_ops.gif)

**27 Live Themes** — `cargo run --example theme_switcher`

![27 live themes](examples/vhs/generated/theme_switcher.gif)

---

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

| 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

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

| 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 `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 ↓"
```

| 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.

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

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

```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));

terminal.draw(|frame| {
    render_themed(&mut explorer, frame, frame.area(), &theme);
})?;
```

### 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:

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

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

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

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

```bash
cargo run --example theme_switcher
```

---

## Example Demos

### Navigation & Search

**Run:** `cargo run --example basic`

![Navigation and search](examples/vhs/generated/basic.gif)

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

![Incremental search](examples/vhs/generated/search.gif)

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`

![Sort modes](examples/vhs/generated/sort.gif)

Demonstrates `Name → Size ↓ → Extension → Name` cycling, combined with search, and sort persistence across directory navigation.

---

### Extension Filter

**Run:** `cargo run --example basic -- rs toml`

![Extension filter](examples/vhs/generated/filter.gif)

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`

![Copy, Cut, Paste, Delete](examples/vhs/generated/file_ops.gif)

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`

![Theme switcher](examples/vhs/generated/theme_switcher.gif)

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`

![Pane toggle](examples/vhs/generated/pane_toggle.gif)

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

| 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

```bash
tfe [OPTIONS] [PATH]
```

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

| Code | Meaning |
|------|---------|
| `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)
command tfe | xargs -r $EDITOR

# Select a Rust source file and edit it
command tfe -e rs | xargs -r nvim

# 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)
command tfe -0 | xargs -0 wc -l
```

> **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

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