claude-code-status-line 1.2.2

A configurable status line for Claude Code with powerline arrows, context tracking, and quota monitoring
Documentation
use crate::colors::{
    SectionColors, CONTEXT_COLORS, COST_COLORS, CWD_COLORS, GIT_COLORS, MODEL_COLORS,
    QUOTA_5H_COLORS, QUOTA_7D_COLORS,
};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

#[derive(Debug, Deserialize, Serialize)]
pub struct ThemeConfig {
    pub separator: (u8, u8, u8), // Color for the separator character
    pub cwd: SectionColors,
    pub git: SectionColors,
    pub model: SectionColors,
    pub context: SectionColors,
    pub quota_5h: SectionColors,
    pub quota_7d: SectionColors,
    pub cost: SectionColors,
}

impl Default for ThemeConfig {
    fn default() -> Self {
        ThemeConfig {
            separator: (65, 65, 62), // Default to muted gray
            cwd: CWD_COLORS,
            git: GIT_COLORS,
            model: MODEL_COLORS,
            context: CONTEXT_COLORS,
            quota_5h: QUOTA_5H_COLORS,
            quota_7d: QUOTA_7D_COLORS,
            cost: COST_COLORS,
        }
    }
}

// Section-specific configurations

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CwdConfig {
    pub enabled: bool,
    pub full_path: bool,
    pub show_username: bool,
}

impl Default for CwdConfig {
    fn default() -> Self {
        CwdConfig {
            enabled: true,
            full_path: true,
            show_username: true,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct GitConfig {
    pub enabled: bool,
    pub show_repo_name: bool,
    pub show_diff_stats: bool,
}

impl Default for GitConfig {
    fn default() -> Self {
        GitConfig {
            enabled: true,
            show_repo_name: true,
            show_diff_stats: true,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ModelConfig {
    pub enabled: bool,
    pub show_output_style: bool,
    pub show_thinking_mode: bool,
}

impl Default for ModelConfig {
    fn default() -> Self {
        ModelConfig {
            enabled: true,
            show_output_style: false,
            show_thinking_mode: true,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ContextConfig {
    pub enabled: bool,
    pub show_decimals: bool,
    pub show_token_counts: bool,
    pub autocompact_buffer_size: u64,
}

impl Default for ContextConfig {
    fn default() -> Self {
        ContextConfig {
            enabled: true,
            show_decimals: false,
            show_token_counts: true,
            autocompact_buffer_size: 45_000,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct QuotaConfig {
    pub enabled: bool,
    pub show_time_remaining: bool,
    pub cache_ttl: u64,
}

impl Default for QuotaConfig {
    fn default() -> Self {
        QuotaConfig {
            enabled: true,
            show_time_remaining: true,
            cache_ttl: 0,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CostConfig {
    pub enabled: bool,
    pub show_durations: bool,
}

impl Default for CostConfig {
    fn default() -> Self {
        CostConfig {
            enabled: false,
            show_durations: true,
        }
    }
}

#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct SectionsConfig {
    pub cwd: CwdConfig,
    pub git: GitConfig,
    pub model: ModelConfig,
    pub context: ContextConfig,
    pub quota: QuotaConfig,
    pub cost: CostConfig,
}

/// Powerline arrow character (hardcoded, not user-configurable)
pub const POWERLINE_ARROW: &str = "\u{E0B0}";

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct DisplayConfig {
    pub multiline: bool,
    pub default_terminal_width: usize,
    pub use_powerline: bool,
    #[serde(skip)] // Arrow is hardcoded, not configurable
    pub arrow: String,
    pub segment_separator: String,
    pub details_separator: String,
    pub section_padding: usize,
    pub show_background: bool,
}

impl Default for DisplayConfig {
    fn default() -> Self {
        DisplayConfig {
            multiline: true,
            default_terminal_width: 120,
            use_powerline: false,
            arrow: POWERLINE_ARROW.to_string(),
            segment_separator: "".to_string(),
            details_separator: ", ".to_string(),
            section_padding: 1,
            show_background: true,
        }
    }
}

/// Configuration loaded from ~/.claude/statusline/settings.json
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct Config {
    pub sections: SectionsConfig,
    pub display: DisplayConfig,
    #[serde(skip)] // Don't load theme from settings.json
    pub theme: ThemeConfig,
}

fn get_config_dir() -> Option<std::path::PathBuf> {
    #[cfg(unix)]
    let home = std::env::var_os("HOME")?;
    #[cfg(windows)]
    let home = std::env::var_os("USERPROFILE")?;

    Some(
        std::path::Path::new(&home)
            .join(".claude")
            .join("statusline"),
    )
}

fn load_theme(dir: &Path) -> ThemeConfig {
    let path = dir.join("colors.json");
    if !path.exists() {
        let theme = ThemeConfig::default();
        if let Ok(json) = serde_json::to_string_pretty(&theme) {
            let _ = fs::write(&path, json);
        }
        return theme;
    }

    match std::fs::read_to_string(&path) {
        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
            eprintln!("statusline warning: invalid colors.json: {}", e);
            ThemeConfig::default()
        }),
        Err(_) => ThemeConfig::default(),
    }
}

pub fn load_config() -> Config {
    let dir = match get_config_dir() {
        Some(d) => d,
        None => return Config::default(),
    };

    if !dir.exists() {
        let _ = fs::create_dir_all(&dir);
    }

    let config_path = dir.join("settings.json");
    if !config_path.exists() {
        let config = Config::default();
        if let Ok(json) = serde_json::to_string_pretty(&config) {
            let _ = fs::write(&config_path, json);
        }
        // Also ensure theme is created
        let mut final_config = config;
        final_config.theme = load_theme(&dir);
        return final_config;
    }

    let mut config = match std::fs::read_to_string(&config_path) {
        Ok(content) => serde_json::from_str::<Config>(&content).unwrap_or_else(|e| {
            eprintln!("statusline warning: invalid settings.json: {}", e);
            Config::default()
        }),
        Err(_) => Config::default(),
    };

    // Load theme from separate file
    config.theme = load_theme(&dir);
    config
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_defaults() {
        let config = Config::default();
        assert_eq!(config.display.segment_separator, "");
        assert_eq!(config.display.details_separator, ", ");
        assert_eq!(config.display.section_padding, 1);
        assert!(config.sections.cwd.enabled);
        assert!(!config.sections.context.show_decimals);
        assert!(config.sections.context.show_token_counts);
        assert_eq!(config.theme.cwd.background, Some((217, 119, 87)));
    }

    #[test]
    fn test_theme_deserialization() {
        let json = r#"{
            "separator": [255, 0, 0],
            "cwd": { "background": null, "foreground": [20, 20, 20], "details": [30, 30, 30] },
            "git": { "background": [40, 40, 40], "foreground": [50, 50, 50], "details": [60, 60, 60] },
            "model": { "background": [70, 70, 70], "foreground": [80, 80, 80], "details": [90, 90, 90] },
            "context": { "background": [100, 100, 100], "foreground": [110, 110, 110], "details": [120, 120, 120] },
            "quota_5h": { "background": [130, 130, 130], "foreground": [140, 140, 140], "details": [150, 150, 150] },
            "quota_7d": { "background": [160, 160, 160], "foreground": [170, 170, 170], "details": [180, 180, 180] },
            "cost": { "background": [190, 190, 190], "foreground": [200, 200, 200], "details": [210, 210, 210] }
        }"#;

        let theme: ThemeConfig = serde_json::from_str(json).unwrap();
        assert_eq!(theme.separator, (255, 0, 0));
        assert_eq!(theme.cwd.background, None);
        assert_eq!(theme.git.background, Some((40, 40, 40)));
    }
}