claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
use std::io;
use std::time::Duration;

use claude_dashboard::app::App;
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;

fn main() -> Result<(), String> {
    // 1. Parse CLI args
    let cli = claude_dashboard::utils::parse_cli_args();

    // 2. Locate .claude directory
    let claude_dir = claude_dashboard::utils::find_claude_dir(cli.data_dir.as_deref())
        .ok_or_else(|| {
            eprintln!("Claude data directory not found.");
            eprintln!("Expected at: %USERPROFILE%\\.claude or ~/.claude");
            eprintln!("Use --data-dir <PATH> to specify a custom location");
            "Claude data directory not found".to_string()
        })?;

    // 2. Parse data files
    let stats = claude_dashboard::data::stats::parse_stats_cache(&claude_dir.join("stats-cache.json"))
        .unwrap_or_else(|e| {
            eprintln!("Warning: Could not parse stats-cache.json: {}", e);
            Default::default()
        });

    let history = claude_dashboard::data::history::parse_history(&claude_dir.join("history.jsonl"))
        .unwrap_or_else(|e| {
            eprintln!("Warning: Could not parse history.jsonl: {}", e);
            Vec::new()
        });

    let project_dirs = claude_dashboard::data::projects::discover_projects(&claude_dir.join("projects"), &history)
        .unwrap_or_else(|e| {
            eprintln!("Warning: Could not discover projects: {}", e);
            Vec::new()
        });

    if project_dirs.is_empty() {
        eprintln!("Warning: No projects found in {}/projects", claude_dir.display());
    }

    // 3. Initialize terminal
    enable_raw_mode().map_err(|e| format!("Failed to enable raw mode: {}", e))?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
        .map_err(|e| format!("Failed to enter alternate screen: {}", e))?;

    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)
        .map_err(|e| format!("Failed to create terminal: {}", e))?;

    // 4. Initialize i18n
    claude_dashboard::i18n::init();

    // 5. Create app state
    let mut app = App::new(stats, history, project_dirs, cli.start_tab, claude_dir);

    // 5. Event loop
    let tick_rate = Duration::from_millis(250);
    let result = run_event_loop(&mut terminal, &mut app, tick_rate);

    // 6. Cleanup
    let _ = disable_raw_mode();
    let _ = execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    );

    result
}

fn run_event_loop(
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    app: &mut App,
    tick_rate: Duration,
) -> Result<(), String> {
    loop {
        terminal
            .draw(|f| claude_dashboard::ui::render(f, app))
            .map_err(|e| format!("Failed to draw frame: {}", e))?;

        if event::poll(tick_rate).map_err(|e| format!("Event poll error: {}", e))? {
            if let Event::Key(key) = event::read().map_err(|e| format!("Event read error: {}", e))? {
                // Only handle Press and Repeat; skip Release to avoid double-firing
                if key.kind == KeyEventKind::Release {
                    continue;
                }
                if key.code == KeyCode::Char('q') || key.code == KeyCode::Esc {
                    return Ok(());
                }
                app.handle_event(key);
            }
        }

        app.tick();
    }
}