claude-code-status-line 1.2.5

A configurable status line for Claude Code with powerline arrows, context tracking, and quota monitoring
Documentation
use chrono::Utc;
use std::io::{self, Read};

use claude_code_status_line::config::{load_config, Config};
use claude_code_status_line::context::{calculate_context, format_ctx_full, format_ctx_short};
use claude_code_status_line::git::get_git_info;
use claude_code_status_line::quota::{
    format_quota_compact, format_quota_display, format_time_remaining, get_quota,
};
use claude_code_status_line::render::get_details_and_fg_codes;
use claude_code_status_line::statusline::build_statusline;
use claude_code_status_line::types::{self, ClaudeInput, Section};
use claude_code_status_line::utils::{get_terminal_width, get_thinking_mode_enabled, get_username};

fn format_duration_ms(ms: u64) -> String {
    let total_seconds = ms / 1000;
    let total_minutes = total_seconds / 60;
    let total_hours = total_minutes / 60;
    let total_days = total_hours / 24;

    if total_days >= 1 {
        let hours = total_hours % 24;
        format!("{}d {}h", total_days, hours)
    } else if total_hours >= 1 {
        let minutes = total_minutes % 60;
        format!("{}h {}m", total_hours, minutes)
    } else if total_minutes >= 1 {
        let seconds = total_seconds % 60;
        format!("{}m {}s", total_minutes, seconds)
    } else {
        format!("{}s", total_seconds.max(1))
    }
}

fn format_cost_display(cost: Option<&types::Cost>, config: &Config) -> Option<String> {
    cost.and_then(|c| c.total_cost_usd)
        .filter(|&usd| usd > 0.0)
        .map(|usd| {
            let mut result = format!("${:.2}", usd);

            if config.sections.cost.show_durations {
                let mut details_parts = Vec::new();

                if let Some(total_ms) = cost.and_then(|c| c.total_duration_ms) {
                    details_parts.push(format_duration_ms(total_ms));
                }

                if let Some(api_ms) = cost.and_then(|c| c.total_api_duration_ms) {
                    details_parts.push(format!("api: {}", format_duration_ms(api_ms)));
                }

                if !details_parts.is_empty() {
                    let (details, fg) = get_details_and_fg_codes(&config.theme.cost, config);
                    result.push_str(&format!(
                        " {}({}){}",
                        details,
                        details_parts.join(&config.display.details_separator),
                        fg
                    ));
                }
            }

            result
        })
}

fn main() {
    const VERSION: &str = env!("CARGO_PKG_VERSION");
    const PKG_NAME: &str = env!("CARGO_PKG_NAME");

    if std::env::args().any(|a| a == "--version" || a == "-V") {
        println!("{} {}", PKG_NAME, VERSION);
        return;
    }

    if std::env::args().any(|a| a == "--help" || a == "-h") {
        println!(
            "{} {} - Beautiful statusline for Claude Code",
            PKG_NAME, VERSION
        );
        println!("\nUsage:");
        println!(
            "  Receives JSON on stdin from Claude Code, outputs formatted statusline to stdout"
        );
        println!("\nOptions:");
        println!("  --version, -V    Show version");
        println!("  --help, -h       Show this help");
        println!("\nConfiguration:");
        println!("  ~/.claude/statusline/settings.json   - Statusline settings");
        println!("  ~/.claude/statusline/colors.json     - Color customization");
        println!("\nSlash Commands:");
        println!("  /install-statusline     - Interactive installation wizard");
        println!("  /update-statusline      - Update to latest version");
        println!("  /customize-statusline   - Interactive configuration wizard");
        println!("\nDocumentation:");
        println!("  Wiki:   https://github.com/ndave92/claude-code-status-line/wiki");
        println!("  README: https://github.com/ndave92/claude-code-status-line#readme");
        println!("\nReport issues:");
        println!("  https://github.com/ndave92/claude-code-status-line/issues");
        return;
    }

    let config = load_config();

    let mut input_str = String::new();
    if io::stdin().read_to_string(&mut input_str).is_err() {
        eprintln!("statusline error: failed to read stdin");
        println!(); // Empty line to stdout to avoid corrupting prompt
        return;
    }

    let input: ClaudeInput = match serde_json::from_str(&input_str) {
        Ok(i) => i,
        Err(_) => {
            eprintln!("statusline error: invalid JSON");
            println!();
            return;
        }
    };

    let cwd = input
        .workspace
        .and_then(|w| w.current_dir)
        .unwrap_or_else(|| ".".to_string());

    let cwd_path = if config.sections.cwd.full_path {
        if let Some(home) = std::env::var_os("HOME") {
            let home_str = home.to_string_lossy();
            if cwd.starts_with(home_str.as_ref()) {
                cwd.replacen(home_str.as_ref(), "~", 1)
            } else {
                cwd.clone()
            }
        } else {
            cwd.clone()
        }
    } else {
        std::path::Path::new(&cwd)
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| cwd.clone())
    };

    let cwd_display = if config.sections.cwd.show_username {
        if let Some(username) = get_username() {
            let (details, fg) = get_details_and_fg_codes(&config.theme.cwd, &config);
            format!("{}{}@{}{}", details, username, fg, cwd_path)
        } else {
            cwd_path
        }
    } else {
        cwd_path
    };

    let git_info = if config.sections.git.enabled {
        get_git_info(&cwd)
    } else {
        None
    };

    let git_display = git_info.as_ref().map(|info| {
        let mut parts = Vec::new();

        // Leading: repo name (if enabled and available)
        if config.sections.git.show_repo_name {
            if let Some(ref repo_name) = info.repo_name {
                parts.push(repo_name.clone());
            }
        }

        // Main: branch with icon
        let branch_icon = if config.display.use_powerline {
            " \u{E0A0}"
        } else {
            ""
        };
        parts.push(format!("[{}]{}", info.branch, branch_icon));

        // Always show * if dirty
        let mut result = parts.join(" ");
        if info.is_dirty {
            result.push_str(" *");
        }

        // Trailing: diff stats (if show_diff_stats and has changes)
        if config.sections.git.show_diff_stats && (info.lines_added > 0 || info.lines_removed > 0) {
            let (details, fg) = get_details_and_fg_codes(&config.theme.git, &config);
            result.push_str(&format!(
                " {}(+{}{}-{}){}",
                details, info.lines_added, config.display.details_separator, info.lines_removed, fg
            ));
        }

        result
    });

    let model_info = input.model.as_ref();
    let model_name = model_info
        .and_then(|m| m.display_name.clone())
        .unwrap_or_else(|| "Sonnet".to_string());

    let model_display = {
        let mut parts = vec![model_name.clone()];
        let mut details_parts = Vec::new();

        // Output style is at top level, not in model object
        if config.sections.model.show_output_style {
            if let Some(ref style) = input.output_style {
                if let Some(ref style_name) = style.name {
                    details_parts.push(style_name.clone());
                }
            }
        }

        // Thinking mode from ~/.claude/settings.json
        if config.sections.model.show_thinking_mode && get_thinking_mode_enabled() {
            details_parts.push("thinking".to_string());
        }

        if !details_parts.is_empty() {
            let (details, fg) = get_details_and_fg_codes(&config.theme.model, &config);
            parts.push(format!(
                "{}({}){}",
                details,
                details_parts.join(&config.display.details_separator),
                fg
            ));
        }

        parts.join(" ")
    };

    let quota = if config.sections.quota.enabled {
        get_quota(&config)
    } else {
        None
    };

    let ctx = if config.sections.context.enabled {
        calculate_context(input.context_window.as_ref(), &config)
    } else {
        None
    };

    let cost_display = if config.sections.cost.enabled {
        format_cost_display(input.cost.as_ref(), &config)
    } else {
        None
    };

    let mut sections = Vec::new();
    let mut priority = 0u16;

    if config.sections.cwd.enabled {
        sections.push(Section::new(cwd_display, priority, config.theme.cwd));
        priority += 1;
    }

    if config.sections.git.enabled {
        if let Some(display) = git_display {
            sections.push(Section::new(display, priority, config.theme.git));
        }
        priority += 1;
    }

    if config.sections.model.enabled {
        sections.push(Section::new(model_display, priority, config.theme.model));
        priority += 1;
    }

    if config.sections.context.enabled {
        match ctx {
            Some(info) => {
                sections.push(Section::new_context_compact(
                    format_ctx_short(info.percentage, info.is_exact, &config),
                    priority,
                    config.theme.context,
                ));
                sections.push(Section::new_context_detailed(
                    format_ctx_full(&info, &config.theme.context, &config),
                    priority + 10,
                    config.theme.context,
                ));
                priority += 20;
            }
            None => {
                sections.push(Section::new(
                    "ctx: -".to_string(),
                    priority,
                    config.theme.context,
                ));
                priority += 1;
            }
        }
    }

    if config.sections.quota.enabled {
        if let Some(q) = quota {
            let now = Utc::now();
            let five_hr_reset = q
                .five_hour_resets_at
                .as_ref()
                .map(|r| format_time_remaining(r, now))
                .unwrap_or_default();

            let seven_day_reset = q
                .seven_day_resets_at
                .as_ref()
                .map(|r| format_time_remaining(r, now))
                .unwrap_or_default();

            sections.push(Section::new_quota_compact(
                "5h",
                format_quota_compact("5h", q.five_hour_pct),
                priority,
                config.theme.quota_5h,
            ));
            if !five_hr_reset.is_empty() {
                sections.push(Section::new_quota_detailed(
                    "5h",
                    format_quota_display(
                        "5h",
                        q.five_hour_pct,
                        &five_hr_reset,
                        &config.theme.quota_5h,
                        &config,
                    ),
                    priority + 10,
                    config.theme.quota_5h,
                ));
            }
            priority += 20;

            sections.push(Section::new_quota_compact(
                "7d",
                format_quota_compact("7d", q.seven_day_pct),
                priority,
                config.theme.quota_7d,
            ));
            if !seven_day_reset.is_empty() {
                sections.push(Section::new_quota_detailed(
                    "7d",
                    format_quota_display(
                        "7d",
                        q.seven_day_pct,
                        &seven_day_reset,
                        &config.theme.quota_7d,
                        &config,
                    ),
                    priority + 10,
                    config.theme.quota_7d,
                ));
            }
            priority += 20;
        } else {
            sections.push(Section::new(
                "5h: -".to_string(),
                priority,
                config.theme.quota_5h,
            ));
            priority += 20;
            sections.push(Section::new(
                "7d: -".to_string(),
                priority,
                config.theme.quota_7d,
            ));
            priority += 20;
        }
    }

    if let Some(cost) = cost_display {
        sections.push(Section::new(cost, priority, config.theme.cost));
    }

    let term_width = get_terminal_width(&config);

    if std::env::var("STATUSLINE_DEBUG").is_ok() {
        eprintln!("Debug: term_width detected as {}", term_width);
    }

    let lines = build_statusline(sections, term_width, &config);

    for line in lines {
        println!("{}", line);
    }
}