ratatree 0.1.0

A terminal file/directory picker widget for ratatui
Documentation

ratatree

A file and directory picker widget for ratatui.

Drop it into any ratatui app. Your users get a full-featured file browser with keyboard navigation, fuzzy search, multi-select, and two view modes - all from a single widget.

┌─────────────────────────────────────────┐
│ ~/projects/ratatree/src                │
│─────────────────────────────────────────│
│   📁 view/                              │
│ ▸ 📄 lib.rs                             │
│   ✓ 📄 state.rs                         │
│   🔗 config -> ../config                │
│─────────────────────────────────────────│
│ 1 selected | . hidden | / search        │
└─────────────────────────────────────────┘

Quick Start

Add to your Cargo.toml:

[dependencies]
ratatree = { git = "https://github.com/namil-k/ratatree.git" }

Three things to know:

  1. FilePickerState holds all the state (current directory, cursor, selections)
  2. FilePicker renders it (implements ratatui's StatefulWidget)
  3. PickerResult tells you what the user did (still browsing, picked files, or cancelled)
use ratatree::{FilePicker, FilePickerState, PickerMode, PickerResult};

// Create the picker
let mut state = FilePickerState::builder()
    .start_dir("~/projects")
    .mode(PickerMode::Both)       // files and directories
    .build();

// In your event loop:
loop {
    terminal.draw(|f| {
        f.render_stateful_widget(FilePicker::default(), f.area(), &mut state);
    })?;

    if let Event::Key(key) = crossterm::event::read()? {
        state.handle_event(Event::Key(key));
    }

    match state.result() {
        PickerResult::Selected(paths) => {
            // User picked these files/directories
            break;
        }
        PickerResult::Cancelled => break,
        PickerResult::Pending => {}
    }
}

Features

  • List and Tree views - toggle with Tab
  • Vim keybindings - hjkl, gg, G, Ctrl+D/U (arrow keys too)
  • Fuzzy search - press / and start typing
  • Multi-select - Space to toggle, Enter to confirm
  • Hidden files - toggle with .
  • Symlink support - follows symlinks with circular reference detection
  • Filter callback - show only the files you want
  • Themeable - customize every color via FilePickerTheme

Builder Options

let mut state = FilePickerState::builder()
    .start_dir("~/Documents")           // starting directory (default: ".")
    .mode(PickerMode::FilesOnly)        // FilesOnly | DirsOnly | Both
    .view(ViewMode::Tree)               // List | Tree (default: List)
    .show_hidden(true)                  // show dotfiles (default: false)
    .filter(|path| {                    // custom filter
        path.extension()
            .map(|e| e == "rs" || e == "toml")
            .unwrap_or(true)            // always show directories
    })
    .theme(my_theme)                    // custom FilePickerTheme
    .build();

Key Bindings

Navigation

Key Action
j / Down Move cursor down
k / Up Move cursor up
l / Right Enter directory
h / Left / Backspace Go to parent directory
gg Jump to top
G Jump to bottom
Ctrl+D Half page down
Ctrl+U Half page up
~ Go to home directory

Actions

Key Action
Enter Confirm selection (or enter directory)
Space Toggle multi-select on current item
Esc / q Cancel
Tab Switch between List and Tree view
. Toggle hidden files
/ Start fuzzy search

Search Mode

Key Action
Type Filter entries in real time
Enter Accept filter, return to normal mode
Esc Clear filter, return to normal mode
j/k / Up/Down Navigate within results

Theming

Every visual element is customizable:

use ratatree::FilePickerTheme;
use ratatui::style::{Color, Modifier, Style};

let theme = FilePickerTheme {
    normal: Style::default(),
    cursor: Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD),
    selected: Style::default().fg(Color::Green),
    directory: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
    symlink: Style::default().fg(Color::Cyan),
    path_bar: Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
    status_bar: Style::default().fg(Color::DarkGray),
    search_input: Style::default().fg(Color::Yellow),
    error: Style::default().fg(Color::Red),
};

let mut state = FilePickerState::builder()
    .theme(theme)
    .build();

Integration with Your App

A common pattern is to show the picker as a modal overlay:

struct App {
    picker_state: Option<FilePickerState>,
    // ... your app state
}

impl App {
    fn open_picker(&mut self) {
        self.picker_state = Some(
            FilePickerState::builder()
                .start_dir(".")
                .mode(PickerMode::FilesOnly)
                .build()
        );
    }

    fn handle_event(&mut self, event: Event) {
        if let Some(picker) = &mut self.picker_state {
            picker.handle_event(event);
            match picker.result() {
                PickerResult::Selected(paths) => {
                    self.on_files_selected(paths);
                    self.picker_state = None;
                }
                PickerResult::Cancelled => {
                    self.picker_state = None;
                }
                PickerResult::Pending => {}
            }
        } else {
            // your normal event handling
        }
    }

    fn render(&mut self, frame: &mut Frame) {
        if let Some(picker) = &mut self.picker_state {
            frame.render_stateful_widget(FilePicker::default(), frame.area(), picker);
        } else {
            // your normal rendering
        }
    }
}

Running the Example

cargo run --example basic

License

MIT