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!(); 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();
if config.sections.git.show_repo_name {
if let Some(ref repo_name) = info.repo_name {
parts.push(repo_name.clone());
}
}
let branch_icon = if config.display.use_powerline {
" \u{E0A0}"
} else {
""
};
parts.push(format!("[{}]{}", info.branch, branch_icon));
let mut result = parts.join(" ");
if info.is_dirty {
result.push_str(" *");
}
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();
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());
}
}
}
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);
}
}