Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions src/core/segments/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct ApiUsageCache {
five_hour_utilization: f64,
seven_day_utilization: f64,
resets_at: Option<String>,
#[serde(default)]
five_hour_resets_at: Option<String>,
cached_at: String,
}

Expand All @@ -47,6 +49,41 @@ impl UsageSegment {
}
}

/// Compute time remaining from now until the given RFC3339 reset time.
/// Returns a compact string like "2h30" or "45m" or "0m".
fn format_time_remaining(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 now = Local::now();
let reset_local = dt.with_timezone(&Local);
let remaining = reset_local.signed_duration_since(now);
let total_minutes = remaining.num_minutes();
if total_minutes <= 0 {
return "0m".to_string();
}
let hours = total_minutes / 60;
let minutes = total_minutes % 60;
if hours > 0 {
return format!("{}h{:02}", hours, minutes);
} else {
return format!("{}m", minutes);
}
}
}
"?".to_string()
}

/// Format a reset time as a local clock time like "14:37".
fn format_reset_clock_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 local_dt = dt.with_timezone(&Local);
return format!("{}:{:02}", local_dt.hour(), local_dt.minute());
}
}
"?".to_string()
}

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) {
Expand Down Expand Up @@ -209,12 +246,18 @@ impl Segment for UsageSegment {
.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 display_format = segment_config
.and_then(|sc| sc.options.get("display_format"))
.and_then(|v| v.as_str())
.unwrap_or("default");

let (five_hour_util, seven_day_util, resets_at, five_hour_resets_at) = if use_cached {
let cache = cached_data.unwrap();
(
cache.five_hour_utilization,
cache.seven_day_utilization,
cache.resets_at,
cache.five_hour_resets_at,
)
} else {
match self.fetch_api_usage(api_base_url, &token, timeout) {
Expand All @@ -223,13 +266,15 @@ impl Segment for UsageSegment {
five_hour_utilization: response.five_hour.utilization,
seven_day_utilization: response.seven_day.utilization,
resets_at: response.seven_day.resets_at.clone(),
five_hour_resets_at: response.five_hour.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,
response.five_hour.resets_at,
)
}
None => {
Expand All @@ -238,6 +283,7 @@ impl Segment for UsageSegment {
cache.five_hour_utilization,
cache.seven_day_utilization,
cache.resets_at,
cache.five_hour_resets_at,
)
} else {
return None;
Expand All @@ -248,8 +294,43 @@ impl Segment for UsageSegment {

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()));

let (primary, secondary) = match display_format {
"session-percent" => (format!("{}%", five_hour_percent), String::new()),
"session-remaining" => {
let remaining =
Self::format_time_remaining(five_hour_resets_at.as_deref());
(
format!("{}%", five_hour_percent),
format!("· {}", remaining),
)
}
"session-full" => {
let remaining =
Self::format_time_remaining(five_hour_resets_at.as_deref());
let clock =
Self::format_reset_clock_time(five_hour_resets_at.as_deref());
(
format!("{}%", five_hour_percent),
format!("· {} {}", remaining, clock),
)
}
"weekly" => {
// 7-day utilization + 7-day reset date (same layout as old default but with weekly data)
let seven_day_percent = seven_day_util.round() as u8;
(
format!("{}%", seven_day_percent),
format!("· {}", Self::format_reset_time(resets_at.as_deref())),
)
}
_ => {
// "default": existing behavior
(
format!("{}%", five_hour_percent),
format!("· {}", Self::format_reset_time(resets_at.as_deref())),
)
}
};

let mut metadata = HashMap::new();
metadata.insert("dynamic_icon".to_string(), dynamic_icon);
Expand Down
74 changes: 60 additions & 14 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,12 @@ impl App {
eprintln!("Warning: Failed to initialize themes: {}", e);
}

// Load config
let mut config = Config::load().unwrap_or_else(|_| Config::default());

// If a theme is specified, reload it to get the latest changes
if !config.theme.is_empty() && config.theme != "default" {
if let Ok(theme_config) =
crate::ui::themes::ThemePresets::load_theme_from_file(&config.theme)
{
config = theme_config;
}
}
// Load config - use saved config.toml as the source of truth,
// only falling back to theme defaults if no config exists
let config = match Config::load() {
Ok(c) => c,
Err(_) => Config::default(),
};

// Terminal setup
enable_raw_mode()?;
Expand Down Expand Up @@ -563,9 +558,60 @@ impl App {
}
}
FieldSelection::Options => {
// TODO: Implement options editor
self.status_message =
Some("Options editor not implemented yet".to_string());
if let Some(segment) =
self.config.segments.get_mut(self.selected_segment)
{
match segment.id {
SegmentId::Usage => {
let current = segment
.options
.get("display_format")
.and_then(|v| v.as_str())
.unwrap_or("default")
.to_string();
let formats =
["default", "session-percent", "session-remaining", "session-full", "weekly"];
let current_idx = formats
.iter()
.position(|&f| f == current)
.unwrap_or(0);
let next_idx = (current_idx + 1) % formats.len();
segment.options.insert(
"display_format".to_string(),
serde_json::Value::String(
formats[next_idx].to_string(),
),
);
self.status_message = Some(format!(
"Usage display format: {}",
formats[next_idx]
));
self.preview.update_preview(&self.config);
}
SegmentId::Git => {
let current = segment
.options
.get("show_sha")
.and_then(|v| v.as_bool())
.unwrap_or(false);
segment.options.insert(
"show_sha".to_string(),
serde_json::Value::Bool(!current),
);
self.status_message = Some(format!(
"Git show SHA {}",
if !current { "enabled" } else { "disabled" }
));
self.preview.update_preview(&self.config);
}
_ => {
self.status_message = Some(
"No configurable options for this segment"
.to_string(),
);
}
}
}
}
}
}
Expand Down
24 changes: 19 additions & 5 deletions src/ui/components/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,25 @@ impl PreviewComponent {
map
},
},
SegmentId::Usage => SegmentData {
primary: "24%".to_string(),
secondary: "· 10-7-2".to_string(),
metadata: HashMap::new(),
},
SegmentId::Usage => {
let display_format = segment_config
.options
.get("display_format")
.and_then(|v| v.as_str())
.unwrap_or("default");
let (primary, secondary) = match display_format {
"session-percent" => ("24%".to_string(), String::new()),
"session-remaining" => ("24%".to_string(), "· 3h12".to_string()),
"session-full" => ("24%".to_string(), "· 3h12 20:19".to_string()),
"weekly" => ("15%".to_string(), "· 3-27-9".to_string()),
_ => ("24%".to_string(), "· 10-7-2".to_string()),
};
SegmentData {
primary,
secondary,
metadata: HashMap::new(),
}
}
SegmentId::Cost => SegmentData {
primary: "$0.02".to_string(),
secondary: "".to_string(),
Expand Down
30 changes: 26 additions & 4 deletions src/ui/components/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,32 @@ impl SettingsComponent {
),
create_field_line(
FieldSelection::Options,
vec![Span::raw(format!(
"└─ Options: {} items",
segment.options.len()
))],
if segment.id == SegmentId::Usage {
let format_value = segment
.options
.get("display_format")
.and_then(|v| v.as_str())
.unwrap_or("default");
vec![Span::raw(format!(
"└─ Display Format: {} [Enter to cycle]",
format_value
))]
} else if segment.id == SegmentId::Git {
let show_sha = segment
.options
.get("show_sha")
.and_then(|v| v.as_bool())
.unwrap_or(false);
vec![Span::raw(format!(
"└─ Show SHA: {} [Enter to toggle]",
if show_sha { "✓" } else { "✗" }
))]
} else {
vec![Span::raw(format!(
"└─ Options: {} items",
segment.options.len()
))]
},
),
];
let text = Text::from(lines);
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_cometix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_gruvbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_minimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_nord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_powerline_dark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_powerline_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_powerline_rose_pine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/ui/themes/theme_powerline_tokyo_night.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub fn usage_segment() -> SegmentConfig {
serde_json::Value::Number(180.into()),
);
opts.insert("timeout".to_string(), serde_json::Value::Number(2.into()));
opts.insert(
"display_format".to_string(),
serde_json::Value::String("default".to_string()),
);
opts
},
}
Expand Down