# fast-fs Quick Start Guide
Get up and running with fast-fs in 5 minutes.
> **Note:** The nav module is fully async for non-blocking I/O performance.
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
fast-fs = "0.2"
tokio = { version = "1.48", features = ["rt-multi-thread", "fs"] }
```
## Part 1: Reading Directories
### Basic Directory Reading
```rust
use fast_fs::read_dir;
#[tokio::main]
async fn main() -> Result<(), fast_fs::Error> {
let entries = read_dir(".").await?;
for entry in entries {
let kind = if entry.is_dir { "DIR " } else { "FILE" };
println!("{} {} ({} bytes)", kind, entry.name, entry.size);
}
Ok(())
}
```
### Recursive with Filtering
```rust
use fast_fs::{read_dir_recursive, TraversalOptions};
#[tokio::main]
async fn main() -> Result<(), fast_fs::Error> {
let options = TraversalOptions::default()
.with_max_depth(3) // Limit depth
.with_extensions(&["rs"]) // Only .rs files
.with_gitignore(true); // Respect .gitignore
let entries = read_dir_recursive(".", options).await?;
println!("Found {} Rust files", entries.len());
Ok(())
}
```
## Part 2: Building a File Browser
The `nav` module provides everything you need for file browsing UI.
### Step 1: Create the Browser
```rust
use fast_fs::nav::{Browser, BrowserConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Choose a preset
let config = BrowserConfig::open_dialog(); // Read-only
// Or: BrowserConfig::save_dialog() // Writable
// Or: BrowserConfig::project_explorer() // Full-featured
let mut browser = Browser::new(config).await?;
println!("Current directory: {}", browser.current_path().display());
println!("Files: {}", browser.files().len());
Ok(())
}
```
### Step 2: Handle Key Input
```rust
use fast_fs::nav::{Browser, BrowserConfig, KeyInput, ActionResult};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = BrowserConfig::open_dialog();
let mut browser = Browser::new(config).await?;
// Simulate user pressing 'j' (move down)
let result = browser.handle_key(KeyInput::Char('j')).await;
match result {
ActionResult::Done => {
println!("Cursor now at: {}", browser.cursor());
}
ActionResult::DirectoryChanged => {
println!("Changed to: {}", browser.current_path().display());
}
ActionResult::FileSelected(path) => {
println!("Selected: {}", path.display());
}
ActionResult::Unhandled => {
println!("Key not bound");
}
_ => {}
}
Ok(())
}
```
### Step 3: Complete Event Loop
```rust
use fast_fs::nav::{Browser, BrowserConfig, KeyInput, ActionResult};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = BrowserConfig::open_dialog()
.with_initial_path("/home");
let mut browser = Browser::new(config).await?;
loop {
// 1. Render the UI
render(&browser);
// 2. Get user input (framework-specific)
let key = get_key(); // You implement this
// 3. Process the key
match browser.handle_key(key).await {
ActionResult::Done => {
// Simple action completed, just re-render
continue;
}
ActionResult::DirectoryChanged => {
// Directory changed, re-render
continue;
}
ActionResult::FileSelected(path) => {
// User selected a file - we're done!
println!("You selected: {}", path.display());
return Ok(());
}
ActionResult::NeedsConfirmation(op) => {
// Show confirmation dialog
let confirmed = show_confirm(&op.message());
browser.resolve_confirmation(confirmed).await?;
}
ActionResult::NeedsInput(req) => {
// Show input dialog
if let Some(input) = show_input(req.prompt(), req.current()) {
browser.complete_input(&input).await?;
} else {
browser.cancel_input();
}
}
ActionResult::Clipboard(state) => {
// Store clipboard state for later paste
println!("{}", state.message()); // "copy 3 items"
// app_clipboard = Some(state);
}
ActionResult::Unhandled => {
// Try jump-to-char for letters
if let KeyInput::Char(c) = key {
browser.jump_to_char(c);
}
}
}
}
}
fn render(browser: &Browser) {
// Clear screen
print!("\x1B[2J\x1B[H");
// Show path
println!("=== {} ===\n", browser.current_path().display());
// Show files
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 {
" "
};
let kind = if entry.is_dir { "/" } else { "" };
println!("{}{} {}{}", cursor, selected, entry.name, kind);
}
// Show status
println!("\n[{}/{}]", browser.cursor() + 1, browser.files().len());
}
// Stub implementations - replace with your framework
fn get_key() -> KeyInput { KeyInput::Char('q') }
fn show_confirm(_msg: &str) -> bool { true }
fn show_input(_prompt: &str, _current: Option<String>) -> Option<String> { None }
```
## Part 3: Key Bindings Reference
Default vim-style bindings:
| `j` / `Down` | MoveDown | Move cursor down |
| `k` / `Up` | MoveUp | Move cursor up |
| `Shift+Down` | MoveDownExtend | Extend range selection down |
| `Shift+Up` | MoveUpExtend | Extend range selection up |
| `g` | MoveToTop | Jump to first item |
| `G` | MoveToBottom | Jump to last item |
| `Enter` / `l` | Enter | Open directory or select file |
| `h` / `Backspace` | GoParent | Go to parent directory |
| `H` | GoBack | History back |
| `L` | GoForward | History forward |
| `Space` | ToggleSelect | Toggle selection |
| `Ctrl+a` | SelectAll | Select all visible |
| `Esc` | ClearSelection | Clear selection |
| `Ctrl+x` | Cut | Cut to clipboard |
| `Ctrl+c` | Copy | Copy to clipboard |
| `d` | Delete | Delete selected |
| `r` | Rename | Rename current |
| `n` | CreateDir | New directory |
| `N` | CreateFile | New file |
| `.` | ToggleHidden | Show/hide hidden files |
| `s` | CycleSort | Change sort order (incl. extension) |
| `/` | StartFilter | Enter filter mode |
| `Ctrl-l` | ClearFilter | Clear filter |
| `Ctrl-g` | StartPathInput | Go to path |
| `Ctrl-r` | Refresh | Reload directory |
## Part 4: Page Navigation & Typeahead
These features require caller handling (not in keymap):
### Page Up/Down
```rust
// The browser needs your viewport height
let viewport_height = terminal_height;
match key {
KeyInput::PageUp => browser.page_up(viewport_height),
KeyInput::PageDown => browser.page_down(viewport_height),
other => {
browser.handle_key(other).await;
}
}
```
### Typeahead Search (Windows-style)
```rust
// Single character jump - cycles through matches on repeat
if let ActionResult::Unhandled = browser.handle_key(key.clone()).await {
if let KeyInput::Char(c) = key {
browser.jump_to_char(c); // Press 'a' repeatedly to cycle 'a' files
}
}
```
### Substring Search
```rust
// Jump to files containing a substring
browser.jump_to_substring("doc"); // Jumps to "document.pdf", "readme.doc", etc.
// Cycles through matches on repeat
browser.jump_to_substring("doc"); // Next match
browser.jump_to_substring("doc"); // Next match (wraps around)
```
### Range Selection (Shift+Arrow)
Range selection works automatically via `MoveUpExtend` / `MoveDownExtend` actions:
```rust
// User holds Shift and presses Down repeatedly
// This is handled by the keymap: Shift+Down -> MoveDownExtend
browser.handle_key(KeyInput::ShiftDown).await; // Sets anchor, selects current
browser.handle_key(KeyInput::ShiftDown).await; // Extends selection down
browser.handle_key(KeyInput::ShiftDown).await; // Extends selection down
// User releases Shift and moves normally
browser.handle_key(KeyInput::Down).await; // Anchor cleared, selection preserved
```
### Clipboard (Cut/Copy)
The library tracks cut/copy intent. You handle actual clipboard and paste:
```rust
use fast_fs::nav::{ActionResult, ClipboardOp, ClipboardState};
let mut app_clipboard: Option<ClipboardState> = None;
match browser.handle_key(KeyInput::Ctrl('c')).await {
ActionResult::Clipboard(state) => {
println!("{}", state.message()); // "copy 'file.txt'" or "copy 3 items"
app_clipboard = Some(state);
}
_ => {}
}
// When user wants to paste (you implement this key):
if let Some(ref clipboard) = app_clipboard {
// Check for conflicts
let conflicts = clipboard.would_conflict(browser.current_path());
if !conflicts.is_empty() {
// Ask user: overwrite existing files?
}
// Perform actual file operations
match clipboard.operation {
ClipboardOp::Cut => {
for src in &clipboard.paths {
let dest = browser.current_path().join(src.file_name().unwrap());
std::fs::rename(src, dest)?; // Move file
}
}
ClipboardOp::Copy => {
for src in &clipboard.paths {
let dest = browser.current_path().join(src.file_name().unwrap());
std::fs::copy(src, dest)?; // Copy file
}
}
}
// Refresh to show changes
browser.refresh().await?;
}
```
## Part 5: Common Patterns
### Custom Key Bindings
```rust
use fast_fs::nav::{KeyMap, KeyInput, Action, BrowserConfig};
// Option 1: Start fresh with minimal bindings
let mut keymap = KeyMap::empty();
keymap.bind(KeyInput::Up, Action::MoveUp);
keymap.bind(KeyInput::Down, Action::MoveDown);
keymap.bind(KeyInput::Enter, Action::Enter);
keymap.bind(KeyInput::Backspace, Action::GoParent);
let config = BrowserConfig::default().with_keymap(keymap);
// Option 2: Start with defaults and customize
let mut keymap = KeyMap::default();
// Free Ctrl+C for terminal interrupt (common requirement)
keymap.unbind(KeyInput::Ctrl('c'));
keymap.unbind(KeyInput::Ctrl('x'));
// Use vim-style yank instead
keymap.bind(KeyInput::Char('y'), Action::Copy);
keymap.bind(KeyInput::Char('x'), Action::Cut);
let config = BrowserConfig::default().with_keymap(keymap);
```
### Filtering Files
```rust
// Programmatic filter
browser.set_filter("*.rs"); // Glob pattern
browser.set_filter("test"); // Substring match
// Or let user type filter
if let ActionResult::NeedsInput(req) = browser.handle_key(KeyInput::Char('/')).await {
// Show input UI, then:
browser.complete_input("*.rs").await?;
}
// Clear filter
browser.clear_filter();
```
### Multi-Selection
```rust
// Toggle current item
browser.handle_key(KeyInput::Char(' ')).await;
// Check selection
for path in browser.selected_paths() {
println!("Selected: {}", path.display());
}
// Get paths to operate on (selection or current)
let targets = browser.selected_or_current();
```
### File Categories for Icons
```rust
use fast_fs::nav::FileCategory;
for entry in browser.files().iter() {
let icon = match entry.category() {
FileCategory::Directory => "📁",
FileCategory::Code => "📄",
FileCategory::Image => "🖼️",
FileCategory::Video => "🎬",
FileCategory::Audio => "🎵",
FileCategory::Archive => "📦",
FileCategory::Document => "📝",
FileCategory::Executable => "⚙️",
FileCategory::Symlink => "🔗",
_ => "📄",
};
println!("{} {}", icon, entry.name);
}
```
### Breadcrumb Navigation
```rust
// Get breadcrumbs
let crumbs = browser.breadcrumbs();
// [("home", "/home"), ("user", "/home/user"), ("project", "/home/user/project")]
// Navigate to breadcrumb
browser.go_to_breadcrumb(1).await?; // Goes to /home/user
```
## Next Steps
- [API.md](API.md) - Complete API reference
- [PRD_LIBRARY_UPGRADE.md](PRD_LIBRARY_UPGRADE.md) - Architecture and design decisions
## Framework Integration Examples
### Ratatui
```rust
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use fast_fs::nav::KeyInput;
fn crossterm_to_keyinput(key_event: KeyEvent) -> Option<KeyInput> {
let KeyEvent { code, modifiers, .. } = key_event;
// Handle Shift+Arrow for range selection
if modifiers.contains(KeyModifiers::SHIFT) {
return match code {
KeyCode::Up => Some(KeyInput::ShiftUp),
KeyCode::Down => Some(KeyInput::ShiftDown),
_ => None,
};
}
// Handle Ctrl combinations
if modifiers.contains(KeyModifiers::CONTROL) {
if let KeyCode::Char(c) = code {
return Some(KeyInput::Ctrl(c));
}
}
Some(match code {
KeyCode::Up => KeyInput::Up,
KeyCode::Down => KeyInput::Down,
KeyCode::Left => KeyInput::Left,
KeyCode::Right => KeyInput::Right,
KeyCode::Enter => KeyInput::Enter,
KeyCode::Backspace => KeyInput::Backspace,
KeyCode::Esc => KeyInput::Escape,
KeyCode::Tab => KeyInput::Tab,
KeyCode::PageUp => KeyInput::PageUp,
KeyCode::PageDown => KeyInput::PageDown,
KeyCode::Char(c) => KeyInput::Char(c),
KeyCode::F(n) => KeyInput::F(n),
_ => return None,
})
}
```
### egui
```rust
use egui::{Key, Modifiers};
use fast_fs::nav::KeyInput;
fn egui_to_keyinput(key: Key, modifiers: Modifiers) -> Option<KeyInput> {
// Handle Shift+Arrow for range selection
if modifiers.shift {
return match key {
Key::ArrowUp => Some(KeyInput::ShiftUp),
Key::ArrowDown => Some(KeyInput::ShiftDown),
_ => None,
};
}
// Handle Ctrl combinations
if modifiers.ctrl {
return match key {
Key::A => Some(KeyInput::Ctrl('a')),
Key::C => Some(KeyInput::Ctrl('c')),
Key::X => Some(KeyInput::Ctrl('x')),
Key::R => Some(KeyInput::Ctrl('r')),
_ => None,
};
}
Some(match key {
Key::ArrowUp => KeyInput::Up,
Key::ArrowDown => KeyInput::Down,
Key::ArrowLeft => KeyInput::Left,
Key::ArrowRight => KeyInput::Right,
Key::Enter => KeyInput::Enter,
Key::Escape => KeyInput::Escape,
Key::PageUp => KeyInput::PageUp,
Key::PageDown => KeyInput::PageDown,
_ => return None,
})
}
```