diff --git a/src/config/loader.rs b/src/config/loader.rs index 8d68211e..582fc3f9 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -124,7 +124,8 @@ impl Config { } let content = fs::read_to_string(config_path)?; - let config: Config = toml::from_str(&content)?; + let mut config: Config = toml::from_str(&content)?; + config.migrate(); Ok(config) } diff --git a/src/config/types.rs b/src/config/types.rs index e5a78dc1..fc101496 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -69,6 +69,10 @@ pub enum SegmentId { Git, ContextWindow, Usage, + #[serde(rename = "usage_5h")] + Usage5h, + #[serde(rename = "usage_7d")] + Usage7d, Cost, Session, OutputStyle, @@ -110,6 +114,20 @@ pub struct OutputStyle { pub name: String, } +#[derive(Deserialize)] +pub struct RateLimitPeriod { + pub used_percentage: f64, + // Unix timestamp in seconds, supplied by Claude Code. + #[serde(rename = "resets_at")] + pub resets_at_unix_secs: Option, +} + +#[derive(Deserialize)] +pub struct RateLimits { + pub five_hour: Option, + pub seven_day: Option, +} + #[derive(Deserialize)] pub struct InputData { pub model: Model, @@ -117,6 +135,7 @@ pub struct InputData { pub transcript_path: String, pub cost: Option, pub output_style: Option, + pub rate_limits: Option, } // OpenAI-style nested token details @@ -271,6 +290,46 @@ impl Config { !self.matches_theme(&self.theme) } + /// Migrate old config: if usage exists but usage_5h/usage_7d don't, add them + pub fn migrate(&mut self) { + let has_usage = self.segments.iter().any(|s| s.id == SegmentId::Usage); + let has_5h = self.segments.iter().any(|s| s.id == SegmentId::Usage5h); + let has_7d = self.segments.iter().any(|s| s.id == SegmentId::Usage7d); + + if has_usage && !has_5h && !has_7d { + // Find the old usage segment to copy its colors + let usage_seg = self.segments.iter().find(|s| s.id == SegmentId::Usage).cloned(); + if let Some(old) = usage_seg { + // Find position of old usage to insert after it + let pos = self.segments.iter().position(|s| s.id == SegmentId::Usage).unwrap(); + + let seg_5h = SegmentConfig { + id: SegmentId::Usage5h, + enabled: old.enabled, + icon: old.icon.clone(), + colors: old.colors.clone(), + styles: old.styles.clone(), + options: old.options.clone(), + }; + let seg_7d = SegmentConfig { + id: SegmentId::Usage7d, + enabled: old.enabled, + icon: old.icon.clone(), + colors: old.colors.clone(), + styles: old.styles.clone(), + options: old.options.clone(), + }; + + // Disable old usage + self.segments[pos].enabled = false; + + // Insert new ones after old usage + self.segments.insert(pos + 1, seg_5h); + self.segments.insert(pos + 2, seg_7d); + } + } + } + /// Compare two segment configs for equality fn segment_matches(&self, current: &SegmentConfig, preset: &SegmentConfig) -> bool { current.id == preset.id diff --git a/src/core/segments/mod.rs b/src/core/segments/mod.rs index ff036a98..9ac90ddc 100644 --- a/src/core/segments/mod.rs +++ b/src/core/segments/mod.rs @@ -33,4 +33,4 @@ pub use model::ModelSegment; pub use output_style::OutputStyleSegment; pub use session::SessionSegment; pub use update::UpdateSegment; -pub use usage::UsageSegment; +pub use usage::{Usage5hSegment, Usage7dSegment, UsageSegment}; diff --git a/src/core/segments/usage.rs b/src/core/segments/usage.rs index d5dd9bde..0423042f 100644 --- a/src/core/segments/usage.rs +++ b/src/core/segments/usage.rs @@ -1,10 +1,12 @@ use super::{Segment, SegmentData}; use crate::config::{InputData, SegmentId}; use crate::utils::credentials; -use chrono::{DateTime, Datelike, Duration, Local, Timelike, Utc}; +use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +// ── API response types ──────────────────────────────────────────────────────── + #[derive(Debug, Deserialize)] struct ApiUsageResponse { five_hour: UsagePeriod, @@ -20,251 +22,365 @@ struct UsagePeriod { #[derive(Debug, Serialize, Deserialize)] struct ApiUsageCache { five_hour_utilization: f64, + #[serde(default)] + five_hour_resets_at: Option, seven_day_utilization: f64, - resets_at: Option, + resets_at: Option, // 7d reset, kept for backward compat cached_at: String, } -#[derive(Default)] -pub struct UsageSegment; +// ── Shared rate limit data ──────────────────────────────────────────────────── -impl UsageSegment { - pub fn new() -> Self { - Self - } +#[derive(Clone)] +struct RateLimitData { + five_hour_util: f64, + five_hour_resets_at: Option, + seven_day_util: f64, + seven_day_resets_at: Option, +} - fn get_circle_icon(utilization: f64) -> String { - let percent = (utilization * 100.0) as u8; - match percent { - 0..=12 => "\u{f0a9e}".to_string(), // circle_slice_1 - 13..=25 => "\u{f0a9f}".to_string(), // circle_slice_2 - 26..=37 => "\u{f0aa0}".to_string(), // circle_slice_3 - 38..=50 => "\u{f0aa1}".to_string(), // circle_slice_4 - 51..=62 => "\u{f0aa2}".to_string(), // circle_slice_5 - 63..=75 => "\u{f0aa3}".to_string(), // circle_slice_6 - 76..=87 => "\u{f0aa4}".to_string(), // circle_slice_7 - _ => "\u{f0aa5}".to_string(), // circle_slice_8 - } +// Process-lifetime cache: computed once per statusline render, shared across +// all Usage segments (Usage, Usage5h, Usage7d) to avoid redundant I/O. Only +// successful fetches are cached so transient failures can be retried. +static RATE_LIMIT_CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); + +// ── Shared helpers ──────────────────────────────────────────────────────────── + +fn get_circle_icon(utilization: f64) -> String { + let percent = (utilization * 100.0) as u8; + match percent { + 0..=12 => "\u{f0a9e}".to_string(), // circle_slice_1 + 13..=25 => "\u{f0a9f}".to_string(), // circle_slice_2 + 26..=37 => "\u{f0aa0}".to_string(), // circle_slice_3 + 38..=50 => "\u{f0aa1}".to_string(), // circle_slice_4 + 51..=62 => "\u{f0aa2}".to_string(), // circle_slice_5 + 63..=75 => "\u{f0aa3}".to_string(), // circle_slice_6 + 76..=87 => "\u{f0aa4}".to_string(), // circle_slice_7 + _ => "\u{f0aa5}".to_string(), // circle_slice_8 } +} - fn format_reset_time(reset_time_str: Option<&str>) -> String { - if let Some(time_str) = reset_time_str { - if let Ok(dt) = DateTime::parse_from_rfc3339(time_str) { - let mut local_dt = dt.with_timezone(&Local); - if local_dt.minute() > 45 { - local_dt += Duration::hours(1); - } - return format!( - "{}-{}-{}", - local_dt.month(), - local_dt.day(), - local_dt.hour() - ); +fn format_reset_time(reset_time_str: Option<&str>) -> String { + if let Some(time_str) = reset_time_str { + if let Ok(dt) = DateTime::parse_from_rfc3339(time_str) { + let mut local_dt = dt.with_timezone(&Local); + if local_dt.minute() > 45 { + local_dt += Duration::hours(1); } + return format!( + "{}-{}-{}", + local_dt.month(), + local_dt.day(), + local_dt.hour() + ); } - "?".to_string() } + "?".to_string() +} + +fn get_cache_path() -> Option { + let home = dirs::home_dir()?; + Some( + home.join(".claude") + .join("ccline") + .join(".api_usage_cache.json"), + ) +} - fn get_cache_path() -> Option { - let home = dirs::home_dir()?; - Some( - home.join(".claude") - .join("ccline") - .join(".api_usage_cache.json"), - ) +fn load_cache() -> Option { + let cache_path = get_cache_path()?; + if !cache_path.exists() { + return None; } + let content = std::fs::read_to_string(&cache_path).ok()?; + serde_json::from_str(&content).ok() +} - fn load_cache(&self) -> Option { - let cache_path = Self::get_cache_path()?; - if !cache_path.exists() { - return None; +fn save_cache(cache: &ApiUsageCache) { + if let Some(cache_path) = get_cache_path() { + if let Some(parent) = cache_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Ok(json) = serde_json::to_string_pretty(cache) { + let _ = std::fs::write(&cache_path, json); } + } +} - let content = std::fs::read_to_string(&cache_path).ok()?; - serde_json::from_str(&content).ok() +fn is_cache_valid(cache: &ApiUsageCache, cache_duration: u64) -> bool { + if let Ok(cached_at) = DateTime::parse_from_rfc3339(&cache.cached_at) { + let elapsed = Utc::now().signed_duration_since(cached_at.with_timezone(&Utc)); + elapsed.num_seconds() < cache_duration as i64 + } else { + false } +} - fn save_cache(&self, cache: &ApiUsageCache) { - if let Some(cache_path) = Self::get_cache_path() { - if let Some(parent) = cache_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Ok(json) = serde_json::to_string_pretty(cache) { - let _ = std::fs::write(&cache_path, json); +fn get_claude_code_version() -> String { + use std::process::Command; + let output = Command::new("npm") + .args(["view", "@anthropic-ai/claude-code", "version"]) + .output(); + match output { + Ok(output) if output.status.success() => { + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !version.is_empty() { + return format!("claude-code/{}", version); } } + _ => {} } + "claude-code".to_string() +} - fn is_cache_valid(&self, cache: &ApiUsageCache, cache_duration: u64) -> bool { - if let Ok(cached_at) = DateTime::parse_from_rfc3339(&cache.cached_at) { - let now = Utc::now(); - let elapsed = now.signed_duration_since(cached_at.with_timezone(&Utc)); - elapsed.num_seconds() < cache_duration as i64 +fn get_proxy_from_settings() -> Option { + let home = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .ok()?; + let content = std::fs::read_to_string(format!("{}/.claude/settings.json", home)).ok()?; + let settings: serde_json::Value = serde_json::from_str(&content).ok()?; + settings + .get("env")? + .get("HTTPS_PROXY") + .or_else(|| settings.get("env")?.get("HTTP_PROXY")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + +fn fetch_api_usage(api_base_url: &str, token: &str, timeout_secs: u64) -> Option { + let url = format!("{}/api/oauth/usage", api_base_url); + let user_agent = get_claude_code_version(); + + let agent = if let Some(proxy_url) = get_proxy_from_settings() { + if let Ok(proxy) = ureq::Proxy::new(&proxy_url) { + ureq::Agent::config_builder() + .proxy(Some(proxy)) + .build() + .new_agent() } else { - false + ureq::Agent::new_with_defaults() } + } else { + ureq::Agent::new_with_defaults() + }; + + let response = agent + .get(&url) + .header("Authorization", &format!("Bearer {}", token)) + .header("anthropic-beta", "oauth-2025-04-20") + .header("User-Agent", &user_agent) + .config() + .timeout_global(Some(std::time::Duration::from_secs(timeout_secs))) + .build() + .call() + .ok()?; + + response.into_body().read_json().ok() +} + +/// Convert a Unix timestamp in seconds to an RFC3339 string. Returns None for +/// out-of-range values. Centralizes the unit assumption for `resets_at_unix_secs`. +fn unix_secs_to_rfc3339(ts: i64) -> Option { + Utc.timestamp_opt(ts, 0).single().map(|dt| dt.to_rfc3339()) +} + +/// Fetch rate limit data: prefer Claude Code's rate_limits field, fallback to API. +/// Successful results are cached for the process lifetime so multiple segments +/// share one fetch; failures are not cached so later calls can retry. +fn fetch_rate_limit_data(input: &InputData) -> Option { + if let Some(cached) = RATE_LIMIT_CACHE.get() { + return Some(cached.clone()); } + let result = fetch_rate_limit_data_inner(input)?; + Some(RATE_LIMIT_CACHE.get_or_init(|| result).clone()) +} - fn get_claude_code_version() -> String { - use std::process::Command; +fn fetch_rate_limit_data_inner(input: &InputData) -> Option { + if let Some(rate_limits) = &input.rate_limits { + return Some(RateLimitData { + five_hour_util: rate_limits + .five_hour + .as_ref() + .map(|p| p.used_percentage) + .unwrap_or(0.0), + five_hour_resets_at: rate_limits + .five_hour + .as_ref() + .and_then(|p| p.resets_at_unix_secs) + .and_then(unix_secs_to_rfc3339), + seven_day_util: rate_limits + .seven_day + .as_ref() + .map(|p| p.used_percentage) + .unwrap_or(0.0), + seven_day_resets_at: rate_limits + .seven_day + .as_ref() + .and_then(|p| p.resets_at_unix_secs) + .and_then(unix_secs_to_rfc3339), + }); + } - let output = Command::new("npm") - .args(["view", "@anthropic-ai/claude-code", "version"]) - .output(); + // Fallback: fetch from Anthropic API with cache + let token = credentials::get_oauth_token()?; + + let config = crate::config::Config::load().ok()?; + let segment_config = config.segments.iter().find(|s| { + matches!(s.id, SegmentId::Usage | SegmentId::Usage5h | SegmentId::Usage7d) + }); + + let api_base_url = segment_config + .and_then(|sc| sc.options.get("api_base_url")) + .and_then(|v| v.as_str()) + .unwrap_or("https://api.anthropic.com"); + + let cache_duration = segment_config + .and_then(|sc| sc.options.get("cache_duration")) + .and_then(|v| v.as_u64()) + .unwrap_or(300); + + let timeout = segment_config + .and_then(|sc| sc.options.get("timeout")) + .and_then(|v| v.as_u64()) + .unwrap_or(2); + + let cached_data = load_cache(); + let use_cached = cached_data + .as_ref() + .map(|c| is_cache_valid(c, cache_duration)) + .unwrap_or(false); + + if use_cached { + let cache = cached_data.unwrap(); + return Some(RateLimitData { + five_hour_util: cache.five_hour_utilization, + five_hour_resets_at: cache.five_hour_resets_at, + seven_day_util: cache.seven_day_utilization, + seven_day_resets_at: cache.resets_at, + }); + } - match output { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !version.is_empty() { - return format!("claude-code/{}", version); - } - } - _ => {} + match fetch_api_usage(api_base_url, &token, timeout) { + Some(response) => { + save_cache(&ApiUsageCache { + five_hour_utilization: response.five_hour.utilization, + five_hour_resets_at: response.five_hour.resets_at.clone(), + seven_day_utilization: response.seven_day.utilization, + resets_at: response.seven_day.resets_at.clone(), + cached_at: Utc::now().to_rfc3339(), + }); + Some(RateLimitData { + five_hour_util: response.five_hour.utilization, + five_hour_resets_at: response.five_hour.resets_at, + seven_day_util: response.seven_day.utilization, + seven_day_resets_at: response.seven_day.resets_at, + }) } + None => cached_data.map(|cache| RateLimitData { + five_hour_util: cache.five_hour_utilization, + five_hour_resets_at: cache.five_hour_resets_at, + seven_day_util: cache.seven_day_utilization, + seven_day_resets_at: cache.resets_at, + }), + } +} + +// ── Usage5hSegment ──────────────────────────────────────────────────────────── + +#[derive(Default)] +pub struct Usage5hSegment; - "claude-code".to_string() +impl Usage5hSegment { + pub fn new() -> Self { + Self } +} + +impl Segment for Usage5hSegment { + fn collect(&self, input: &InputData) -> Option { + let data = fetch_rate_limit_data(input)?; + let percent = data.five_hour_util.round() as u8; + let reset = format_reset_time(data.five_hour_resets_at.as_deref()); + + let mut metadata = HashMap::new(); + metadata.insert( + "dynamic_icon".to_string(), + get_circle_icon(data.five_hour_util / 100.0), + ); - fn get_proxy_from_settings() -> Option { - let home = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .ok()?; - let settings_path = format!("{}/.claude/settings.json", home); - - let content = std::fs::read_to_string(&settings_path).ok()?; - let settings: serde_json::Value = serde_json::from_str(&content).ok()?; - - // Try HTTPS_PROXY first, then HTTP_PROXY - settings - .get("env")? - .get("HTTPS_PROXY") - .or_else(|| settings.get("env")?.get("HTTP_PROXY")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) + Some(SegmentData { + primary: format!("{}%", percent), + secondary: format!("· {}", reset), + metadata, + }) } - fn fetch_api_usage( - &self, - api_base_url: &str, - token: &str, - timeout_secs: u64, - ) -> Option { - let url = format!("{}/api/oauth/usage", api_base_url); - let user_agent = Self::get_claude_code_version(); - - let agent = if let Some(proxy_url) = Self::get_proxy_from_settings() { - if let Ok(proxy) = ureq::Proxy::new(&proxy_url) { - ureq::Agent::config_builder() - .proxy(Some(proxy)) - .build() - .new_agent() - } else { - ureq::Agent::new_with_defaults() - } - } else { - ureq::Agent::new_with_defaults() - }; - - let response = agent - .get(&url) - .header("Authorization", &format!("Bearer {}", token)) - .header("anthropic-beta", "oauth-2025-04-20") - .header("User-Agent", &user_agent) - .config() - .timeout_global(Some(std::time::Duration::from_secs(timeout_secs))) - .build() - .call() - .ok()?; - - response.into_body().read_json().ok() + fn id(&self) -> SegmentId { + SegmentId::Usage5h } } -impl Segment for UsageSegment { - fn collect(&self, _input: &InputData) -> Option { - let token = credentials::get_oauth_token()?; - - // Load config from file to get segment options - let config = crate::config::Config::load().ok()?; - let segment_config = config.segments.iter().find(|s| s.id == SegmentId::Usage); - - let api_base_url = segment_config - .and_then(|sc| sc.options.get("api_base_url")) - .and_then(|v| v.as_str()) - .unwrap_or("https://api.anthropic.com"); - - let cache_duration = segment_config - .and_then(|sc| sc.options.get("cache_duration")) - .and_then(|v| v.as_u64()) - .unwrap_or(300); - - let timeout = segment_config - .and_then(|sc| sc.options.get("timeout")) - .and_then(|v| v.as_u64()) - .unwrap_or(2); - - let cached_data = self.load_cache(); - let use_cached = cached_data - .as_ref() - .map(|cache| self.is_cache_valid(cache, cache_duration)) - .unwrap_or(false); - - let (five_hour_util, seven_day_util, resets_at) = if use_cached { - let cache = cached_data.unwrap(); - ( - cache.five_hour_utilization, - cache.seven_day_utilization, - cache.resets_at, - ) - } else { - match self.fetch_api_usage(api_base_url, &token, timeout) { - Some(response) => { - let cache = ApiUsageCache { - five_hour_utilization: response.five_hour.utilization, - seven_day_utilization: response.seven_day.utilization, - resets_at: response.seven_day.resets_at.clone(), - cached_at: Utc::now().to_rfc3339(), - }; - self.save_cache(&cache); - ( - response.five_hour.utilization, - response.seven_day.utilization, - response.seven_day.resets_at, - ) - } - None => { - if let Some(cache) = cached_data { - ( - cache.five_hour_utilization, - cache.seven_day_utilization, - cache.resets_at, - ) - } else { - return None; - } - } - } - }; +// ── Usage7dSegment ──────────────────────────────────────────────────────────── - let dynamic_icon = Self::get_circle_icon(seven_day_util / 100.0); - let five_hour_percent = five_hour_util.round() as u8; - let primary = format!("{}%", five_hour_percent); - let secondary = format!("· {}", Self::format_reset_time(resets_at.as_deref())); +#[derive(Default)] +pub struct Usage7dSegment; + +impl Usage7dSegment { + pub fn new() -> Self { + Self + } +} + +impl Segment for Usage7dSegment { + fn collect(&self, input: &InputData) -> Option { + let data = fetch_rate_limit_data(input)?; + let percent = data.seven_day_util.round() as u8; + let reset = format_reset_time(data.seven_day_resets_at.as_deref()); let mut metadata = HashMap::new(); - metadata.insert("dynamic_icon".to_string(), dynamic_icon); metadata.insert( - "five_hour_utilization".to_string(), - five_hour_util.to_string(), + "dynamic_icon".to_string(), + get_circle_icon(data.seven_day_util / 100.0), ); + + Some(SegmentData { + primary: format!("{}%", percent), + secondary: format!("· {}", reset), + metadata, + }) + } + + fn id(&self) -> SegmentId { + SegmentId::Usage7d + } +} + +// ── UsageSegment (legacy, combined view) ───────────────────────────────────── + +#[derive(Default)] +pub struct UsageSegment; + +impl UsageSegment { + pub fn new() -> Self { + Self + } +} + +impl Segment for UsageSegment { + fn collect(&self, input: &InputData) -> Option { + let data = fetch_rate_limit_data(input)?; + let five_hour_percent = data.five_hour_util.round() as u8; + let seven_day_reset = format_reset_time(data.seven_day_resets_at.as_deref()); + + let mut metadata = HashMap::new(); metadata.insert( - "seven_day_utilization".to_string(), - seven_day_util.to_string(), + "dynamic_icon".to_string(), + get_circle_icon(data.seven_day_util / 100.0), ); Some(SegmentData { - primary, - secondary, + primary: format!("{}%", five_hour_percent), + secondary: format!("· {}", seven_day_reset), metadata, }) } diff --git a/src/core/statusline.rs b/src/core/statusline.rs index c2724931..8b801915 100644 --- a/src/core/statusline.rs +++ b/src/core/statusline.rs @@ -493,6 +493,14 @@ pub fn collect_all_segments( let segment = UsageSegment::new(); segment.collect(input) } + crate::config::SegmentId::Usage5h => { + let segment = Usage5hSegment::new(); + segment.collect(input) + } + crate::config::SegmentId::Usage7d => { + let segment = Usage7dSegment::new(); + segment.collect(input) + } crate::config::SegmentId::Cost => { let segment = CostSegment::new(); segment.collect(input) diff --git a/src/ui/app.rs b/src/ui/app.rs index 4487dc91..b07475ca 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -501,11 +501,13 @@ impl App { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::ContextWindow => "Context Window", - SegmentId::Usage => "Usage", + SegmentId::Usage => "Usage (deprecated)", SegmentId::Cost => "Cost", SegmentId::Session => "Session", SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", + SegmentId::Usage5h => "Usage 5h", + SegmentId::Usage7d => "Usage 7d", }; let is_enabled = segment.enabled; self.status_message = Some(format!( @@ -528,11 +530,13 @@ impl App { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::ContextWindow => "Context Window", - SegmentId::Usage => "Usage", + SegmentId::Usage => "Usage (deprecated)", SegmentId::Cost => "Cost", SegmentId::Session => "Session", SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", + SegmentId::Usage5h => "Usage 5h", + SegmentId::Usage7d => "Usage 7d", }; let is_enabled = segment.enabled; self.status_message = Some(format!( diff --git a/src/ui/components/preview.rs b/src/ui/components/preview.rs index 4195878e..84e79959 100644 --- a/src/ui/components/preview.rs +++ b/src/ui/components/preview.rs @@ -183,6 +183,24 @@ impl PreviewComponent { map }, }, + SegmentId::Usage5h => SegmentData { + primary: "18%".to_string(), + secondary: "· 3-17-15".to_string(), + metadata: { + let mut map = HashMap::new(); + map.insert("dynamic_icon".to_string(), "\u{f0a9f}".to_string()); + map + }, + }, + SegmentId::Usage7d => SegmentData { + primary: "26%".to_string(), + secondary: "· 3-28-0".to_string(), + metadata: { + let mut map = HashMap::new(); + map.insert("dynamic_icon".to_string(), "\u{f0aa0}".to_string()); + map + }, + }, }; segments_data.push((segment_config.clone(), mock_data)); diff --git a/src/ui/components/segment_list.rs b/src/ui/components/segment_list.rs index 832834b8..e858f9e3 100644 --- a/src/ui/components/segment_list.rs +++ b/src/ui/components/segment_list.rs @@ -52,11 +52,13 @@ impl SegmentListComponent { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::ContextWindow => "Context Window", - SegmentId::Usage => "Usage", + SegmentId::Usage => "Usage (deprecated)", SegmentId::Cost => "Cost", SegmentId::Session => "Session", SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", + SegmentId::Usage5h => "Usage 5h", + SegmentId::Usage7d => "Usage 7d", }; if is_selected { diff --git a/src/ui/components/settings.rs b/src/ui/components/settings.rs index aa65acb5..670f1807 100644 --- a/src/ui/components/settings.rs +++ b/src/ui/components/settings.rs @@ -31,11 +31,13 @@ impl SettingsComponent { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::ContextWindow => "Context Window", - SegmentId::Usage => "Usage", + SegmentId::Usage => "Usage (deprecated)", SegmentId::Cost => "Cost", SegmentId::Session => "Session", SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", + SegmentId::Usage5h => "Usage 5h", + SegmentId::Usage7d => "Usage 7d", }; let current_icon = match config.style.mode { StyleMode::Plain => &segment.icon.plain, diff --git a/src/ui/themes/presets.rs b/src/ui/themes/presets.rs index 0a51dabc..cd0de2e2 100644 --- a/src/ui/themes/presets.rs +++ b/src/ui/themes/presets.rs @@ -133,7 +133,8 @@ impl ThemePresets { theme_cometix::directory_segment(), theme_cometix::git_segment(), theme_cometix::context_window_segment(), - theme_cometix::usage_segment(), + theme_cometix::usage_5h_segment(), + theme_cometix::usage_7d_segment(), theme_cometix::cost_segment(), theme_cometix::session_segment(), theme_cometix::output_style_segment(), @@ -153,7 +154,8 @@ impl ThemePresets { theme_default::directory_segment(), theme_default::git_segment(), theme_default::context_window_segment(), - theme_default::usage_segment(), + theme_default::usage_5h_segment(), + theme_default::usage_7d_segment(), theme_default::cost_segment(), theme_default::session_segment(), theme_default::output_style_segment(), @@ -173,7 +175,8 @@ impl ThemePresets { theme_minimal::directory_segment(), theme_minimal::git_segment(), theme_minimal::context_window_segment(), - theme_minimal::usage_segment(), + theme_minimal::usage_5h_segment(), + theme_minimal::usage_7d_segment(), theme_minimal::cost_segment(), theme_minimal::session_segment(), theme_minimal::output_style_segment(), @@ -193,7 +196,8 @@ impl ThemePresets { theme_gruvbox::directory_segment(), theme_gruvbox::git_segment(), theme_gruvbox::context_window_segment(), - theme_gruvbox::usage_segment(), + theme_gruvbox::usage_5h_segment(), + theme_gruvbox::usage_7d_segment(), theme_gruvbox::cost_segment(), theme_gruvbox::session_segment(), theme_gruvbox::output_style_segment(), @@ -213,7 +217,8 @@ impl ThemePresets { theme_nord::directory_segment(), theme_nord::git_segment(), theme_nord::context_window_segment(), - theme_nord::usage_segment(), + theme_nord::usage_5h_segment(), + theme_nord::usage_7d_segment(), theme_nord::cost_segment(), theme_nord::session_segment(), theme_nord::output_style_segment(), @@ -233,7 +238,8 @@ impl ThemePresets { theme_powerline_dark::directory_segment(), theme_powerline_dark::git_segment(), theme_powerline_dark::context_window_segment(), - theme_powerline_dark::usage_segment(), + theme_powerline_dark::usage_5h_segment(), + theme_powerline_dark::usage_7d_segment(), theme_powerline_dark::cost_segment(), theme_powerline_dark::session_segment(), theme_powerline_dark::output_style_segment(), @@ -253,7 +259,8 @@ impl ThemePresets { theme_powerline_light::directory_segment(), theme_powerline_light::git_segment(), theme_powerline_light::context_window_segment(), - theme_powerline_light::usage_segment(), + theme_powerline_light::usage_5h_segment(), + theme_powerline_light::usage_7d_segment(), theme_powerline_light::cost_segment(), theme_powerline_light::session_segment(), theme_powerline_light::output_style_segment(), @@ -273,7 +280,8 @@ impl ThemePresets { theme_powerline_rose_pine::directory_segment(), theme_powerline_rose_pine::git_segment(), theme_powerline_rose_pine::context_window_segment(), - theme_powerline_rose_pine::usage_segment(), + theme_powerline_rose_pine::usage_5h_segment(), + theme_powerline_rose_pine::usage_7d_segment(), theme_powerline_rose_pine::cost_segment(), theme_powerline_rose_pine::session_segment(), theme_powerline_rose_pine::output_style_segment(), @@ -293,7 +301,8 @@ impl ThemePresets { theme_powerline_tokyo_night::directory_segment(), theme_powerline_tokyo_night::git_segment(), theme_powerline_tokyo_night::context_window_segment(), - theme_powerline_tokyo_night::usage_segment(), + theme_powerline_tokyo_night::usage_5h_segment(), + theme_powerline_tokyo_night::usage_7d_segment(), theme_powerline_tokyo_night::cost_segment(), theme_powerline_tokyo_night::session_segment(), theme_powerline_tokyo_night::output_style_segment(), diff --git a/src/ui/themes/theme_cometix.rs b/src/ui/themes/theme_cometix.rs index dfcd1e6b..f1291fda 100644 --- a/src/ui/themes/theme_cometix.rs +++ b/src/ui/themes/theme_cometix.rs @@ -133,9 +133,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -162,3 +162,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_default.rs b/src/ui/themes/theme_default.rs index 20b21cae..e25dfdae 100644 --- a/src/ui/themes/theme_default.rs +++ b/src/ui/themes/theme_default.rs @@ -79,36 +79,6 @@ pub fn context_window_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: false, - icon: IconConfig { - plain: "📊".to_string(), - nerd_font: "\u{f0a9e}".to_string(), // circle_slice_1 - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan - text: Some(AnsiColor::Color16 { c16: 14 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert( - "api_base_url".to_string(), - serde_json::Value::String("https://api.anthropic.com".to_string()), - ); - opts.insert( - "cache_duration".to_string(), - serde_json::Value::Number(180.into()), - ); - opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); - opts - }, - } -} - pub fn cost_segment() -> SegmentConfig { SegmentConfig { id: SegmentId::Cost, @@ -162,3 +132,45 @@ pub fn output_style_segment() -> SegmentConfig { options: HashMap::new(), } } + +fn usage_segment_base(id: SegmentId) -> SegmentConfig { + SegmentConfig { + id, + enabled: false, + icon: IconConfig { + plain: "📊".to_string(), + nerd_font: "\u{f0a9e}".to_string(), // circle_slice_1 + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan + text: Some(AnsiColor::Color16 { c16: 14 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert( + "api_base_url".to_string(), + serde_json::Value::String("https://api.anthropic.com".to_string()), + ); + opts.insert( + "cache_duration".to_string(), + serde_json::Value::Number(180.into()), + ); + opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_gruvbox.rs b/src/ui/themes/theme_gruvbox.rs index 6b071e1c..739cb303 100644 --- a/src/ui/themes/theme_gruvbox.rs +++ b/src/ui/themes/theme_gruvbox.rs @@ -133,9 +133,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -162,3 +162,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_minimal.rs b/src/ui/themes/theme_minimal.rs index 0c1cdd60..d873a2e2 100644 --- a/src/ui/themes/theme_minimal.rs +++ b/src/ui/themes/theme_minimal.rs @@ -133,9 +133,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -162,3 +162,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_nord.rs b/src/ui/themes/theme_nord.rs index 29bba681..666d9ab3 100644 --- a/src/ui/themes/theme_nord.rs +++ b/src/ui/themes/theme_nord.rs @@ -217,9 +217,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -258,3 +258,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_powerline_dark.rs b/src/ui/themes/theme_powerline_dark.rs index 06e7e4bf..5a8feb8a 100644 --- a/src/ui/themes/theme_powerline_dark.rs +++ b/src/ui/themes/theme_powerline_dark.rs @@ -217,9 +217,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -258,3 +258,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_powerline_light.rs b/src/ui/themes/theme_powerline_light.rs index 7b95a238..fd552ba1 100644 --- a/src/ui/themes/theme_powerline_light.rs +++ b/src/ui/themes/theme_powerline_light.rs @@ -209,9 +209,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -250,3 +250,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_powerline_rose_pine.rs b/src/ui/themes/theme_powerline_rose_pine.rs index abea8606..3235d14b 100644 --- a/src/ui/themes/theme_powerline_rose_pine.rs +++ b/src/ui/themes/theme_powerline_rose_pine.rs @@ -217,9 +217,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -258,3 +258,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +} diff --git a/src/ui/themes/theme_powerline_tokyo_night.rs b/src/ui/themes/theme_powerline_tokyo_night.rs index 4e0143b5..e4a7101c 100644 --- a/src/ui/themes/theme_powerline_tokyo_night.rs +++ b/src/ui/themes/theme_powerline_tokyo_night.rs @@ -217,9 +217,9 @@ pub fn output_style_segment() -> SegmentConfig { } } -pub fn usage_segment() -> SegmentConfig { +fn usage_segment_base(id: SegmentId) -> SegmentConfig { SegmentConfig { - id: SegmentId::Usage, + id, enabled: false, icon: IconConfig { plain: "📊".to_string(), @@ -258,3 +258,15 @@ pub fn usage_segment() -> SegmentConfig { }, } } + +pub fn usage_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage) +} + +pub fn usage_5h_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage5h) +} + +pub fn usage_7d_segment() -> SegmentConfig { + usage_segment_base(SegmentId::Usage7d) +}