fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
//! File browser example
//!
//! This example demonstrates the nav module's file browser state machine.
//! It's a simple terminal-based file browser for demonstration purposes.
//!
//! Run with: `cargo run --example file_browser`
//!
//! Controls:
//!   j/k or arrows - Move up/down
//!   Enter/l       - Open directory or select file
//!   h/Backspace   - Go to parent directory
//!   Space         - Toggle selection
//!   .             - Toggle hidden files
//!   s             - Cycle sort order
//!   q             - Quit

use fast_fs::nav::{ActionResult, Browser, BrowserConfig, KeyInput};
use std::io::{self, Write};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or_else(|| ".".to_string());

    println!("=== fast-fs File Browser Demo ===");
    println!("Starting at: {}\n", path);

    // Create browser with open_dialog preset
    let config = BrowserConfig::open_dialog()
        .with_initial_path(&path)
        .with_show_parent_entry(true);

    let mut browser = Browser::at_path(&path, config).await?;

    // Simple event loop
    loop {
        // Clear screen and render
        print!("\x1B[2J\x1B[1;1H"); // ANSI clear screen
        render(&browser);

        // Get key input
        print!("\nCommand (j/k/Enter/h/Space/./s/q): ");
        io::stdout().flush()?;

        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        let input = input.trim();

        // Map input to KeyInput
        let key = match input {
            "j" => KeyInput::Down,
            "k" => KeyInput::Up,
            "l" | "" => KeyInput::Enter, // Enter key
            "h" => KeyInput::Backspace,
            " " => KeyInput::Char(' '),
            "." => KeyInput::Char('.'),
            "s" => KeyInput::Char('s'),
            "g" => KeyInput::Char('g'),
            "G" => KeyInput::Char('G'),
            "q" => break,
            c if c.len() == 1 => {
                // Single character - try typeahead
                let ch = c.chars().next().unwrap();
                if browser.jump_to_char(ch) {
                    continue;
                }
                KeyInput::Char(ch)
            }
            _ => continue,
        };

        // Handle key
        match browser.handle_key(key).await {
            ActionResult::Done => {}
            ActionResult::DirectoryChanged => {
                println!("\nChanged to: {}", browser.current_path().display());
            }
            ActionResult::FileSelected(path) => {
                println!("\n\nSelected file: {}", path.display());
                println!("Press Enter to continue...");
                let mut buf = String::new();
                io::stdin().read_line(&mut buf)?;
            }
            ActionResult::Clipboard(state) => {
                println!("\n{}", state.message());
            }
            ActionResult::NeedsConfirmation(op) => {
                println!("\nConfirm: {} (y/n)", op.message());
                let mut buf = String::new();
                io::stdin().read_line(&mut buf)?;
                let confirmed = buf.trim().to_lowercase() == "y";
                browser.resolve_confirmation(confirmed).await?;
            }
            ActionResult::NeedsInput(req) => {
                print!("\n{} ", req.prompt());
                io::stdout().flush()?;
                let mut buf = String::new();
                io::stdin().read_line(&mut buf)?;
                let input = buf.trim();
                if !input.is_empty() {
                    browser.complete_input(input).await?;
                } else {
                    browser.cancel_input();
                }
            }
            ActionResult::Unhandled => {}
        }
    }

    println!("\nGoodbye!");
    Ok(())
}

fn render(browser: &Browser) {
    // Header
    println!("Directory: {}", browser.current_path().display());
    println!(
        "Sort: {:?} | Hidden: {} | Selected: {}",
        browser.files().sort_by(),
        browser.config().show_hidden,
        browser.selection().count()
    );
    println!("{}", "-".repeat(60));

    // File list
    let viewport_height = 15;
    let range = browser.visible_range(viewport_height);
    let range_start = range.start;
    let range_end = range.end;

    for i in range {
        if let Some(entry) = browser.files().get(i) {
            let cursor = if i == browser.cursor() { ">" } else { " " };
            let selected = if browser.selection().is_selected(&entry.path) {
                "*"
            } else {
                " "
            };
            let kind = if entry.is_dir { "/" } else { " " };
            let hidden = if entry.is_hidden { "." } else { " " };

            println!("{}{}{}{} {}", cursor, selected, hidden, kind, entry.name);
        }
    }

    // Footer
    println!("{}", "-".repeat(60));
    println!(
        "Total: {} | Showing: {}-{} of {}",
        browser.total_count(),
        range_start + 1,
        range_end,
        browser.filtered_count()
    );
}