tui_file_explorer/lib.rs
1//! # tui-file-explorer
2//!
3//! A self-contained, keyboard-driven file-browser widget for
4//! [Ratatui](https://ratatui.rs).
5//!
6//! ## Design goals
7//!
8//! * **Zero application-specific dependencies** — only `ratatui`, `crossterm`,
9//! and the standard library are required.
10//! * **Narrow public surface** — the public API is intentionally small so the
11//! crate can evolve without breaking changes.
12//! * **Extension filtering** — pass a list of allowed extensions so that only
13//! relevant files are selectable (e.g. `["iso", "img"]`); directories are
14//! always navigable.
15//! * **Keyboard-driven** — `↑`/`↓`/`←`/`→` scroll the list, `Enter` / `l`
16//! to descend or confirm, `Backspace` / `h` to ascend, `/` to search,
17//! `s` to cycle sort, `n` to create a new folder, `N` to create a new file,
18//! `Esc` / `q` to dismiss.
19//! * **Searchable** — press `/` to enter incremental search; entries are
20//! filtered live as you type. `Esc` clears the query; a second `Esc`
21//! dismisses the explorer.
22//! * **Sortable** — press `s` to cycle through `Name`, `Size ↓`, and
23//! `Extension` sort modes, or set one programmatically via
24//! [`FileExplorer::set_sort_mode`].
25//! * **Themeable** — every colour is overridable via [`Theme`] and
26//! [`render_themed`].
27//! * **Dual-pane** — [`DualPane`] owns two independent [`FileExplorer`]s,
28//! manages focus switching (`Tab`), and supports a single-pane toggle (`w`).
29//!
30//! ## Quick start
31//!
32//! ```no_run
33//! use tui_file_explorer::{FileExplorer, ExplorerOutcome, SortMode, render};
34//! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
35//! # use ratatui::{Terminal, backend::TestBackend};
36//! # let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
37//!
38//! // 1. Create once (e.g. in your App::new).
39//! let mut explorer = FileExplorer::builder(std::env::current_dir().unwrap())
40//! .allow_extension("iso")
41//! .allow_extension("img")
42//! .sort_mode(SortMode::SizeDesc)
43//! .build();
44//!
45//! // 2. In your Terminal::draw closure:
46//! // render(&mut explorer, frame, frame.area());
47//!
48//! // 3. In your key-handler:
49//! # let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
50//! match explorer.handle_key(key) {
51//! ExplorerOutcome::Selected(path) => println!("chosen: {}", path.display()),
52//! ExplorerOutcome::Dismissed => { /* close the overlay */ }
53//! _ => {}
54//! }
55//! ```
56//!
57//! ## Builder / configuration
58//!
59//! Use [`FileExplorer::builder`] for a more ergonomic construction API:
60//!
61//! ```no_run
62//! use tui_file_explorer::{FileExplorer, SortMode};
63//!
64//! let explorer = FileExplorer::builder(std::env::current_dir().unwrap())
65//! .allow_extension("rs")
66//! .allow_extension("toml")
67//! .show_hidden(true)
68//! .sort_mode(SortMode::Extension)
69//! .build();
70//! ```
71//!
72//! ## Incremental search
73//!
74//! Press `/` to activate search mode. Subsequent keystrokes append to the
75//! query and the entry list is filtered live (case-insensitive substring
76//! match on the file name). `Backspace` removes the last character; an
77//! extra `Backspace` on an empty query deactivates search. `Esc` clears
78//! the query and deactivates search without dismissing the explorer; a
79//! second `Esc` (when search is already inactive) dismisses it.
80//!
81//! Search state is also accessible programmatically:
82//!
83//! ```no_run
84//! use tui_file_explorer::FileExplorer;
85//!
86//! let explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
87//! println!("searching: {}", explorer.is_searching());
88//! println!("query : {}", explorer.search_query());
89//! ```
90//!
91//! ## Sort modes
92//!
93//! Press `s` to cycle through the three sort modes, or set one directly:
94//!
95//! ```no_run
96//! use tui_file_explorer::{FileExplorer, SortMode};
97//!
98//! let mut explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
99//! explorer.set_sort_mode(SortMode::SizeDesc); // largest files first
100//!
101//! println!("{}", explorer.sort_mode().label()); // "size ↓"
102//! ```
103//!
104//! ## Theming
105//!
106//! Every colour is customisable via [`Theme`] and [`render_themed`]:
107//!
108//! ```no_run
109//! use tui_file_explorer::{FileExplorer, Theme, render_themed};
110//! use ratatui::style::Color;
111//!
112//! let theme = Theme::default()
113//! .brand(Color::Magenta)
114//! .accent(Color::Cyan)
115//! .dir(Color::LightYellow);
116//!
117//! // terminal.draw(|frame| {
118//! // render_themed(&mut explorer, frame, frame.area(), &theme);
119//! // });
120//! ```
121//!
122//! ## Named presets
123//!
124//! Twenty well-known editor / terminal colour schemes are available as
125//! associated constructors on [`Theme`], mirroring the catalogue found in
126//! [Iced](https://docs.rs/iced/latest/iced/theme/enum.Theme.html):
127//!
128//! ```
129//! use tui_file_explorer::Theme;
130//!
131//! let t = Theme::dracula();
132//! let t = Theme::nord();
133//! let t = Theme::catppuccin_mocha();
134//! let t = Theme::tokyo_night();
135//! let t = Theme::gruvbox_dark();
136//! let t = Theme::kanagawa_wave();
137//! let t = Theme::oxocarbon();
138//!
139//! // Iterate the full catalogue (name, description, theme):
140//! for (name, desc, _theme) in Theme::all_presets() {
141//! println!("{name} — {desc}");
142//! }
143//! ```
144//!
145//! The complete list: `Default`, `Dracula`, `Nord`, `Solarized Dark`,
146//! `Solarized Light`, `Gruvbox Dark`, `Gruvbox Light`, `Catppuccin Latte`,
147//! `Catppuccin Frappé`, `Catppuccin Macchiato`, `Catppuccin Mocha`,
148//! `Tokyo Night`, `Tokyo Night Storm`, `Tokyo Night Light`, `Kanagawa Wave`,
149//! `Kanagawa Dragon`, `Kanagawa Lotus`, `Moonfly`, `Nightfly`, `Oxocarbon`.
150//!
151//! ## Key bindings reference
152//!
153//! | Key | Action |
154//! |-----|--------|
155//! | `↑` / `k` | Move cursor up |
156//! | `↓` / `j` | Move cursor down |
157//! | `PgUp` / `PgDn` | Jump 10 entries |
158//! | `Home` / `g` | Jump to top |
159//! | `End` / `G` | Jump to bottom |
160//! | `→` / `l` / `Enter` | Descend into directory; on a file `→` moves cursor down, `l`/`Enter` confirm |
161//! | `←` / `h` / `Backspace` | Ascend to parent directory |
162//! | `Space` | Toggle space-mark on current entry and advance cursor |
163//! | `/` | Activate incremental search |
164//! | `s` | Cycle sort mode (`Name` → `Size ↓` → `Extension`) |
165//! | `.` | Toggle hidden (dot-file) entries |
166//! | `n` | Create a new folder (type name → `Enter` confirm, `Esc` cancel) |
167//! | `N` | Create a new file (type name → `Enter` confirm, `Esc` cancel) |
168//! | `r` | Rename current entry (pre-filled with current name → `Enter` confirm, `Esc` cancel) |
169//! | `Esc` | Clear search / cancel mkdir / cancel touch / cancel rename (if active), then dismiss |
170//! | `q` | Dismiss (when search / mkdir / touch / rename is not active) |
171//!
172//! ## Dual-pane quick start
173//!
174//! ```no_run
175//! use tui_file_explorer::{DualPane, DualPaneOutcome, render_dual_pane_themed, Theme};
176//! use crossterm::event::{Event, KeyCode, KeyModifiers, self};
177//! # use ratatui::{Terminal, backend::TestBackend};
178//! # let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
179//!
180//! // 1. Create once — left pane defaults to cwd; right pane mirrors it.
181//! let mut dual = DualPane::builder(std::env::current_dir().unwrap())
182//! .right_dir(std::path::PathBuf::from("/tmp"))
183//! .build();
184//!
185//! let theme = Theme::default();
186//!
187//! // 2. In your Terminal::draw closure:
188//! // terminal.draw(|frame| {
189//! // render_dual_pane_themed(&mut dual, frame, frame.area(), &theme);
190//! // }).unwrap();
191//!
192//! // 3. In your event loop:
193//! # let Event::Key(key) = event::read().unwrap() else { return; };
194//! match dual.handle_key(key) {
195//! DualPaneOutcome::Selected(path) => println!("chosen: {}", path.display()),
196//! DualPaneOutcome::Dismissed => { /* close the overlay */ }
197//! _ => {}
198//! }
199//! ```
200//!
201//! ### Extra key bindings provided by `DualPane`
202//!
203//! | Key | Action |
204//! |---------|-------------------------------------------|
205//! | `Tab` | Switch focus between left and right pane |
206//! | `w` | Toggle single-pane / dual-pane mode |
207//! | `Space` | Mark current entry (forwarded to active pane) |
208//!
209//! All standard [`FileExplorer`] bindings continue to work on whichever pane
210//! is currently active.
211//!
212//! ## Folder and file creation
213//!
214//! ### New folder — `n`
215//!
216//! Press `n` to enter mkdir mode. Type the new folder name, then press
217//! `Enter` to create it (using `fs::create_dir_all`, so nested paths like
218//! `a/b/c` work) or `Esc` to cancel without creating anything. On success
219//! [`ExplorerOutcome::MkdirCreated`] is returned and the cursor moves to the
220//! new directory.
221//!
222//! ### New file — `N`
223//!
224//! Press `N` (Shift+N) to enter touch mode. Type the new file name (including
225//! extension), then press `Enter` to create it or `Esc` to cancel. Parent
226//! directories are created automatically when the name contains `/`. An
227//! existing file at the target path is left untouched (no truncation). On
228//! success [`ExplorerOutcome::TouchCreated`] is returned and the cursor moves
229//! to the new file.
230//!
231//! Both modes are also accessible programmatically:
232//!
233//! ```no_run
234//! use tui_file_explorer::FileExplorer;
235//!
236//! let explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
237//! println!("mkdir active : {}", explorer.is_mkdir_active());
238//! println!("mkdir input : {}", explorer.mkdir_input());
239//! println!("touch active : {}", explorer.is_touch_active());
240//! println!("touch input : {}", explorer.touch_input());
241//! ```
242//!
243//! ## Module layout
244//!
245//! | Module | Contents |
246//! |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
247//! | `types` | [`FsEntry`], [`ExplorerOutcome`], [`SortMode`] |
248//! | `palette` | Palette constants (all `pub`) + [`Theme`] builder + 27 named presets |
249//! | `explorer` | [`FileExplorer`], [`FileExplorerBuilder`], [`entry_icon`], [`fmt_size`], `load_entries` |
250//! | `dual_pane` | [`DualPane`], [`DualPaneBuilder`], [`DualPaneActive`], [`DualPaneOutcome`] |
251//! | `render` | [`render`], [`render_themed`], [`render_dual_pane`], [`render_dual_pane_themed`] — pure rendering, no I/O |
252
253pub mod dual_pane;
254pub mod explorer;
255pub mod palette;
256pub mod render;
257pub mod types;
258
259// ── Full-app modules ──────────────────────────────────────────────────────────
260
261pub mod app;
262pub mod fs;
263pub mod persistence;
264pub mod ui;
265
266// ── Convenience re-exports ────────────────────────────────────────────────────
267
268pub use dual_pane::{DualPane, DualPaneActive, DualPaneBuilder, DualPaneOutcome};
269pub use explorer::{entry_icon, fmt_size, FileExplorer, FileExplorerBuilder};
270pub use palette::Theme;
271pub use render::{render, render_dual_pane, render_dual_pane_themed, render_themed};
272pub use types::{ExplorerOutcome, FsEntry, SortMode};
273
274// ── Full-app re-exports ───────────────────────────────────────────────────────
275
276pub use app::{
277 App, AppOptions, ClipOp, ClipboardItem, CopyProgress, Editor, Modal, Pane, Snackbar,
278};
279pub use fs::{copy_dir_all, resolve_output_path};
280pub use persistence::{load_state, resolve_theme_idx, save_state, AppState};
281pub use ui::{
282 draw, render_action_bar, render_copy_progress, render_editor_panel, render_modal,
283 render_nav_hints, render_options_panel, render_snackbar, render_theme_panel,
284};