Introduction

This post explores the journey of building a terminal user interface (TUI) file manager in Rust. We'll examine Blaze Ultra, a practical CLI application that demonstrates key concepts in Rust development including terminal manipulation, event handling, and modern UI frameworks.

Why Rust for CLI Applications?

Rust has become increasingly popular for CLI tools due to several compelling advantages:

  • Performance: Near C-level performance with zero-cost abstractions
  • Safety: Memory safety without garbage collection prevents common bugs
  • Ergonomics: Modern tooling with Cargo makes distribution trivial
  • Cross-platform: Write once, compile anywhere with minimal platform-specific code
  • Rich Ecosystem: Mature crates like clap, ratatui, and tokio accelerate development

Popular examples include ripgrep, fd, bat, and exa - all demonstrating Rust's capability to create fast, reliable command-line tools.

Project Overview: Blaze Ultra

Blaze Ultra is a TUI file manager that combines several powerful features:

  • Interactive file browsing with keyboard navigation
  • Fuzzy search powered by the skim library
  • Live preview of file contents
  • Multi-panel layout for efficient file management
  • Vim-style keybindings for power users

The project structure is intentionally minimal, with the core logic contained in a single main.rs file (~200 lines), making it an excellent learning resource.

Core Dependencies

Let's examine the key dependencies that power this application:

[dependencies]
clap = { version = "4.5", features = ["derive", "wrap_help", "color"] }
ratatui = "0.28"
crossterm = { version = "0.27", features = ["event-stream"] }
tokio = { version = "1", features = ["full"] }
skim = "0.10"
walkdir = "2"
bytesize = "1.3"
syntect = "5.2"

Key Libraries Explained

clap: The de-facto standard for command-line argument parsing. Using the derive macros makes defining CLI interfaces declarative and type-safe:

#[derive(Parser)]
#[command(name = "blaze", about = "TUI File Commander 2025", version)]
struct Args {
    #[arg(default_value = ".")]
    path: PathBuf,
}

ratatui: A modern TUI framework (fork of tui-rs) that provides widgets, layouts, and rendering primitives. It follows a retained-mode architecture where you describe what to render on each frame.

crossterm: Cross-platform terminal manipulation library handling raw mode, keyboard events, cursor control, and alternate screen buffers.

skim: A fuzzy finder library (similar to fzf) that enables fast, interactive searching through large datasets.

walkdir: Efficient directory traversal with configurable depth and filtering.

Application Architecture

State Management

The application state is encapsulated in a simple struct:

struct App {
    current_path: PathBuf,
    entries: Vec<walkdir::DirEntry>,
    selected: usize,
    active_panel: Panel,
    preview: String,
    search_query: String,
}

This immutable-first approach makes state transitions predictable. When navigating to a new directory, we create a fresh App instance rather than mutating deeply nested state.

Event Loop Pattern

The core of any TUI application is the event loop:

loop {
    terminal.draw(|f| ui(f, &app))?;
    
    if let crossterm::event::Event::Key(key) = read()? {
        match key.code {
            KeyCode::Char('q') => break,
            KeyCode::Down => /* navigate down */,
            KeyCode::Enter => /* open/preview */,
            _ => {}
        }
    }
}

This pattern:

  1. Renders the current state
  2. Blocks waiting for user input
  3. Processes events and updates state
  4. Repeats

This synchronous approach works well for keyboard-driven applications. For more complex scenarios with async I/O, you'd integrate tokio more deeply.

Terminal Management

Proper terminal setup and cleanup is critical:

fn setup_terminal() -> Result<Terminal<CrosstermBackend<std::io::Stdout>>, Box<dyn std::error::Error>> {
    enable_raw_mode()?;
    let mut stdout = std::io::stdout();
    execute!(stdout, EnterAlternateScreen, Hide)?;
    Ok(Terminal::new(CrosstermBackend::new(stdout))?)
}

fn restore_terminals(terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>) 
    -> Result<(), Box<dyn std::error::Error>> {
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen, Show)?;
    Ok(())
}

Raw mode disables line buffering and canonical processing, giving the application direct access to keypresses. The alternate screen preserves the user's terminal state, returning them to their original view on exit.

Building the UI with Ratatui

Ratatui uses a declarative layout system with constraints:

fn ui(f: &mut Frame, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
        .split(f.area());
    
    // Left panel: file list
    let list = List::new(items)
        .block(Block::default().title("Files").borders(Borders::ALL))
        .highlight_style(Style::default().bg(Color::Magenta));
    
    // Right panel: preview
    let preview = Paragraph::new(app.preview.as_str())
        .block(Block::default().title("Preview").borders(Borders::ALL));
    
    f.render_widget(list, chunks[0]);
    f.render_widget(preview, chunks[1]);
}

The layout is recalculated on every frame based on terminal size, making the UI responsive without additional code.

Advanced Features

Fuzzy Search Integration

The fuzzy search temporarily exits the main TUI, launches skim's interface, then returns:

fn fuzzy_search(app: &mut App, terminal: &mut Terminal<...>) -> Result<...> {
    restore_terminals(terminal)?;  // Exit TUI mode
    
    let items: Vec<String> = app.entries.iter()
        .map(|e| e.path().display().to_string())
        .collect();
    
    let options = SkimOptionsBuilder::default()
        .multi(false)
        .build()?;
    
    let (tx, rx) = unbounded();
    for item in items {
        tx.send(Arc::new(item))?;
    }
    
    let input = Skim::run_with(&options, Some(rx));
    
    // Process selection...
    
    *terminal = setup_terminal()?;  // Re-enter TUI mode
    Ok(())
}

This pattern of temporarily yielding terminal control to external tools is common in TUI applications.

File Preview with Safety

File reading includes error handling and size limits:

fn read_preview(path: &Path) -> String {
    if path.is_dir() {
        return "📁 DIRECTORY".to_string();
    }
    std::fs::read_to_string(path)
        .unwrap_or_else(|_| "Cannot read file".into())
        .lines()
        .take(50)  // Limit preview size
        .collect::<Vec<_>>()
        .join("\n")
}

This prevents loading massive files into memory and handles binary files gracefully.

Performance Optimizations

The release profile in Cargo.toml is aggressively optimized:

[profile.release]
lto = true              # Link-time optimization
opt-level = 'z'         # Optimize for size
strip = true            # Remove debug symbols
panic = "abort"         # Smaller panic handler
codegen-units = 1       # Better optimization (slower compile)

These settings produce a ~2MB binary that's blazingly fast while maintaining Rust's safety guarantees.

Key Takeaways

Building CLI applications in Rust teaches several important concepts:

  1. Terminal Abstractions: Understanding raw mode, alternate screens, and event handling
  2. UI Frameworks: Declarative layouts with constraint-based sizing
  3. State Management: Immutable patterns for predictable updates
  4. Error Handling: Result<T, E> types force explicit error consideration
  5. Zero-Cost Abstractions: High-level APIs that compile to efficient machine code

Next Steps

To extend this project, consider:

  • Async file operations: Use tokio::fs for non-blocking I/O
  • Syntax highlighting: Integrate syntect for code preview
  • File operations: Add copy, move, delete with confirmation dialogs
  • Bookmarks: Persistent favorite directories with serde
  • Configuration: User-customizable keybindings and colors
  • Testing: Unit tests for state transitions and integration tests for UI

Resources

Conclusion

Rust's combination of performance, safety, and excellent library ecosystem makes it ideal for CLI development. Projects like Blaze Ultra demonstrate that you can build sophisticated terminal applications with relatively little code while maintaining the robustness Rust is known for.

The patterns explored here - event loops, terminal management, declarative UIs, and structured error handling - form the foundation for any Rust CLI application. Whether you're building file managers, system monitors, or development tools, these concepts will serve you well.

Happy coding! 🦀