ferritin 0.8.0

Human-friendly CLI for browsing Rust documentation
#![allow(dead_code)]

use clap::Parser;

// Include the generated themes module
mod themes {
    include!(concat!(env!("OUT_DIR"), "/themes.rs"));
}
use ferritin_common::{
    Navigator,
    sources::{DocsRsSource, LocalSource, StdSource},
};
use std::{path::PathBuf, process::ExitCode};
use terminal_size::{Width, terminal_size};

use crate::{
    commands::Commands, format_context::FormatContext, render_context::RenderContext,
    renderer::OutputMode, request::Request,
};

mod color_scheme;
mod commands;
mod format;
mod format_context;
mod generate_docsrs_url;
mod indent;
mod logging;
mod markdown;
mod render_context;
mod renderer;
mod request;
mod styled_string;
#[cfg(test)]
mod tests;
mod traits;
mod verbosity;

#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

/// A friendly CLI for browsing Rust documentation
#[derive(Parser, Debug)]
#[command(name = "ferritin")]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Path to Cargo.toml (defaults to current directory)
    #[arg(short, long, global = true)]
    manifest_path: Option<PathBuf>,

    /// Syntax highlighting theme (theme name or path to .tmTheme file)
    #[arg(
        long,
        short,
        global = true,
        default_value = "Catppuccin Frappe",
        env = "FERRITIN_THEME",
        long_help = build_theme_help()
    )]
    theme: String,

    /// Enable interactive mode with scrolling and navigation
    #[arg(short, long, global = true)]
    interactive: bool,

    /// Use local workspace (implies --manifest-path cwd if not set). By default, docs.rs is used.
    #[arg(short = 'l', long, global = true)]
    local: bool,

    /// Output in LLM-friendly format (also enabled by CLAUDECODE or GEMINI_CLI env vars)
    #[arg(long, global = true)]
    ai: bool,

    #[command(subcommand)]
    command: Option<Commands>,
}

fn build_theme_help() -> &'static str {
    use std::sync::OnceLock;
    static HELP: OnceLock<String> = OnceLock::new();

    HELP.get_or_init(|| {
        let mut help = String::from("Syntax highlighting theme\n\n");
        help.push_str("Can be either:\n");
        help.push_str("  - A theme name from the list below\n");
        help.push_str("  - A path to a .tmTheme file\n\n");
        help.push_str("Available themes:\n");

        for name in themes::THEME_NAMES {
            help.push_str(&format!("  - {}\n", name));
        }

        help
    })
}

struct IoFmtWriter<T>(T);
impl<T> std::fmt::Write for IoFmtWriter<T>
where
    T: std::io::Write,
{
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error)
    }
}

fn main() -> ExitCode {
    let cli = Cli::parse();

    let use_local = cli.local || cli.manifest_path.is_some();
    let path = cli
        .manifest_path
        .unwrap_or_else(|| std::env::current_dir().unwrap());

    let mut output_mode = OutputMode::detect();
    if cli.ai {
        output_mode = OutputMode::Ai;
    }

    let mut render_context = RenderContext::new()
        .with_output_mode(output_mode)
        .with_terminal_width(
            terminal_size()
                .map(|(Width(w), _)| w as usize)
                .unwrap_or(80),
        )
        .with_interactive(cli.interactive);

    if let Err(e) = render_context.set_theme_name(&cli.theme) {
        eprintln!("{e}");
        return ExitCode::FAILURE;
    };

    if cli.interactive {
        // Interactive mode with scrolling and navigation
        // Install custom log backend that captures logs for status bar
        let (log_backend, log_reader) = logging::StatusLogBackend::new(10_000);
        if let Err(e) = log_backend.install() {
            eprintln!("Failed to install log backend: {}", e);
            return ExitCode::FAILURE;
        }

        if let Err(e) =
            renderer::render_interactive(path, use_local, render_context, cli.command, log_reader)
        {
            eprintln!("Interactive mode error: {}", e);
            return ExitCode::FAILURE;
        }
        return ExitCode::SUCCESS;
    }

    // Non-interactive mode: build sources eagerly and handle errors upfront
    let std_source = StdSource::from_rustup();
    let navigator = if use_local {
        let local_source = LocalSource::load(&path);
        if let Err(error) = &local_source {
            eprintln!("could not load rust project at {}", path.display());
            log::error!("{error:?}");
            return ExitCode::FAILURE;
        }
        Navigator::default()
            .with_std_source(std_source)
            .with_local_source(local_source.ok())
    } else {
        Navigator::default()
            .with_std_source(std_source)
            .with_docsrs_source(DocsRsSource::from_default_cache())
    };

    let format_context = FormatContext::new();
    let request = Request::new(navigator, format_context);

    // One-shot mode: execute command and render to stdout
    // Use env_logger for CLI mode
    env_logger::init();
    let (document, is_error, _initial_entry) =
        cli.command.unwrap_or_else(Commands::list).execute(&request);

    // Render to stdout and exit
    if renderer::render(
        &document,
        &render_context,
        &mut IoFmtWriter(std::io::stdout()),
    )
    .is_err()
    {
        return ExitCode::FAILURE;
    }

    if is_error {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}