# fast-fs/nav API Reference
Complete API reference for the `fast_fs::nav` module.
> **Note:** The Browser API is fully async. All methods that perform I/O require `.await`.
## Table of Contents
- [Browser](#browser)
- [BrowserConfig](#browserconfig)
- [KeyInput](#keyinput)
- [KeyMap](#keymap)
- [Action](#action)
- [ActionResult](#actionresult)
- [ClipboardState](#clipboardstate)
- [InputRequest](#inputrequest)
- [PendingOp](#pendingop)
- [Selection](#selection)
- [History](#history)
- [FileCategory](#filecategory)
- [NavError](#naverror)
- [Functions](#functions)
---
## Browser
Core state container for file navigation. All I/O methods are async.
```rust
use fast_fs::nav::{Browser, BrowserConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = BrowserConfig::open_dialog();
let mut browser = Browser::new(config).await?;
Ok(())
}
```
### Construction (async)
| `Browser::new(config).await` | Create browser at current directory |
| `Browser::at_path(path, config).await` | Create browser at specified path |
### State Access
| `files()` | `&FileList` | Current file list (filtered) |
| `cursor()` | `usize` | Current cursor position |
| `current_entry()` | `Option<&FileEntry>` | Entry at cursor |
| `current_path()` | `&Path` | Current directory path |
| `selection()` | `&Selection` | Selection state |
| `config()` | `&BrowserConfig` | Browser configuration |
| `is_readonly()` | `bool` | Whether mutations are blocked |
| `visible_range(viewport_height)` | `Range<usize>` | Range for viewport rendering |
| `selected_paths()` | `impl Iterator<Item = &Path>` | Selected path iterator |
| `selected_or_current()` | `Vec<&Path>` | Selection or current entry |
### Filtering
| `set_filter(pattern: &str)` | Set filter (glob if contains `*` or `?`, else substring) |
| `clear_filter()` | Remove active filter |
| `filter()` | Get current filter pattern |
| `total_count()` | Entries before filtering |
| `filtered_count()` | Entries after filtering |
### Navigation (async)
| `breadcrumbs()` | Get path components as `Vec<(String, PathBuf)>` (sync) |
| `go_to_breadcrumb(index).await` | Navigate to breadcrumb |
| `navigate_to(path).await` | Navigate to path (relative or absolute) |
| `has_parent_entry()` | Whether parent navigation is available (sync) |
| `Browser::roots()` | Get filesystem roots (static method) |
| `refresh().await` | Reload current directory |
### Key Handling (async)
| `handle_key(key: KeyInput).await` | Process key input, return `ActionResult` |
| `execute(action: Action).await` | Execute action directly |
### Cursor Navigation (sync)
These methods handle cursor movement and typeahead search. They're synchronous and don't perform I/O.
| `jump_to_char(c: char)` | `bool` | Jump to next item starting with char (cycles through matches) |
| `jump_to_substring(query: &str)` | `bool` | Jump to next item containing substring (cycles through matches) |
| `page_up(viewport_height: usize)` | `()` | Move cursor up by viewport height (with 1-line overlap) |
| `page_down(viewport_height: usize)` | `()` | Move cursor down by viewport height (with 1-line overlap) |
**Typeahead vs Filtering:**
- `jump_to_char` / `jump_to_substring`: Navigate to matches without hiding non-matches
- `set_filter`: Hides non-matching entries from the list
**Example: Windows-style typeahead**
```rust
match browser.handle_key(key).await {
ActionResult::Unhandled => {
// Unbound character key - use for typeahead
if let KeyInput::Char(c) = key {
browser.jump_to_char(c); // Cycles on repeated press
}
}
_ => {}
}
```
**Example: Page navigation**
```rust
// In your UI framework's key handler
match key {
KeyInput::PageUp => browser.page_up(terminal_height),
KeyInput::PageDown => browser.page_down(terminal_height),
_ => {}
}
```
### Confirmation/Input (async)
| `resolve_confirmation(confirmed: bool).await` | Resolve pending confirmation dialog |
| `complete_input(value: &str).await` | Complete pending input request |
| `cancel_input()` | Cancel pending input (sync) |
| `pending_operation()` | Get pending operation if any (sync) |
### Thread Safety
- `Send`: Can be moved to another thread
- `!Sync`: Use `Arc<Mutex<Browser>>` for shared access
---
## BrowserConfig
Configuration for browser behavior.
```rust
use fast_fs::nav::BrowserConfig;
// Use presets
let config = BrowserConfig::open_dialog();
let config = BrowserConfig::save_dialog();
let config = BrowserConfig::project_explorer();
// Or customize
let config = BrowserConfig::default()
.with_readonly(true)
.with_show_hidden(true)
.with_initial_path("/home/user");
```
### Presets
| `open_dialog()` | `true` | `false` | N/A | `false` |
| `save_dialog()` | `false` | `false` | `false` | `false` |
| `project_explorer()` | `false` | `false` | `true` | `true` |
### Builder Methods
| `with_readonly(bool)` | Block all mutations |
| `with_show_hidden(bool)` | Show hidden files |
| `with_show_parent_entry(bool)` | Show `..` entry |
| `with_confirm_delete(bool)` | Require delete confirmation |
| `with_confirm_overwrite(bool)` | Require overwrite confirmation |
| `with_clear_selection_on_navigate(bool)` | Clear selection on navigation |
| `with_respect_ignore_files(bool)` | Honor `.gitignore` |
| `with_initial_path(path)` | Set starting directory |
| `with_sort(SortBy)` | Set initial sort order |
| `with_history_limit(usize)` | Set history stack size |
| `with_keymap(KeyMap)` | Set custom key bindings |
### Fields (Public)
```rust
pub struct BrowserConfig {
pub readonly: bool,
pub show_hidden: bool,
pub show_parent_entry: bool,
pub confirm_delete: bool,
pub confirm_overwrite: bool,
pub clear_selection_on_navigate: bool,
pub respect_ignore_files: bool,
pub initial_path: Option<PathBuf>,
pub sort_by: SortBy,
pub history_limit: usize,
pub keymap: KeyMap,
}
```
---
## KeyInput
Framework-agnostic key representation.
```rust
use fast_fs::nav::KeyInput;
// Character input
let key = KeyInput::Char('j');
// Navigation keys
let key = KeyInput::Down;
let key = KeyInput::Enter;
// Modifiers
let key = KeyInput::Ctrl('d');
let key = KeyInput::Alt('h');
```
### Variants
| `Char(char)` | Regular character |
| `Ctrl(char)` | Ctrl + character |
| `Alt(char)` | Alt + character |
| `Shift(char)` | Shift + character |
| `Up`, `Down`, `Left`, `Right` | Arrow keys |
| `ShiftUp`, `ShiftDown` | Shift + arrow (range selection) |
| `Home`, `End` | Home/End keys |
| `PageUp`, `PageDown` | Page navigation |
| `Enter` | Enter/Return |
| `Tab`, `BackTab` | Tab navigation |
| `Backspace`, `Delete` | Deletion keys |
| `Escape` | Escape |
| `F(u8)` | Function keys (F1-F12) |
---
## KeyMap
Action-to-key bindings.
```rust
use fast_fs::nav::{KeyMap, KeyInput, Action};
// Use default vim-style bindings
let keymap = KeyMap::default();
// Or build custom
let mut keymap = KeyMap::empty();
keymap.bind(KeyInput::Up, Action::MoveUp);
keymap.bind(KeyInput::Down, Action::MoveDown);
```
### Default Bindings (Vim-style)
| `j`, `Down` | MoveDown |
| `k`, `Up` | MoveUp |
| `g` | MoveToTop |
| `G` | MoveToBottom |
| `Enter`, `l`, `Right` | Enter |
| `h`, `Left`, `Backspace` | GoParent |
| `-` | GoBack |
| `_` | GoForward |
| `Space` | ToggleSelect |
| `Ctrl-a` | SelectAll |
| `Escape` | ClearSelection |
| `Shift+Up` | MoveUpExtend (range select) |
| `Shift+Down` | MoveDownExtend (range select) |
| `Ctrl-x` | Cut |
| `Ctrl-c` | Copy |
| `d` | Delete |
| `r` | Rename |
| `n` | CreateDir |
| `N` | CreateFile |
| `.` | ToggleHidden |
| `s` | CycleSort |
| `Shift-R` | Refresh |
| `/` | StartFilter |
| `:` | StartPathInput |
### Methods
| `KeyMap::default()` | Vim-style defaults |
| `KeyMap::empty()` | No bindings |
| `bind(key, action)` | Add binding |
| `unbind(key)` | Remove binding |
| `get(key)` | Get action for key |
| `keys_for(action)` | Get all keys for action |
### Customizing Key Bindings
KeyMap is fully configurable. You can override any default binding:
```rust
use fast_fs::nav::{KeyMap, KeyInput, Action, BrowserConfig};
// Option 1: Start with defaults and modify
let mut keymap = KeyMap::default();
keymap.unbind(KeyInput::Ctrl('c')); // Free Ctrl+C for terminal interrupt
keymap.unbind(KeyInput::Ctrl('x')); // Free Ctrl+X for terminal usage
keymap.bind(KeyInput::Char('y'), Action::Copy); // Use 'y' for yank instead
keymap.bind(KeyInput::Char('x'), Action::Cut); // Use 'x' for cut
// Option 2: Start empty and build your own
let mut keymap = KeyMap::empty();
keymap.bind(KeyInput::Up, Action::MoveUp);
keymap.bind(KeyInput::Down, Action::MoveDown);
// ... add only what you need
// Use with BrowserConfig
let config = BrowserConfig::default().with_keymap(keymap);
```
**Common Customizations:**
| Free Ctrl+C for interrupt | `keymap.unbind(KeyInput::Ctrl('c'))` |
| Arrow-only navigation | Start with `KeyMap::empty()`, add only arrow keys |
| Emacs-style bindings | Unbind vim keys, bind `Ctrl('n')` → MoveDown, etc. |
| Disable dangerous ops | Don't bind `Action::Delete` |
---
## Action
User actions the browser can execute.
```rust
use fast_fs::nav::Action;
// Navigation
Action::MoveUp
Action::MoveDown
Action::MoveToTop
Action::MoveToBottom
Action::PageUp
Action::PageDown
Action::Enter
Action::GoParent
Action::GoBack
Action::GoForward
// Selection
Action::ToggleSelect
Action::SelectAll
Action::ClearSelection
Action::MoveUpExtend // Shift+Up: extend selection
Action::MoveDownExtend // Shift+Down: extend selection
// Clipboard
Action::Cut // Returns ClipboardState
Action::Copy // Returns ClipboardState
// File operations
Action::Delete
Action::Rename
Action::CreateDir
Action::CreateFile
// View
Action::ToggleHidden
Action::CycleSort
Action::Refresh
// Input
Action::StartFilter
Action::ClearFilter
Action::StartPathInput
```
### Methods
| `is_mutation()` | Returns `true` for Delete, Rename, CreateDir, CreateFile |
| `is_selection()` | Returns `true` for selection-related actions |
| `is_clipboard()` | Returns `true` for Cut, Copy |
| `is_navigation()` | Returns `true` for directory navigation actions |
| `description()` | Human-readable action name |
---
## ActionResult
Result of executing an action.
```rust
use fast_fs::nav::ActionResult;
match browser.handle_key(key).await {
ActionResult::Done => {
// Re-render UI
}
ActionResult::DirectoryChanged => {
// Full re-render, directory changed
}
ActionResult::NeedsConfirmation(op) => {
// Show confirmation dialog
// Then call browser.resolve_confirmation(user_confirmed)
}
ActionResult::NeedsInput(req) => {
// Show input UI
// Then call browser.complete_input(user_input)
}
ActionResult::FileSelected(path) => {
// User selected a file (for open/save dialogs)
}
ActionResult::Clipboard(state) => {
// Cut/Copy performed - store state for paste
app_clipboard = Some(state);
}
ActionResult::Unhandled => {
// Key not bound - try jump_to_char for character keys
}
}
```
### Variants
| `Done` | Action completed, may need re-render |
| `DirectoryChanged` | Directory changed, full re-render needed |
| `NeedsConfirmation(PendingOp)` | Show confirmation dialog |
| `NeedsInput(InputRequest)` | Show input UI |
| `FileSelected(PathBuf)` | File was activated |
| `Clipboard(ClipboardState)` | Cut/Copy performed, caller should store state |
| `Unhandled` | Key not bound to action |
### Methods
| `is_directory_changed()` | Check if directory changed |
| `needs_interaction()` | Check if user interaction required |
---
## ClipboardState
Tracks cut/copy intent. The library tracks which files and operation type; the caller handles actual clipboard integration and paste operations.
```rust
use fast_fs::nav::{ClipboardOp, ClipboardState};
// Returned by ActionResult::Clipboard
match browser.handle_key(KeyInput::Ctrl('c')).await {
ActionResult::Clipboard(state) => {
println!("{}", state.message()); // "copy 3 items"
// Store for later paste
app_clipboard = Some(state);
}
_ => {}
}
// Later, when pasting
if let Some(ref clipboard) = app_clipboard {
// Check for conflicts
let conflicts = clipboard.would_conflict(browser.current_path());
if !conflicts.is_empty() {
// Ask user about overwrites
}
// Perform actual file operations (caller's responsibility)
match clipboard.operation {
ClipboardOp::Cut => { /* move files */ }
ClipboardOp::Copy => { /* copy files */ }
}
}
```
### ClipboardOp
| `Cut` | Move operation (delete source after paste) |
| `Copy` | Copy operation (keep source) |
### ClipboardState Fields
| `operation` | `ClipboardOp` | Cut or Copy |
| `paths` | `Vec<PathBuf>` | Files that were cut/copied |
| `source_dir` | `PathBuf` | Directory where operation originated |
### ClipboardState Methods
| `message()` | `String` | Human-readable (e.g., "copy 3 items") |
| `would_conflict(dest)` | `Vec<PathBuf>` | Paths that would conflict at destination |
| `is_same_directory(dest)` | `bool` | Whether dest equals source |
| `len()` | `usize` | Number of items |
| `is_empty()` | `bool` | Whether clipboard is empty |
---
## InputRequest
Request for user input.
```rust
use fast_fs::nav::InputRequest;
match input_request {
InputRequest::Filter { current } => {
// Show filter input, pre-fill with current
}
InputRequest::Path { current } => {
// Show path input, pre-fill with current path
}
InputRequest::Rename { current_name } => {
// Show rename input, pre-fill with current name
}
InputRequest::NewDirectory => {
// Show new directory name input
}
InputRequest::NewFile => {
// Show new file name input
}
}
```
### Methods
| `prompt()` | `&'static str` | Prompt text ("Filter:", "New name:", etc.) |
| `current()` | `Option<String>` | Current value for pre-fill |
| `current_path()` | `Option<&Path>` | Path for Path variant (preserves non-UTF8) |
| `is_create()` | `bool` | Whether this creates a new item |
---
## PendingOp
Pending operation awaiting confirmation.
```rust
use fast_fs::nav::PendingOp;
match pending_op {
PendingOp::Delete { paths } => {
println!("Delete {} items?", paths.len());
}
PendingOp::Rename { from, to } => {
println!("Rename {} to {}?", from.display(), to.display());
}
PendingOp::Overwrite { path } => {
println!("{} exists. Overwrite?", path.display());
}
}
```
### Methods
| `message()` | `String` | Human-readable confirmation message |
| `paths()` | `impl Iterator<Item = &Path>` | Affected paths |
| `operation_type()` | `&'static str` | "delete", "rename", or "overwrite" |
---
## Selection
Multi-selection state with range selection support.
```rust
use fast_fs::nav::Selection;
let mut selection = Selection::new();
selection.select(Path::new("/path/to/file"));
selection.toggle(Path::new("/path/to/other"));
if selection.is_selected(Path::new("/path/to/file")) {
println!("Selected!");
}
for path in selection.iter() {
println!("{}", path.display());
}
```
### Basic Methods
| `new()` | Create empty selection |
| `select(path)` | Add path to selection |
| `deselect(path)` | Remove path from selection |
| `toggle(path)` | Toggle path selection |
| `clear()` | Clear all selections |
| `is_selected(path)` | Check if path is selected |
| `count()` | Number of selected items |
| `is_empty()` | Whether selection is empty |
| `iter()` | Iterator over selected paths |
| `paths()` | Vec of selected paths |
| `select_all(paths)` | Select multiple paths |
| `retain(predicate)` | Keep paths matching predicate |
### Range Selection Methods
Range selection uses an anchor point. When the user starts selecting (Shift+Arrow), the anchor is set. Moving with Shift held extends the selection from anchor to cursor. Moving without Shift clears the anchor.
| `anchor()` | `Option<usize>` | Get current anchor index |
| `set_anchor(index)` | `()` | Set anchor for range selection |
| `clear_anchor()` | `()` | Clear anchor (end range selection mode) |
| `has_anchor()` | `bool` | Check if range selection is active |
| `select_range(from, to, get_path)` | `()` | Select items from index `from` to `to` (inclusive) |
| `extend_to(from, to, get_path, total)` | `()` | Extend selection from anchor to new position |
**Range Selection Example:**
```rust
// The browser handles this internally via MoveUpExtend/MoveDownExtend,
// but you can also use Selection directly:
let mut selection = Selection::new();
let files: Vec<PathBuf> = get_visible_files();
// User presses Shift+Down from position 2
selection.set_anchor(2);
// User continues holding Shift and presses Down twice more
selection.extend_to(2, 4, |i| files.get(i).cloned(), files.len());
// Now items at indices 2, 3, 4 are selected
assert_eq!(selection.count(), 3);
// User releases Shift and presses Down (normal movement)
selection.clear_anchor(); // Range selection mode ends
```
---
## History
Navigation history with cursor memory.
```rust
use fast_fs::nav::History;
let mut history = History::new(50); // 50 entry limit
history.push("/home", 5); // Save position 5 at /home
if let Some(entry) = history.go_back("/current", 10) {
// entry.path = previous path
// entry.cursor = cursor position when we left
}
```
### Methods
| `new(limit)` | Create history with entry limit |
| `push(path, cursor)` | Push entry (clears forward stack) |
| `go_back(current_path, cursor)` | Navigate back |
| `go_forward(current_path, cursor)` | Navigate forward |
| `can_go_back()` | Whether back navigation available |
| `can_go_forward()` | Whether forward navigation available |
| `peek_back()` | Preview previous entry |
| `peek_forward()` | Preview forward entry |
| `back_count()` | Number of back entries |
| `forward_count()` | Number of forward entries |
| `clear()` | Clear all history |
### HistoryEntry
```rust
pub struct HistoryEntry {
pub path: PathBuf,
pub cursor: usize,
}
```
---
## FileCategory
File type categorization for UI.
```rust
use fast_fs::nav::FileCategory;
// From extension
let cat = FileCategory::from_extension("rs"); // FileCategory::Code
let cat = FileCategory::from_extension("png"); // FileCategory::Image
// From FileEntry
let cat = file_entry.category();
if cat.is_navigable() {
// It's a directory
}
```
### Variants
| `Directory` | (directories) |
| `Text` | txt, md, log, csv, json, yaml, xml, html, css |
| `Code` | rs, py, js, ts, go, c, cpp, java, rb, php, sh, etc. |
| `Image` | png, jpg, gif, svg, webp, bmp, ico, etc. |
| `Video` | mp4, mkv, avi, mov, webm |
| `Audio` | mp3, wav, flac, ogg, m4a |
| `Archive` | zip, tar, gz, 7z, rar |
| `Document` | pdf, doc, docx, xls, xlsx, ppt |
| `Executable` | exe, bat, cmd, ps1 (or +x on Unix) |
| `Symlink` | (symbolic links) |
| `Unknown` | (default) |
### Methods
| `from_extension(ext)` | Categorize by extension |
| `is_navigable()` | Whether category is Directory |
### FileEntry::category()
Resolution order:
1. If `is_symlink` → `Symlink`
2. If `is_dir` → `Directory`
3. If `is_executable()` → `Executable`
4. Otherwise → `from_extension()`
---
## NavError
Navigation-specific errors.
```rust
use fast_fs::nav::NavError;
match error {
NavError::NotFound(path) => { /* Path doesn't exist */ }
NavError::NotADirectory(path) => { /* Path is not a directory */ }
NavError::PermissionDenied(path) => { /* Access denied */ }
NavError::InvalidName(reason) => { /* Invalid filename */ }
NavError::NoPendingOperation => { /* No confirmation/input pending */ }
NavError::NoHistory => { /* History empty */ }
NavError::Io { path, source } => { /* I/O error with path context */ }
}
```
---
## Functions
### validate_name
Validate a filename for creation/rename.
```rust
use fast_fs::nav::validate_name;
match validate_name("new_file.txt") {
Ok(()) => { /* Valid */ }
Err(NavError::InvalidName(reason)) => {
println!("Invalid: {}", reason);
}
}
```
**Rejected names:**
- Empty strings
- `.` or `..`
- Names containing `/` or `\`
- Names containing null bytes or control characters
---
## Complete Example
```rust
use fast_fs::nav::{Browser, BrowserConfig, KeyInput, ActionResult};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create browser
let config = BrowserConfig::open_dialog()
.with_initial_path("/home/user");
let mut browser = Browser::new(config).await?;
let viewport_height = 20; // Your terminal height
// Event loop
loop {
// Render current state
render(&browser);
// Get user input (framework-specific)
let key = get_key_input();
// Handle page navigation (these aren't bound by default)
match &key {
KeyInput::PageUp => {
browser.page_up(viewport_height);
continue;
}
KeyInput::PageDown => {
browser.page_down(viewport_height);
continue;
}
_ => {}
}
// Process input through keymap
match browser.handle_key(key.clone()).await {
ActionResult::Done => continue,
ActionResult::DirectoryChanged => continue,
ActionResult::FileSelected(path) => {
println!("Selected: {}", path.display());
break;
}
ActionResult::NeedsConfirmation(op) => {
let confirmed = show_confirm_dialog(&op.message());
browser.resolve_confirmation(confirmed).await?;
}
ActionResult::NeedsInput(req) => {
let input = show_input_dialog(req.prompt(), req.current());
if let Some(value) = input {
browser.complete_input(&value).await?;
} else {
browser.cancel_input();
}
}
ActionResult::Unhandled => {
// Unbound character key - use for typeahead (Windows-style)
if let KeyInput::Char(c) = key {
browser.jump_to_char(c); // Cycles through matches on repeat
}
}
}
}
Ok(())
}
fn render(browser: &Browser) {
// Clear screen, draw files, cursor, selection, etc.
for (i, entry) in browser.files().iter().enumerate() {
let cursor = if i == browser.cursor() { ">" } else { " " };
let selected = if browser.selection().is_selected(&entry.path) { "*" } else { " " };
println!("{}{} {}", cursor, selected, entry.name);
}
}
```
### Incremental Search Example
For a more sophisticated search UI (like Ctrl+F in file managers):
```rust
// User presses Ctrl+F, enters "doc" incrementally
let mut search_query = String::new();
// On each keystroke in search mode:
search_query.push('d'); // User types 'd'
browser.jump_to_substring(&search_query); // Jumps to first match
search_query.push('o'); // User types 'o'
browser.jump_to_substring(&search_query); // Jumps to "doc..." file
// User presses Enter/Down to cycle through matches
browser.jump_to_substring(&search_query); // Advances to next "doc..." file
```