tui-file-explorer 4.0.1

Embeddable keyboard-driven multi-pane file explorer for Ratatui
Documentation
//! # tui-file-explorer
//!
//! A self-contained, keyboard-driven file-browser widget for
//! [Ratatui](https://ratatui.rs).
//!
//! ## Design goals
//!
//! * **Layered API** — use [`FileExplorer`], [`DualPane`], or the complete
//!   `App` behind the `full-app` feature.
//! * **Lightweight core** — with default features disabled, only `ratatui`,
//!   `crossterm`, and platform filesystem bindings are required.
//! * **Host control** — semantic commands, custom filesystems, typed outcomes,
//!   and area-aware rendering integrate with an existing event loop.
//! * **Extension filtering** — pass a list of allowed extensions so that only
//!   relevant files are selectable (e.g. `["iso", "img"]`); directories are
//!   always navigable.
//! * **Keyboard-driven** — `↑`/`↓`/`←`/`→` scroll the list, `Enter` / `l`
//!   to descend or confirm, `Backspace` / `h` to ascend, `/` to search,
//!   `s` to cycle sort, `n` to create a new folder, `N` to create a new file,
//!   `Esc` / `q` to dismiss.
//! * **Searchable** — press `/` to enter incremental search; entries are
//!   filtered live as you type.  `Esc` clears the query; a second `Esc`
//!   dismisses the explorer.
//! * **Sortable** — press `s` to cycle through `Name`, `Size ↓`, and
//!   `Extension` sort modes, or set one programmatically via
//!   [`FileExplorer::set_sort_mode`].
//! * **Themeable** — every colour is overridable via [`Theme`] and
//!   [`render_themed`].
//! * **Dual-pane** — [`DualPane`] owns two independent [`FileExplorer`]s,
//!   manages focus switching (`Tab`), and supports a single-pane toggle (`w`).
//!
//! ## Quick start
//!
//! ```
//! use tui_file_explorer::{FileExplorer, ExplorerOutcome, SortMode, render};
//! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
//! # use ratatui::{Terminal, backend::TestBackend};
//! # let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
//!
//! // 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)
//!     .build();
//!
//! // 2. In your Terminal::draw closure:
//! //    render(&mut explorer, frame, frame.area());
//!
//! // 3. In your key-handler:
//! # 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 */ }
//!     _                               => {}
//! }
//! ```
//!
//! ## Builder / configuration
//!
//! Use [`FileExplorer::builder`] for a more ergonomic construction API:
//!
//! ```
//! use tui_file_explorer::{FileExplorer, SortMode};
//!
//! let explorer = FileExplorer::builder(std::env::current_dir().unwrap())
//!     .allow_extension("rs")
//!     .allow_extension("toml")
//!     .show_hidden(true)
//!     .sort_mode(SortMode::Extension)
//!     .build();
//! ```
//!
//! ## Incremental search
//!
//! Press `/` to activate search mode.  Subsequent keystrokes append to the
//! query and the entry list is filtered live (case-insensitive substring
//! match on the file name).  `Backspace` removes the last character; an
//! extra `Backspace` on an empty query deactivates search.  `Esc` clears
//! the query and deactivates search without dismissing the explorer; a
//! second `Esc` (when search is already inactive) dismisses it.
//!
//! Search state is also accessible programmatically:
//!
//! ```
//! use tui_file_explorer::FileExplorer;
//!
//! let explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
//! println!("searching: {}", explorer.is_searching());
//! println!("query    : {}", explorer.search_query());
//! ```
//!
//! ## Sort modes
//!
//! Press `s` to cycle through the three sort modes, or set one directly:
//!
//! ```
//! 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 ↓"
//! ```
//!
//! ## Theming
//!
//! Every colour is customisable via [`Theme`] and [`render_themed`]:
//!
//! ```
//! use tui_file_explorer::{FileExplorer, Theme, render_themed};
//! use ratatui::style::Color;
//!
//! let theme = Theme::default()
//!     .brand(Color::Magenta)
//!     .accent(Color::Cyan)
//!     .dir(Color::LightYellow);
//!
//! // terminal.draw(|frame| {
//! //     render_themed(&mut explorer, frame, frame.area(), &theme);
//! // });
//! ```
//!
//! ## Named presets
//!
//! All 90 `tui-nuances` built-in themes are available as
//! shared themes, converted with `Theme::from`:
//!
//! ```
//! use tui_file_explorer::Theme;
//!
//! let t = Theme::from(&tui_nuances::Theme::dracula());
//! let t = Theme::from(&tui_nuances::Theme::nord());
//! let t = Theme::from(&tui_nuances::Theme::catppuccin_mocha());
//! let t = Theme::from(&tui_nuances::Theme::tokyo_night());
//! let t = Theme::from(&tui_nuances::Theme::gruvbox());
//! let t = Theme::from(&tui_nuances::Theme::kanagawa_wave());
//! let t = Theme::from(&tui_nuances::Theme::oxocarbon());
//!
//! // Iterate the full catalogue (name, theme):
//! for (name, _theme) in Theme::presets() {
//!     println!("{name}");
//! }
//! ```
//!
//! The full list is `tui_nuances::Theme::names()` (also `tfe --list-themes`).
//!
//! ## Key bindings reference
//!
//! | Key | Action |
//! |-----|--------|
//! | `↑` / `k` | Move cursor up |
//! | `↓` / `j` | Move cursor down |
//! | `PgUp` / `PgDn` | Jump 10 entries |
//! | `Home` / `g` | Jump to top |
//! | `End` / `G` | Jump to bottom |
//! | `→` / `l` / `Enter` | Descend into directory; on a file `→` moves cursor down, `l`/`Enter` confirm |
//! | `←` / `h` / `Backspace` | Ascend to parent directory |
//! | `Space` | Toggle space-mark on current entry and advance cursor |
//! | `/` | Activate incremental search |
//! | `s` | Cycle sort mode (`Name` → `Size ↓` → `Extension`) |
//! | `.` | Toggle hidden (dot-file) entries |
//! | `n` | Create a new folder (type name → `Enter` confirm, `Esc` cancel) |
//! | `N` | Create a new file   (type name → `Enter` confirm, `Esc` cancel) |
//! | `r` | Rename current entry (pre-filled with current name → `Enter` confirm, `Esc` cancel) |
//! | `Esc` | Clear search / cancel mkdir / cancel touch / cancel rename (if active), then dismiss |
//! | `q` | Dismiss (when search / mkdir / touch / rename is not active) |
//!
//! ## Dual-pane quick start
//!
//! ```
//! use tui_file_explorer::{DualPane, DualPaneOutcome, render_dual_pane_themed, Theme};
//! use crossterm::event::{Event, KeyCode, KeyModifiers, self};
//! # use ratatui::{Terminal, backend::TestBackend};
//! # let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
//!
//! // 1. Create once — left pane defaults to cwd; right pane mirrors it.
//! let mut dual = DualPane::builder(std::env::current_dir().unwrap())
//!     .right_dir(std::path::PathBuf::from("/tmp"))
//!     .build();
//!
//! let theme = Theme::default();
//!
//! // 2. In your Terminal::draw closure:
//! // terminal.draw(|frame| {
//! //     render_dual_pane_themed(&mut dual, frame, frame.area(), &theme);
//! // }).unwrap();
//!
//! // 3. In your event loop:
//! # let key = crossterm::event::KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
//! match dual.handle_key(key) {
//!     DualPaneOutcome::Selected(path) => println!("chosen: {}", path.display()),
//!     DualPaneOutcome::Dismissed      => { /* close the overlay */ }
//!     _                               => {}
//! }
//! ```
//!
//! ### Extra key bindings provided by `DualPane`
//!
//! | Key     | Action                                    |
//! |---------|-------------------------------------------|
//! | `Tab`   | Switch focus between left and right pane  |
//! | `w`     | Toggle single-pane / dual-pane mode       |
//! | `Space` | Mark current entry (forwarded to active pane) |
//!
//! All standard [`FileExplorer`] bindings continue to work on whichever pane
//! is currently active.
//!
//! ## Folder and file creation
//!
//! ### New folder — `n`
//!
//! Press `n` to enter mkdir mode.  Type the new folder name, then press
//! `Enter` to create it (using the configured [`FileSystem`], so nested paths
//! like `a/b/c` work) or `Esc` to cancel without creating anything.  On success
//! [`ExplorerOutcome::MkdirCreated`] is returned and the cursor moves to the
//! new directory.
//!
//! ### New file — `N`
//!
//! Press `N` (Shift+N) to enter touch mode.  Type the new file name (including
//! extension), then press `Enter` to create it or `Esc` to cancel.  Parent
//! directories are created automatically when the name contains `/`.  An
//! existing file at the target path is left untouched (no truncation).  On
//! success [`ExplorerOutcome::TouchCreated`] is returned and the cursor moves
//! to the new file.
//!
//! Both modes are also accessible programmatically:
//!
//! ```
//! use tui_file_explorer::FileExplorer;
//!
//! let explorer = FileExplorer::new(std::env::current_dir().unwrap(), vec![]);
//! println!("mkdir active : {}", explorer.is_mkdir_active());
//! println!("mkdir input  : {}", explorer.mkdir_input());
//! println!("touch active : {}", explorer.is_touch_active());
//! println!("touch input  : {}", explorer.touch_input());
//! ```
//!
//! ## Module layout
//!
//! | Module      | Contents                                                                                                                                        |
//! |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
//! | `types`     | [`FsEntry`], [`ExplorerOutcome`], [`SortMode`]                                                                                                  |
//! | `palette`   | Palette constants (all `pub`) + [`Theme`] builder + 43 named presets                                                                           |
//! | `explorer`  | [`FileExplorer`], [`FileExplorerBuilder`], [`entry_icon`], [`fmt_size`]                                                                         |
//! | `filesystem` | [`FileSystem`], [`StdFileSystem`], and virtual backend support                                                                                 |
//! | `dual_pane` | [`DualPane`], [`DualPaneBuilder`], [`DualPaneActive`], [`DualPaneOutcome`]                                                                      |
//! | `render`    | [`render()`], [`render_themed`], [`render_dual_pane`], [`render_dual_pane_themed`] — pure rendering, no I/O                                     |
//! | `preview`   | `PreviewState`, `PreviewContent`, `render_preview` — optional text, image, binary hex dump, and directory preview support                    |
//! | `inline_editor` | [`InlineEditor`], [`EditorAction`], [`render_inline_editor`] — built-in text editor                                                       |

/// Define a fieldless command enum together with its stable snake_case names
/// (used by config files): `ALL`, `name()` and `from_name()`.
macro_rules! command_enum {
    (
        $(#[$meta:meta])*
        $vis:vis enum $name:ident {
            $( $(#[$vmeta:meta])* $variant:ident => $text:literal ),+ $(,)?
        }
    ) => {
        $(#[$meta])*
        $vis enum $name {
            $( $(#[$vmeta])* $variant ),+
        }

        impl $name {
            /// Every variant, in declaration order.
            pub const ALL: &'static [Self] = &[ $( Self::$variant ),+ ];

            /// Stable snake_case name used in config files.
            #[must_use]
            pub fn name(self) -> &'static str {
                match self { $( Self::$variant => $text ),+ }
            }

            /// Parse a [`name`](Self::name) (case-insensitive, `-` == `_`).
            #[must_use]
            pub fn from_name(name: &str) -> Option<Self> {
                let wanted = name.trim().to_ascii_lowercase().replace('-', "_");
                Self::ALL.iter().copied().find(|c| c.name() == wanted)
            }
        }
    };
}

/// Saturating `usize → u16` for terminal geometry (never wraps).
pub(crate) fn to_u16(n: usize) -> u16 {
    u16::try_from(n).unwrap_or(u16::MAX)
}

/// Early-return from a key handler for anything but a key *press*.
///
/// On Windows (and terminals that negotiate the kitty keyboard protocol)
/// crossterm delivers both `Press` and `Release` events for every physical
/// key. Without this guard handlers run twice per key — double-toggling
/// marks, double-navigating, and so on.
macro_rules! press_only {
    ($key:expr, $ret:expr) => {
        if $key.kind != ::crossterm::event::KeyEventKind::Press {
            return $ret;
        }
    };
}

#[cfg(feature = "archive")]
pub mod archive;
pub mod bulk_rename;
pub mod dual_pane;
pub mod explorer;
pub mod filesystem;
pub mod git;
pub mod inline_editor;
pub mod keymap;
pub mod nuances;
pub mod palette;
pub mod picker;
#[cfg(feature = "preview")]
pub mod preview;
pub mod render;
pub mod types;
pub mod worker;

// ── Full-app modules ──────────────────────────────────────────────────────────

#[cfg(feature = "full-app")]
pub mod app;
pub mod fs_util;
#[cfg(feature = "persistence")]
pub mod persistence;
#[cfg(feature = "full-app")]
pub mod ui;

// ── Convenience re-exports ────────────────────────────────────────────────────

pub use dual_pane::{DualPane, DualPaneActive, DualPaneBuilder, DualPaneCommand, DualPaneOutcome};
pub use explorer::{
    ExplorerError, FileExplorer, FileExplorerBuilder, PAGE_SIZE, entry_icon, fmt_size,
};
#[cfg(feature = "trash")]
pub use filesystem::TrashFileSystem;
pub use filesystem::{
    FileSystem, SharedFileSystem, StdFileSystem, TRASH_SUPPORTED, VirtualEntry, local_filesystem,
    std_filesystem,
};
pub use nuances::{
    NuancesSwitcher, NuancesTheme, ThemeBuilder, ThemeProvider, Themed, render_dual_pane_nuanced,
    render_nuanced,
};
pub use palette::Theme;
pub use render::{
    FOOTER_HEIGHT, HEADER_HEIGHT, MARK_INDICATOR, SCROLLBAR_CHAR, render, render_dual_pane,
    render_dual_pane_themed, render_themed,
};
pub use tui_nuances;
pub use types::{
    DiskUsage, ExplorerCommand, ExplorerOutcome, FsEntry, Prompt, PromptKind, SelectionMode,
    SortMode,
};

// ── Full-app re-exports ───────────────────────────────────────────────────────

#[cfg(feature = "full-app")]
pub use app::{
    App, AppBuildError, AppBuilder, AppCommand, AppEvent, AppOptions, AppOutcome, ClipOp,
    ClipboardItem, CopyProgress, Editor, FileOperation, KeyBinding, KeyBindings, Modal,
    OperationApplyError, OperationFailure, OperationId, OperationMode, OperationProgress,
    OperationRequest, OperationResult, PaneOptions, Snackbar, execute_operation,
    execute_operation_with, execute_operation_with_progress,
};
pub use fs_util::{copy_dir_all, resolve_output_path};
pub use git::{GitMark, GitStatus};
pub use inline_editor::{EditorAction, InlineEditor, render_inline_editor};
pub use keymap::{ChordMods, KeyChord, Keymap, KeymapError};
#[cfg(feature = "full-app")]
pub use keymap::{
    KeymapConfig, default_keymap_toml, keymap_path, load_keymap_config, parse_keymap_config,
};
#[cfg(feature = "persistence")]
pub use persistence::{
    AppState, load_state, load_state_from, resolve_theme_idx, save_state, save_state_to,
    try_load_state_from, try_save_state,
};
pub use picker::{Picker, PickerAction, PickerItem, PickerKind, fuzzy_match, render_picker};
#[cfg(feature = "preview")]
pub use preview::{PreviewContent, PreviewState, render_preview};
#[cfg(feature = "full-app")]
pub use ui::{
    AppViewOptions, HintLayout, draw, draw_in, draw_in_with_options, render_action_bar,
    render_copy_progress, render_editor_panel, render_modal, render_modal_for, render_nav_hints,
    render_options_panel, render_snackbar, render_theme_panel,
};