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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,30 @@ All segments are configurable with:

Supported segments: Directory, Git, Model, Usage, Time, Cost, OutputStyle

### Usage Segment Options

The `usage` segment displays Claude API usage from the `/api/oauth/usage` endpoint and supports the following options:

| Option | Type | Default | Description |
|---|---|---|---|
| `api_base_url` | string | `"https://api.anthropic.com"` | Base URL for the Anthropic API |
| `cache_duration` | integer | `300` | How long to cache API results (seconds) |
| `timeout` | integer | `2` | API request timeout (seconds) |
| `reset_period` | string | `"session"` | Which reset time to display: `"session"` (5-hour window) or `"weekly"` (7-day window) |
| `reset_format` | string | `"time"` | How to format the reset time: `"time"` (e.g. `2-22-13`) or `"duration"` (e.g. `4h 52m`) |

Example configuration:

```toml
[[segments]]
id = "usage"
enabled = true

[segments.options]
reset_period = "session"
reset_format = "duration"
cache_duration = 180
```

## Requirements

Expand Down
24 changes: 24 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,30 @@ CCometixLine 支持通过 TOML 文件和交互式 TUI 进行完整配置:

支持的段落:目录、Git、模型、使用量、时间、成本、输出样式

### 使用量段落选项

`usage` 段落通过 `/api/oauth/usage` 接口显示 Claude API 使用情况,支持以下选项:

| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `api_base_url` | 字符串 | `"https://api.anthropic.com"` | Anthropic API 的基础 URL |
| `cache_duration` | 整数 | `300` | API 结果缓存时长(秒) |
| `timeout` | 整数 | `2` | API 请求超时时间(秒) |
| `reset_period` | 字符串 | `"session"` | 显示哪个重置时间:`"session"`(5小时窗口)或 `"weekly"`(7天窗口) |
| `reset_format` | 字符串 | `"time"` | 重置时间的显示格式:`"time"`(例如 `2-22-13`)或 `"duration"`(例如 `4h 52m`) |

配置示例:

```toml
[[segments]]
id = "usage"
enabled = true

[segments.options]
reset_period = "session"
reset_format = "duration"
cache_duration = 180
```

## 系统要求

Expand Down
68 changes: 62 additions & 6 deletions src/core/segments/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ struct UsagePeriod {
struct ApiUsageCache {
five_hour_utilization: f64,
seven_day_utilization: f64,
resets_at: Option<String>,
five_hour_resets_at: Option<String>,
seven_day_resets_at: Option<String>,
cached_at: String,
}

Expand Down Expand Up @@ -65,6 +66,34 @@ impl UsageSegment {
"?".to_string()
}

fn format_reset_duration(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 = Utc::now();
let reset_utc = dt.with_timezone(&Utc);
let remaining = reset_utc.signed_duration_since(now);

if remaining.num_seconds() <= 0 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Duration formatting can show 0m when there is still almost a full minute remaining.

Because remaining.num_minutes() truncates fractional minutes, any remaining time under 60 seconds will display as 0m rather than something like "now". If you want very short durations to show "now", consider a small threshold (e.g. treat durations < 60 seconds as "now") or base this decision on num_seconds() instead of minutes.

Suggested change
if remaining.num_seconds() <= 0 {
if remaining.num_seconds() < 60 {

return "now".to_string();
}

let total_minutes = remaining.num_minutes();
let days = total_minutes / (24 * 60);
let hours = (total_minutes % (24 * 60)) / 60;
let minutes = total_minutes % 60;

return if days > 0 {
format!("{}d {}h", days, hours)
} else if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
};
}
}
"?".to_string()
}

fn get_cache_path() -> Option<std::path::PathBuf> {
let home = dirs::home_dir()?;
Some(
Expand Down Expand Up @@ -209,26 +238,41 @@ 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 reset_period = segment_config
.and_then(|sc| sc.options.get("reset_period"))
.and_then(|v| v.as_str())
.unwrap_or("session")
.to_string();

let reset_format = segment_config
.and_then(|sc| sc.options.get("reset_format"))
.and_then(|v| v.as_str())
.unwrap_or("time")
.to_string();

let (five_hour_util, seven_day_util, five_hour_resets_at, seven_day_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,
cache.seven_day_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(),
five_hour_resets_at: response.five_hour.resets_at.clone(),
seven_day_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.five_hour.resets_at,
response.seven_day.resets_at,
)
}
Expand All @@ -237,7 +281,8 @@ impl Segment for UsageSegment {
(
cache.five_hour_utilization,
cache.seven_day_utilization,
cache.resets_at,
cache.five_hour_resets_at,
cache.seven_day_resets_at,
)
} else {
return None;
Expand All @@ -246,10 +291,21 @@ impl Segment for UsageSegment {
}
};

let resets_at = if reset_period == "weekly" {
seven_day_resets_at.as_deref()
} else {
five_hour_resets_at.as_deref()
};

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 reset_str = if reset_format == "duration" {
Self::format_reset_duration(resets_at)
} else {
Self::format_reset_time(resets_at)
};
let secondary = format!("· {}", reset_str);

let mut metadata = HashMap::new();
metadata.insert("dynamic_icon".to_string(), dynamic_icon);
Expand Down