mirror of
https://github.com/starship/starship.git
synced 2026-06-24 02:01:36 +07:00
feat: add statusline subcommand for Claude Code integration (#7234)
This commit is contained in:
+211
-1
@@ -60,7 +60,9 @@
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {}
|
||||
"default": {
|
||||
"claude-code": "$claude_model$git_branch$claude_context$claude_cost"
|
||||
}
|
||||
},
|
||||
"aws": {
|
||||
"$ref": "#/$defs/AwsConfig",
|
||||
@@ -182,6 +184,72 @@
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"claude_context": {
|
||||
"$ref": "#/$defs/ClaudeContextConfig",
|
||||
"default": {
|
||||
"format": "[$gauge $percentage]($style) ",
|
||||
"symbol": "",
|
||||
"gauge_width": 5,
|
||||
"display": [
|
||||
{
|
||||
"threshold": 0.0,
|
||||
"style": "bold green",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"threshold": 30.0,
|
||||
"style": "bold green",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 60.0,
|
||||
"style": "bold yellow",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 80.0,
|
||||
"style": "bold red",
|
||||
"hidden": false
|
||||
}
|
||||
],
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"claude_cost": {
|
||||
"$ref": "#/$defs/ClaudeCostConfig",
|
||||
"default": {
|
||||
"format": "[$symbol(\\$$cost)]($style) ",
|
||||
"symbol": "💰 ",
|
||||
"display": [
|
||||
{
|
||||
"threshold": 0.0,
|
||||
"style": "bold green",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"threshold": 1.0,
|
||||
"style": "bold yellow",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 5.0,
|
||||
"style": "bold red",
|
||||
"hidden": false
|
||||
}
|
||||
],
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"claude_model": {
|
||||
"$ref": "#/$defs/ClaudeModelConfig",
|
||||
"default": {
|
||||
"format": "[$symbol$model]($style) ",
|
||||
"symbol": "🤖 ",
|
||||
"style": "bold blue",
|
||||
"model_aliases": {},
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"cmake": {
|
||||
"$ref": "#/$defs/CMakeConfig",
|
||||
"default": {
|
||||
@@ -2233,6 +2301,148 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ClaudeContextConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"default": "[$gauge $percentage]($style) "
|
||||
},
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"gauge_width": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"default": 5
|
||||
},
|
||||
"display": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ClaudeDisplayConfig"
|
||||
},
|
||||
"default": [
|
||||
{
|
||||
"threshold": 0.0,
|
||||
"style": "bold green",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"threshold": 30.0,
|
||||
"style": "bold green",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 60.0,
|
||||
"style": "bold yellow",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 80.0,
|
||||
"style": "bold red",
|
||||
"hidden": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ClaudeDisplayConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"threshold": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"default": 0.0
|
||||
},
|
||||
"style": {
|
||||
"type": "string",
|
||||
"default": "bold green"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ClaudeCostConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"default": "[$symbol(\\$$cost)]($style) "
|
||||
},
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"default": "💰 "
|
||||
},
|
||||
"display": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ClaudeDisplayConfig"
|
||||
},
|
||||
"default": [
|
||||
{
|
||||
"threshold": 0.0,
|
||||
"style": "bold green",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"threshold": 1.0,
|
||||
"style": "bold yellow",
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"threshold": 5.0,
|
||||
"style": "bold red",
|
||||
"hidden": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ClaudeModelConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"default": "[$symbol$model]($style) "
|
||||
},
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"default": "🤖 "
|
||||
},
|
||||
"style": {
|
||||
"type": "string",
|
||||
"default": "bold blue"
|
||||
},
|
||||
"model_aliases": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {}
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"CMakeConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -338,6 +338,316 @@ Note: Continuation prompts are only available in the following shells:
|
||||
continuation_prompt = '▶▶ '
|
||||
```
|
||||
|
||||
## Statusline for Claude Code
|
||||
|
||||
Starship supports displaying a custom statusline when running inside Claude Code, Anthropic's CLI tool for interactive coding with Claude. This statusline provides real-time information about your Claude session, including the model being used, context window usage, and session costs.
|
||||
|
||||
For more information about the Claude Code statusline feature, see the [Claude Code statusline documentation](https://code.claude.com/docs/en/statusline).
|
||||
|
||||
### Setup
|
||||
|
||||
To use Starship as your Claude Code statusline:
|
||||
|
||||
1. Run `/statusline` in Claude Code and ask it to configure Starship, or manually add the following to your `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "starship statusline claude-code"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Customize the statusline appearance in your `~/.config/starship.toml` (see [Configuration](#configuration) below)
|
||||
|
||||
### Overview
|
||||
|
||||
When invoked with `starship statusline claude-code`, Starship receives Claude Code session data via stdin and renders a statusline using a dedicated profile named `claude-code`.
|
||||
|
||||
The profile includes three specialized modules:
|
||||
|
||||
- `claude_model`: Displays the current Claude model being used
|
||||
- `claude_context`: Shows context window usage with a visual gauge
|
||||
- `claude_cost`: Displays session cost and statistics
|
||||
|
||||
The default profile format is:
|
||||
|
||||
```toml
|
||||
[profiles]
|
||||
claude-code = "$claude_model$git_branch$claude_context$claude_cost"
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
You can customize the Claude Code statusline by modifying the `claude-code` profile and individual module configurations in your `~/.config/starship.toml`:
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
# Customize the claude-code profile
|
||||
[profiles]
|
||||
claude-code = "$claude_model$claude_context$claude_cost"
|
||||
|
||||
# Configure individual modules
|
||||
[claude_model]
|
||||
format = "[$symbol$model]($style) "
|
||||
symbol = "🤖 "
|
||||
style = "bold blue"
|
||||
|
||||
[claude_context]
|
||||
format = "[$gauge $percentage]($style) "
|
||||
gauge_width = 10
|
||||
|
||||
[claude_cost]
|
||||
format = "[$symbol$cost]($style) "
|
||||
symbol = "💰 "
|
||||
```
|
||||
|
||||
### Claude Model
|
||||
|
||||
The `claude_model` module displays the current Claude model being used in the session.
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------- | ---------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `format` | `'[$symbol$model]($style) '` | The format for the module. |
|
||||
| `symbol` | `'🤖 '` | The symbol shown before the model name. |
|
||||
| `style` | `'bold blue'` | The style for the module. |
|
||||
| `model_aliases` | `{}` | Map of model IDs or display names to shorter aliases. Checks ID first, then display name. |
|
||||
| `disabled` | `false` | Disables the `claude_model` module. |
|
||||
|
||||
#### Variables
|
||||
|
||||
| Variable | Example | Description |
|
||||
| -------- | ------------------- | ------------------------------------- |
|
||||
| model | `Claude 3.5 Sonnet` | The display name of the current model |
|
||||
| model_id | `claude-3-5-sonnet` | The model ID |
|
||||
| symbol | | Mirrors the value of option `symbol` |
|
||||
| style\* | | Mirrors the value of option `style` |
|
||||
|
||||
\*: This variable can only be used as a part of a style string
|
||||
|
||||
#### Examples
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
# Basic customization
|
||||
[claude_model]
|
||||
format = "on [$symbol$model]($style) "
|
||||
symbol = "🧠 "
|
||||
style = "bold cyan"
|
||||
|
||||
# Using model aliases for vendor-specific model names
|
||||
# You can alias by model ID or display name
|
||||
[claude_model.model_aliases]
|
||||
# Alias by vendor model ID (e.g. AWS Bedrock)
|
||||
"global.anthropic.claude-sonnet-4-5-20250929-v1:0" = "Sonnet 4.5"
|
||||
# Alias by display name
|
||||
"Claude Sonnet 4.5 (Vendor Proxy)" = "Sonnet"
|
||||
```
|
||||
|
||||
### Claude Context
|
||||
|
||||
The `claude_context` module displays context window usage as a percentage and visual gauge. The style automatically changes based on configurable thresholds.
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------------------- | --------------------------------- | -------------------------------------------------- |
|
||||
| `format` | `'[$gauge $percentage]($style) '` | The format for the module. |
|
||||
| `symbol` | `''` | The symbol shown before the gauge. |
|
||||
| `gauge_width` | `5` | The width of the gauge in characters. |
|
||||
| `gauge_full_symbol` | `'█'` | The symbol used for filled segments of the gauge. |
|
||||
| `gauge_partial_symbol` | `'▒'` | The symbol used for partial segments of the gauge. |
|
||||
| `gauge_empty_symbol` | `'░'` | The symbol used for empty segments of the gauge. |
|
||||
| `display` | [see below](#display) | Threshold and style configurations. |
|
||||
| `disabled` | `false` | Disables the `claude_context` module. |
|
||||
|
||||
##### Display
|
||||
|
||||
The `display` option is an array of objects that define thresholds and styles for different usage levels. The module uses the style from the highest matching threshold or hides the module if `hidden` is `true`.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------- | ------------ | ------------------------------------------------------------------------ |
|
||||
| `threshold` | `0.0` | The minimum context windows usage percentage to match this configuration |
|
||||
| `style` | `bold green` | The value of `style` if this display configuration is matched |
|
||||
| `hidden` | `false` | Hide this module if this the configuration is matched. |
|
||||
|
||||
```toml
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
hidden = true
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 30
|
||||
style = "bold green"
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 60
|
||||
style = "bold yellow"
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 80
|
||||
style = "bold red"
|
||||
```
|
||||
|
||||
#### Variables
|
||||
|
||||
| Variable | Example | Description |
|
||||
| -------------------------- | ------- | ----------------------------------------------------- |
|
||||
| gauge | `██▒░░` | Visual representation of context usage |
|
||||
| percentage | `65%` | Context usage as a percentage |
|
||||
| input_tokens | `45.2k` | Total input tokens in conversation |
|
||||
| output_tokens | `12.3k` | Total output tokens in conversation |
|
||||
| curr_input_tokens | `5.1k` | Input tokens from most recent API call |
|
||||
| curr_output_tokens | `1.2k` | Output tokens from most recent API call |
|
||||
| curr_cache_creation_tokens | `1.5k` | Cache creation tokens from most recent API call |
|
||||
| curr_cache_read_tokens | `23.4k` | Cache read tokens from most recent API call |
|
||||
| total_tokens | `200k` | Total context window size |
|
||||
| symbol | | Mirrors the value of option `symbol` |
|
||||
| style\* | | Mirrors the style from the matching display threshold |
|
||||
|
||||
\*: This variable can only be used as a part of a style string
|
||||
|
||||
#### Examples
|
||||
|
||||
**Minimal gauge-only display**
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
[claude_context]
|
||||
format = "[$gauge]($style) "
|
||||
gauge_width = 10
|
||||
```
|
||||
|
||||
**Detailed token information**
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
[claude_context]
|
||||
format = "[$percentage ($input_tokens in / $output_tokens out)]($style) "
|
||||
```
|
||||
|
||||
**Custom gauge symbols**
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
[claude_context]
|
||||
gauge_full_symbol = "▰"
|
||||
gauge_partial_symbol = ""
|
||||
gauge_empty_symbol = "▱"
|
||||
gauge_width = 10
|
||||
format = "[$gauge]($style) "
|
||||
```
|
||||
|
||||
**Custom thresholds**
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 50
|
||||
style = "bold yellow"
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 75
|
||||
style = "bold orange"
|
||||
|
||||
[[claude_context.display]]
|
||||
threshold = 90
|
||||
style = "bold red"
|
||||
```
|
||||
|
||||
### Claude Cost
|
||||
|
||||
The `claude_cost` module displays the total cost of the current Claude Code session in USD. Like `claude_context`, it supports threshold-based styling.
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------- | -------------------------------- | ----------------------------------- |
|
||||
| `format` | `'[$symbol(\\$$cost)]($style) '` | The format for the module. |
|
||||
| `symbol` | `'💰 '` | The symbol shown before the cost. |
|
||||
| `display` | [see below](#display-1) | Threshold and style configurations. |
|
||||
| `disabled` | `false` | Disables the `claude_cost` module. |
|
||||
|
||||
##### Display
|
||||
|
||||
The `display` option is an array of objects that define cost thresholds and styles. The module uses the style from the highest matching threshold or hides the module if `hidden` is `true`.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------- | ------------ | ------------------------------------------------------------- |
|
||||
| `threshold` | `0.0` | The minimum cost in USD to match this configuration |
|
||||
| `style` | `bold green` | The value of `style` if this display configuration is matched |
|
||||
| `hidden` | `false` | Hide this module if this configuration is matched. |
|
||||
|
||||
**Default configuration:**
|
||||
|
||||
```toml
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
hidden = true
|
||||
|
||||
[[claude_cost.display]]
|
||||
threshold = 1.0
|
||||
style = "bold yellow"
|
||||
|
||||
[[claude_cost.display]]
|
||||
threshold = 5.0
|
||||
style = "bold red"
|
||||
```
|
||||
|
||||
#### Variables
|
||||
|
||||
| Variable | Example | Description |
|
||||
| ------------- | -------- | ----------------------------------------------------- |
|
||||
| cost | `1.23` | Total session cost in USD (formatted to 2 decimals) |
|
||||
| duration | `1m 30s` | Total session duration |
|
||||
| api_duration | `45s` | Total API call duration |
|
||||
| lines_added | `1.2k` | Total lines of code added |
|
||||
| lines_removed | `500` | Total lines of code removed |
|
||||
| symbol | | Mirrors the value of option `symbol` |
|
||||
| style\* | | Mirrors the style from the matching display threshold |
|
||||
|
||||
\*: This variable can only be used as a part of a style string
|
||||
|
||||
#### Examples
|
||||
|
||||
```toml
|
||||
# ~/.config/starship.toml
|
||||
|
||||
# Cost with code change statistics
|
||||
[claude_cost]
|
||||
format = "[$symbol$cost (+$lines_added -$lines_removed)]($style) "
|
||||
|
||||
# Hide module until cost exceeds $0.10
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
hidden = true
|
||||
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.10
|
||||
style = "bold yellow"
|
||||
|
||||
[[claude_cost.display]]
|
||||
threshold = 2.0
|
||||
style = "bold red"
|
||||
|
||||
# Show duration information
|
||||
[claude_cost]
|
||||
format = "[$symbol$cost ($duration)]($style) "
|
||||
```
|
||||
|
||||
## Style Strings
|
||||
|
||||
Style strings are a list of words, separated by whitespace. The words are not case sensitive (i.e. `bold` and `BoLd` are considered the same string). Each word can be one of the following:
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
use crate::configs::Palette;
|
||||
use crate::context::Context;
|
||||
|
||||
use crate::serde_utils::{ValueDeserializer, ValueRef};
|
||||
use crate::utils;
|
||||
use crate::utils::serde::{ValueDeserializer, ValueRef};
|
||||
use nu_ansi_term::Color;
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, de::Error as SerdeError, de::value::Error as ValueError,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct ClaudeContextConfig<'a> {
|
||||
pub format: &'a str,
|
||||
pub symbol: &'a str,
|
||||
pub gauge_width: u8,
|
||||
pub gauge_full_symbol: &'a str,
|
||||
pub gauge_partial_symbol: &'a str,
|
||||
pub gauge_empty_symbol: &'a str,
|
||||
#[serde(borrow)]
|
||||
pub display: Vec<ClaudeDisplayConfig<'a>>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Default for ClaudeContextConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "[$gauge $percentage]($style) ",
|
||||
symbol: "",
|
||||
gauge_width: 5,
|
||||
gauge_full_symbol: "█",
|
||||
gauge_partial_symbol: "▒",
|
||||
gauge_empty_symbol: "░",
|
||||
display: vec![
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 0.,
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
},
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 30.,
|
||||
style: "bold green",
|
||||
..Default::default()
|
||||
},
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 60.,
|
||||
style: "bold yellow",
|
||||
..Default::default()
|
||||
},
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 80.,
|
||||
style: "bold red",
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct ClaudeDisplayConfig<'a> {
|
||||
pub threshold: f32,
|
||||
pub style: &'a str,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl Default for ClaudeDisplayConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 0.,
|
||||
style: "bold green",
|
||||
hidden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::configs::claude_context::ClaudeDisplayConfig;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct ClaudeCostConfig<'a> {
|
||||
pub format: &'a str,
|
||||
pub symbol: &'a str,
|
||||
#[serde(borrow)]
|
||||
pub display: Vec<ClaudeDisplayConfig<'a>>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Default for ClaudeCostConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "[$symbol(\\$$cost)]($style) ",
|
||||
symbol: "💰 ",
|
||||
display: vec![
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 0.00,
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
},
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 1.0,
|
||||
style: "bold yellow",
|
||||
..Default::default()
|
||||
},
|
||||
ClaudeDisplayConfig {
|
||||
threshold: 5.0,
|
||||
style: "bold red",
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct ClaudeModelConfig<'a> {
|
||||
pub format: &'a str,
|
||||
pub symbol: &'a str,
|
||||
pub style: &'a str,
|
||||
pub model_aliases: IndexMap<String, &'a str>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Default for ClaudeModelConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "[$symbol$model]($style) ",
|
||||
symbol: "🤖 ",
|
||||
style: "bold blue",
|
||||
model_aliases: IndexMap::new(),
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ pub mod bun;
|
||||
pub mod c;
|
||||
pub mod cc;
|
||||
pub mod character;
|
||||
pub mod claude_context;
|
||||
pub mod claude_cost;
|
||||
pub mod claude_model;
|
||||
pub mod cmake;
|
||||
pub mod cmd_duration;
|
||||
pub mod cobol;
|
||||
@@ -139,6 +142,12 @@ pub struct FullConfig<'a> {
|
||||
#[serde(borrow)]
|
||||
character: character::CharacterConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
claude_context: claude_context::ClaudeContextConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
claude_cost: claude_cost::ClaudeCostConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
claude_model: claude_model::ClaudeModelConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
cmake: cmake::CMakeConfig<'a>,
|
||||
#[serde(borrow)]
|
||||
cmd_duration: cmd_duration::CmdDurationConfig<'a>,
|
||||
|
||||
@@ -2,6 +2,13 @@ use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn default_profiles() -> IndexMap<String, String> {
|
||||
IndexMap::from_iter([(
|
||||
"claude-code".to_string(),
|
||||
"$claude_model$git_branch$claude_context$claude_cost".to_string(),
|
||||
)])
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
@@ -22,7 +29,11 @@ pub struct StarshipRootConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub palette: Option<String>,
|
||||
pub palettes: HashMap<String, Palette>,
|
||||
pub profiles: IndexMap<String, String>,
|
||||
#[serde(rename = "profiles")]
|
||||
#[cfg_attr(feature = "config-schema", schemars(default = "default_profiles"))]
|
||||
pub user_profiles: IndexMap<String, String>,
|
||||
#[serde(skip)]
|
||||
pub internal_profiles: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
pub type Palette = HashMap<String, String>;
|
||||
@@ -146,7 +157,8 @@ impl Default for StarshipRootConfig {
|
||||
format: "$all".to_string(),
|
||||
right_format: String::new(),
|
||||
continuation_prompt: "[∙](bright-black) ".to_string(),
|
||||
profiles: Default::default(),
|
||||
user_profiles: IndexMap::new(),
|
||||
internal_profiles: default_profiles(),
|
||||
scan_timeout: 30,
|
||||
command_timeout: 500,
|
||||
add_newline: true,
|
||||
|
||||
+1
-4
@@ -318,10 +318,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
use toml_edit::Item;
|
||||
|
||||
use crate::{
|
||||
context::{Properties, Shell, Target},
|
||||
context_env::Env,
|
||||
};
|
||||
use crate::context::{Env, Properties, Shell, Target};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
+15
-1
@@ -1,6 +1,5 @@
|
||||
use crate::config::{ModuleConfig, StarshipConfig};
|
||||
use crate::configs::StarshipRootConfig;
|
||||
use crate::context_env::Env;
|
||||
use crate::module::Module;
|
||||
use crate::utils::{CommandOutput, PathExt, create_command, exec_timeout, read_file};
|
||||
|
||||
@@ -29,6 +28,11 @@ use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use terminal_size::terminal_size;
|
||||
|
||||
pub use crate::utils::env::Env;
|
||||
pub use crate::utils::statusline::{
|
||||
ClaudeCodeData, ContextWindow, CostInfo, CurrentUsage, ModelInfo, Workspace,
|
||||
};
|
||||
|
||||
/// Context contains data or common methods that may be used by multiple modules.
|
||||
/// The data contained within Context will be relevant to this particular rendering
|
||||
/// of the prompt.
|
||||
@@ -79,6 +83,9 @@ pub struct Context<'a> {
|
||||
/// Starship root config
|
||||
pub root_config: StarshipRootConfig,
|
||||
|
||||
/// Claude Code session data (when running as statusline)
|
||||
pub claude_code_data: Option<Box<ClaudeCodeData>>,
|
||||
|
||||
/// Avoid issues with unused lifetimes when features are disabled
|
||||
_marker: PhantomData<&'a ()>,
|
||||
}
|
||||
@@ -180,6 +187,7 @@ impl<'a> Context<'a> {
|
||||
#[cfg(feature = "battery")]
|
||||
battery_info_provider: &crate::modules::BatteryInfoProviderImpl,
|
||||
root_config,
|
||||
claude_code_data: None,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -193,6 +201,12 @@ impl<'a> Context<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the Claude Code session data
|
||||
pub fn with_claude_code_data(mut self, data: ClaudeCodeData) -> Self {
|
||||
self.claude_code_data = Some(Box::new(data));
|
||||
self
|
||||
}
|
||||
|
||||
// Tries to retrieve home directory from a table in testing mode or else retrieves it from the os
|
||||
pub fn get_home(&self) -> Option<PathBuf> {
|
||||
home_dir(&self.env)
|
||||
|
||||
@@ -13,7 +13,6 @@ pub mod config;
|
||||
pub mod configs;
|
||||
pub mod configure;
|
||||
pub mod context;
|
||||
pub mod context_env;
|
||||
pub mod formatter;
|
||||
pub mod init;
|
||||
pub mod logger;
|
||||
@@ -21,7 +20,6 @@ pub mod module;
|
||||
mod modules;
|
||||
pub mod print;
|
||||
mod segment;
|
||||
mod serde_utils;
|
||||
mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+27
@@ -57,6 +57,12 @@ fn generate_completions(shell: CompletionShell) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
enum Statuslines {
|
||||
#[clap(alias = "claude")]
|
||||
ClaudeCode,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Create a pre-populated GitHub issue with information about your configuration
|
||||
@@ -129,6 +135,15 @@ enum Commands {
|
||||
},
|
||||
/// Generate random session key
|
||||
Session,
|
||||
/// Prints the statusline with a specific profile
|
||||
Statusline {
|
||||
/// The statusline provider to use
|
||||
provider: Statuslines,
|
||||
#[clap(long)]
|
||||
profile: Option<String>,
|
||||
#[clap(flatten)]
|
||||
properties: Properties,
|
||||
},
|
||||
/// Prints time in milliseconds
|
||||
#[clap(hide = true)]
|
||||
Time,
|
||||
@@ -275,6 +290,18 @@ fn main() {
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
),
|
||||
Commands::Statusline {
|
||||
provider,
|
||||
profile,
|
||||
properties,
|
||||
} => {
|
||||
let profile = profile.unwrap_or_else(|| match provider {
|
||||
Statuslines::ClaudeCode => "claude-code".to_string(),
|
||||
});
|
||||
|
||||
let target = Target::Profile(profile);
|
||||
print::prompt_with_claude_code(properties, target);
|
||||
}
|
||||
#[cfg(feature = "config-schema")]
|
||||
Commands::ConfigSchema => print::print_schema(),
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub const ALL_MODULES: &[&str] = &[
|
||||
"bun",
|
||||
"c",
|
||||
"character",
|
||||
"claude_context",
|
||||
"claude_cost",
|
||||
"claude_model",
|
||||
"cmake",
|
||||
"cmd_duration",
|
||||
"cobol",
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
use crate::configs::claude_context::ClaudeContextConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
use crate::utils::humanize_int;
|
||||
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("claude_context");
|
||||
let config = ClaudeContextConfig::try_load(module.config);
|
||||
|
||||
if config.disabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read Claude Code data from Context
|
||||
let claude_data = context.claude_code_data.as_ref()?;
|
||||
|
||||
let total_tokens = claude_data.context_window.context_window_size;
|
||||
let percentage_float = claude_data.context_window.used_percentage.clamp(0.0, 100.0);
|
||||
let percentage = percentage_float.round() as u8;
|
||||
|
||||
// Determine style based on percentage
|
||||
let display_style = config
|
||||
.display
|
||||
.iter()
|
||||
.filter(|s| percentage_float >= s.threshold)
|
||||
.max_by(|a, b| {
|
||||
a.threshold
|
||||
.partial_cmp(&b.threshold)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
if display_style.is_some_and(|s| s.hidden) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(display_style) = display_style {
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
.map_meta(|variable, _| match variable {
|
||||
"symbol" => Some(config.symbol),
|
||||
_ => None,
|
||||
})
|
||||
.map_style(|variable| match variable {
|
||||
"style" => Some(Ok(display_style.style)),
|
||||
_ => None,
|
||||
})
|
||||
.map(|variable| match variable {
|
||||
"gauge" => {
|
||||
let filled_float = (percentage as f64 / 100.0) * config.gauge_width as f64;
|
||||
let (filled_count, partial) = if !config.gauge_partial_symbol.is_empty() {
|
||||
let full = filled_float.floor() as usize;
|
||||
let rem = filled_float - full as f64;
|
||||
// Show partial block if remainder is significant enough (> 0.25)
|
||||
// but if it's very close to 1 (> 0.75), just round up to full (handled by empty_count check)
|
||||
if rem >= 0.25 {
|
||||
(full, true)
|
||||
} else {
|
||||
(full, false)
|
||||
}
|
||||
} else {
|
||||
(filled_float.round() as usize, false)
|
||||
};
|
||||
|
||||
let filled_count = filled_count.min(config.gauge_width as usize);
|
||||
let partial_count = if partial && filled_count < config.gauge_width as usize
|
||||
{
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let empty_count = (config.gauge_width as usize)
|
||||
.saturating_sub(filled_count + partial_count);
|
||||
|
||||
let gauge = config.gauge_full_symbol.repeat(filled_count)
|
||||
+ &config.gauge_partial_symbol.repeat(partial_count)
|
||||
+ &config.gauge_empty_symbol.repeat(empty_count);
|
||||
Some(Ok(gauge))
|
||||
}
|
||||
"percentage" => Some(Ok(format!("{percentage}%"))),
|
||||
"input_tokens" => Some(Ok(humanize_int(
|
||||
claude_data.context_window.total_input_tokens,
|
||||
))),
|
||||
"output_tokens" => Some(Ok(humanize_int(
|
||||
claude_data.context_window.total_output_tokens,
|
||||
))),
|
||||
"curr_input_tokens" => Some(Ok(humanize_int(
|
||||
claude_data.context_window.current_usage.input_tokens,
|
||||
))),
|
||||
"curr_output_tokens" => Some(Ok(humanize_int(
|
||||
claude_data.context_window.current_usage.output_tokens,
|
||||
))),
|
||||
"curr_cache_creation_tokens" => Some(Ok(humanize_int(
|
||||
claude_data
|
||||
.context_window
|
||||
.current_usage
|
||||
.cache_creation_input_tokens,
|
||||
))),
|
||||
"curr_cache_read_tokens" => Some(Ok(humanize_int(
|
||||
claude_data
|
||||
.context_window
|
||||
.current_usage
|
||||
.cache_read_input_tokens,
|
||||
))),
|
||||
"total_tokens" => Some(Ok(humanize_int(total_tokens))),
|
||||
_ => None,
|
||||
})
|
||||
.parse(None, Some(context))
|
||||
});
|
||||
|
||||
module.set_segments(match parsed {
|
||||
Ok(segments) => segments,
|
||||
Err(error) => {
|
||||
log::warn!("Error in module `claude_context`: {error}");
|
||||
return None;
|
||||
}
|
||||
});
|
||||
|
||||
Some(module)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::ModuleRenderer;
|
||||
use nu_ansi_term::Color;
|
||||
|
||||
#[test]
|
||||
fn test_without_data() {
|
||||
let actual = ModuleRenderer::new("claude_context").collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled() {
|
||||
let data = get_test_claude_data(50.0);
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
disabled = true
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_format_variables() {
|
||||
let data = get_test_claude_data(0.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
format = "[$input_tokens/$output_tokens/$total_tokens/$curr_input_tokens/$curr_output_tokens/$curr_cache_creation_tokens/$curr_cache_read_tokens]($style) "
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!(
|
||||
"{} ",
|
||||
Color::Green.bold().paint("1k/500/200k/1k/500/1k/2k")
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_total_tokens() {
|
||||
let mut data = get_test_claude_data(0.0);
|
||||
data.context_window.context_window_size = 0;
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!("{} ", Color::Green.bold().paint("░░░░░ 0%"))),
|
||||
"zero context window size should render as 0%"
|
||||
);
|
||||
}
|
||||
|
||||
fn get_test_claude_data(used_percentage: f32) -> crate::context::ClaudeCodeData {
|
||||
crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "claude-3-5-sonnet".to_string(),
|
||||
display_name: "Claude 3.5 Sonnet".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow {
|
||||
context_window_size: 200000,
|
||||
total_input_tokens: 1000,
|
||||
total_output_tokens: 500,
|
||||
used_percentage,
|
||||
current_usage: crate::context::CurrentUsage {
|
||||
input_tokens: 1000,
|
||||
output_tokens: 500,
|
||||
cache_creation_input_tokens: 1000,
|
||||
cache_read_input_tokens: 2000,
|
||||
},
|
||||
},
|
||||
cost: None,
|
||||
workspace: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_with_data() {
|
||||
let data = get_test_claude_data(50.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context.display]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!("{} ", Color::Green.bold().paint("██▒░░ 50%")));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_thresholds() {
|
||||
let data_low = get_test_claude_data(25.0);
|
||||
let data_medium = get_test_claude_data(65.0);
|
||||
let data_high = get_test_claude_data(85.0);
|
||||
|
||||
let config = toml::toml! {
|
||||
[claude_context]
|
||||
format = "[$gauge]($style) "
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
[[claude_context.display]]
|
||||
threshold = 60
|
||||
style = "bold yellow"
|
||||
[[claude_context.display]]
|
||||
threshold = 80
|
||||
style = "bold red"
|
||||
};
|
||||
|
||||
let actual_low = ModuleRenderer::new("claude_context")
|
||||
.config(config.clone())
|
||||
.claude_code_data(data_low)
|
||||
.collect();
|
||||
let expected_low = Some(format!("{} ", Color::Green.bold().paint("█▒░░░")));
|
||||
assert_eq!(actual_low, expected_low);
|
||||
|
||||
let actual_medium = ModuleRenderer::new("claude_context")
|
||||
.config(config.clone())
|
||||
.claude_code_data(data_medium)
|
||||
.collect();
|
||||
let expected_medium = Some(format!("{} ", Color::Yellow.bold().paint("███▒░")));
|
||||
assert_eq!(actual_medium, expected_medium);
|
||||
|
||||
let actual_high = ModuleRenderer::new("claude_context")
|
||||
.config(config)
|
||||
.claude_code_data(data_high)
|
||||
.collect();
|
||||
let expected_high = Some(format!("{} ", Color::Red.bold().paint("████▒")));
|
||||
assert_eq!(actual_high, expected_high);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gauge_width() {
|
||||
let data = get_test_claude_data(50.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
gauge_width = 10
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!("{} ", Color::Green.bold().paint("█████░░░░░ 50%")));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_partial_symbol_rounds() {
|
||||
let data = get_test_claude_data(50.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
gauge_partial_symbol = ""
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!("{} ", Color::Green.bold().paint("███░░ 50%"))),
|
||||
"empty gauge_partial_symbol should round to nearest full block"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_not_shown_when_remainder_below_threshold() {
|
||||
let data = get_test_claude_data(22.0); // rem=0.1 < 0.25 at gauge_width=5
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!("{} ", Color::Green.bold().paint("█░░░░ 22%"))),
|
||||
"partial block should not appear when remainder < 0.25"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_gauge_suppresses_partial() {
|
||||
let data = get_test_claude_data(100.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold red"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!("{} ", Color::Red.bold().paint("█████ 100%"))),
|
||||
"fully filled gauge should show no partial or empty blocks"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_when_below_threshold() {
|
||||
let data = get_test_claude_data(25.0); // below default threshold of 30
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"module should be hidden below the 30% threshold"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_when_no_display_matches() {
|
||||
let data = get_test_claude_data(10.0);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
[[claude_context.display]]
|
||||
threshold = 50
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"module should be hidden when no display entry matches"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_gauge_symbols() {
|
||||
let data = get_test_claude_data(27.5);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_context")
|
||||
.config(toml::toml! {
|
||||
[claude_context]
|
||||
gauge_full_symbol = "f"
|
||||
gauge_partial_symbol = "p"
|
||||
gauge_empty_symbol = "e"
|
||||
gauge_width = 10
|
||||
[[claude_context.display]]
|
||||
threshold = 0
|
||||
style = "bold green"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
// 27.5% of 10 is 2.75.
|
||||
// Full: 2 (floor of 2.75)
|
||||
// Rem: 0.75 -> >= 0.25 so partial is true
|
||||
// Gauge: 2 full + 1 partial + 7 empty = 10 total
|
||||
let expected = Some(format!("{} ", Color::Green.bold().paint("ffpeeeeeee 28%")));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
use crate::configs::claude_cost::ClaudeCostConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
use crate::utils::{humanize_int, render_time};
|
||||
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("claude_cost");
|
||||
let config = ClaudeCostConfig::try_load(module.config);
|
||||
|
||||
if config.disabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read Claude Code data from Context
|
||||
let claude_data = context.claude_code_data.as_ref()?;
|
||||
let cost_info = claude_data.cost.as_ref()?;
|
||||
let total_cost = cost_info.total_cost_usd;
|
||||
|
||||
let display_style = config
|
||||
.display
|
||||
.iter()
|
||||
.filter(|s| total_cost >= (s.threshold as f64))
|
||||
.max_by(|a, b| {
|
||||
a.threshold
|
||||
.partial_cmp(&b.threshold)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
if display_style.is_some_and(|s| s.hidden) {
|
||||
return None;
|
||||
}
|
||||
if let Some(display_style) = display_style {
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
.map_meta(|variable, _| match variable {
|
||||
"symbol" => Some(config.symbol),
|
||||
_ => None,
|
||||
})
|
||||
.map_style(|variable| match variable {
|
||||
"style" => Some(Ok(display_style.style)),
|
||||
_ => None,
|
||||
})
|
||||
.map(|variable| match variable {
|
||||
"cost" => Some(Ok(format!("{:.2}", total_cost))),
|
||||
"duration" => Some(Ok(render_time(cost_info.total_duration_ms as u128, false))),
|
||||
"api_duration" => Some(Ok(render_time(
|
||||
cost_info.total_api_duration_ms as u128,
|
||||
false,
|
||||
))),
|
||||
"lines_added" => Some(Ok(humanize_int(cost_info.total_lines_added))),
|
||||
"lines_removed" => Some(Ok(humanize_int(cost_info.total_lines_removed))),
|
||||
_ => None,
|
||||
})
|
||||
.parse(None, Some(context))
|
||||
});
|
||||
|
||||
module.set_segments(match parsed {
|
||||
Ok(segments) => segments,
|
||||
Err(error) => {
|
||||
log::warn!("Error in module `claude_cost`: {error}");
|
||||
return None;
|
||||
}
|
||||
});
|
||||
|
||||
Some(module)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::ModuleRenderer;
|
||||
use nu_ansi_term::Color;
|
||||
|
||||
#[test]
|
||||
fn test_without_data() {
|
||||
let actual = ModuleRenderer::new("claude_cost").collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled() {
|
||||
let data = get_test_claude_data(1.0);
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.config(toml::toml! {
|
||||
[claude_cost]
|
||||
disabled = true
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_by_default_below_threshold() {
|
||||
let data = get_test_claude_data(0.5); // below default threshold of 1.0
|
||||
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"module should be hidden below the $1.00 threshold"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_format_variables() {
|
||||
let data = get_test_claude_data(1.234);
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.config(toml::toml! {
|
||||
[claude_cost]
|
||||
format = "[$cost $duration $api_duration $lines_added $lines_removed]($style) "
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
style = "bold yellow"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!(
|
||||
"{} ",
|
||||
Color::Yellow.bold().paint("1.23 1m0s 45s 1.2k 500")
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_duration_variable() {
|
||||
let data = get_test_claude_data(1.234); // total_api_duration_ms = 45000
|
||||
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.config(toml::toml! {
|
||||
[claude_cost]
|
||||
format = "[$api_duration]($style) "
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
style = "bold yellow"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(format!("{} ", Color::Yellow.bold().paint("45s"))),
|
||||
);
|
||||
}
|
||||
|
||||
fn get_test_claude_data(total_cost_usd: f64) -> crate::context::ClaudeCodeData {
|
||||
crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "claude-3-5-sonnet".to_string(),
|
||||
display_name: "Claude 3.5 Sonnet".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow {
|
||||
context_window_size: 200000,
|
||||
total_input_tokens: 1000,
|
||||
total_output_tokens: 500,
|
||||
used_percentage: 0.0,
|
||||
current_usage: crate::context::CurrentUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
cost: Some(crate::context::CostInfo {
|
||||
total_cost_usd,
|
||||
total_duration_ms: 60000,
|
||||
total_api_duration_ms: 45000,
|
||||
total_lines_added: 1200,
|
||||
total_lines_removed: 500,
|
||||
}),
|
||||
workspace: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_with_data() {
|
||||
let data = get_test_claude_data(1.234);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.config(toml::toml! {
|
||||
[claude_cost]
|
||||
format = "[$symbol(\\$$cost) (\\(+ $lines_added - $lines_removed\\))]($style) "
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
style = "bold yellow"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!(
|
||||
"{} ",
|
||||
Color::Yellow.bold().paint("💰 $1.23 (+ 1.2k - 500)")
|
||||
));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cost_below_threshold() {
|
||||
let data = get_test_claude_data(0.5);
|
||||
|
||||
let actual = ModuleRenderer::new("claude_cost")
|
||||
.config(toml::toml! {
|
||||
[claude_cost]
|
||||
[[claude_cost.display]]
|
||||
threshold = 1.0
|
||||
style = "bold yellow"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_thresholds() {
|
||||
let data_low = get_test_claude_data(0.5);
|
||||
let data_medium = get_test_claude_data(2.5);
|
||||
let data_high = get_test_claude_data(5.5);
|
||||
|
||||
let config = toml::toml! {
|
||||
[claude_cost]
|
||||
format = "[$symbol(\\$$cost)]($style) "
|
||||
[[claude_cost.display]]
|
||||
threshold = 0.0
|
||||
style = "bold green"
|
||||
[[claude_cost.display]]
|
||||
threshold = 2.0
|
||||
style = "bold yellow"
|
||||
[[claude_cost.display]]
|
||||
threshold = 5.0
|
||||
style = "bold red"
|
||||
};
|
||||
|
||||
let actual_low = ModuleRenderer::new("claude_cost")
|
||||
.config(config.clone())
|
||||
.claude_code_data(data_low)
|
||||
.collect();
|
||||
let expected_low = Some(format!("{} ", Color::Green.bold().paint("💰 $0.50")));
|
||||
assert_eq!(actual_low, expected_low);
|
||||
|
||||
let actual_medium = ModuleRenderer::new("claude_cost")
|
||||
.config(config.clone())
|
||||
.claude_code_data(data_medium)
|
||||
.collect();
|
||||
let expected_medium = Some(format!("{} ", Color::Yellow.bold().paint("💰 $2.50")));
|
||||
assert_eq!(actual_medium, expected_medium);
|
||||
|
||||
let actual_high = ModuleRenderer::new("claude_cost")
|
||||
.config(config)
|
||||
.claude_code_data(data_high)
|
||||
.collect();
|
||||
let expected_high = Some(format!("{} ", Color::Red.bold().paint("💰 $5.50")));
|
||||
assert_eq!(actual_high, expected_high);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
use crate::configs::claude_model::ClaudeModelConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("claude_model");
|
||||
let config = ClaudeModelConfig::try_load(module.config);
|
||||
|
||||
if config.disabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read Claude Code data from Context
|
||||
let claude_data = context.claude_code_data.as_ref()?;
|
||||
|
||||
// Check if there's an alias for this model
|
||||
let model_display = config
|
||||
.model_aliases
|
||||
.get(&claude_data.model.id)
|
||||
.or_else(|| {
|
||||
config
|
||||
.model_aliases
|
||||
.get(claude_data.model.display_name.as_str())
|
||||
})
|
||||
.copied()
|
||||
.unwrap_or(&claude_data.model.display_name);
|
||||
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
.map_meta(|variable, _| match variable {
|
||||
"symbol" => Some(config.symbol),
|
||||
_ => None,
|
||||
})
|
||||
.map_style(|variable| match variable {
|
||||
"style" => Some(Ok(config.style)),
|
||||
_ => None,
|
||||
})
|
||||
.map(|variable| match variable {
|
||||
"model" => Some(Ok(model_display)),
|
||||
"model_id" => Some(Ok(claude_data.model.id.as_str())),
|
||||
_ => None,
|
||||
})
|
||||
.parse(None, Some(context))
|
||||
});
|
||||
|
||||
module.set_segments(match parsed {
|
||||
Ok(segments) => segments,
|
||||
Err(error) => {
|
||||
log::warn!("Error in module `claude_model`: {error}");
|
||||
return None;
|
||||
}
|
||||
});
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::ModuleRenderer;
|
||||
use nu_ansi_term::Color;
|
||||
|
||||
#[test]
|
||||
fn test_without_data() {
|
||||
let actual = ModuleRenderer::new("claude_model").collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled() {
|
||||
let data = crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "claude-3-5-sonnet".to_string(),
|
||||
display_name: "Claude 3.5 Sonnet".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow::default(),
|
||||
cost: None,
|
||||
workspace: None,
|
||||
};
|
||||
let actual = ModuleRenderer::new("claude_model")
|
||||
.config(toml::toml! {
|
||||
[claude_model]
|
||||
disabled = true
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_model_alias() {
|
||||
let data = crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(),
|
||||
display_name: "Claude Sonnet 4.5 (AWS Bedrock)".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow::default(),
|
||||
cost: None,
|
||||
workspace: None,
|
||||
};
|
||||
|
||||
let actual = ModuleRenderer::new("claude_model")
|
||||
.config(toml::toml! {
|
||||
[claude_model]
|
||||
symbol = ""
|
||||
[claude_model.model_aliases]
|
||||
"global.anthropic.claude-sonnet-4-5-20250929-v1:0" = "Sonnet 4.5"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!("{} ", Color::Blue.bold().paint("Sonnet 4.5")));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_without_alias_uses_display_name() {
|
||||
let data = crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "claude-3-5-sonnet".to_string(),
|
||||
display_name: "Claude 3.5 Sonnet".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow::default(),
|
||||
cost: None,
|
||||
workspace: None,
|
||||
};
|
||||
|
||||
let actual = ModuleRenderer::new("claude_model")
|
||||
.config(toml::toml! {
|
||||
[claude_model]
|
||||
symbol = ""
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!(
|
||||
"{} ",
|
||||
Color::Blue.bold().paint("Claude 3.5 Sonnet")
|
||||
));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias_by_display_name() {
|
||||
let data = crate::context::ClaudeCodeData {
|
||||
cwd: None,
|
||||
model: crate::context::ModelInfo {
|
||||
id: "some-long-vendor-specific-id".to_string(),
|
||||
display_name: "Claude Sonnet 4.5 (Vendor Proxy)".to_string(),
|
||||
},
|
||||
context_window: crate::context::ContextWindow::default(),
|
||||
cost: None,
|
||||
workspace: None,
|
||||
};
|
||||
|
||||
let actual = ModuleRenderer::new("claude_model")
|
||||
.config(toml::toml! {
|
||||
[claude_model]
|
||||
symbol = ""
|
||||
[claude_model.model_aliases]
|
||||
"Claude Sonnet 4.5 (Vendor Proxy)" = "Sonnet"
|
||||
})
|
||||
.claude_code_data(data)
|
||||
.collect();
|
||||
|
||||
let expected = Some(format!("{} ", Color::Blue.bold().paint("Sonnet")));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ mod bun;
|
||||
mod c;
|
||||
mod cc;
|
||||
mod character;
|
||||
mod claude_context;
|
||||
mod claude_cost;
|
||||
mod claude_model;
|
||||
mod cmake;
|
||||
mod cmd_duration;
|
||||
mod cobol;
|
||||
@@ -129,6 +132,9 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
"bun" => bun::module(context),
|
||||
"c" => c::module(context),
|
||||
"character" => character::module(context),
|
||||
"claude_context" => claude_context::module(context),
|
||||
"claude_cost" => claude_cost::module(context),
|
||||
"claude_model" => claude_model::module(context),
|
||||
"cmake" => cmake::module(context),
|
||||
"cmd_duration" => cmd_duration::module(context),
|
||||
"cobol" => cobol::module(context),
|
||||
@@ -263,6 +269,9 @@ pub fn description(module: &str) -> &'static str {
|
||||
"character" => {
|
||||
"A character (usually an arrow) beside where the text is entered in your terminal"
|
||||
}
|
||||
"claude_context" => "Context window usage for Claude Code session",
|
||||
"claude_cost" => "Cost info for Claude Code session",
|
||||
"claude_model" => "AI model name for Claude Code session",
|
||||
"cmake" => "The currently installed version of CMake",
|
||||
"cmd_duration" => "How long the last command took to execute",
|
||||
"cobol" => "The currently installed version of COBOL/GNUCOBOL",
|
||||
|
||||
+1
-2
@@ -497,8 +497,7 @@ impl RustupSettings {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::context::{Properties, Shell, Target};
|
||||
use crate::context_env::Env;
|
||||
use crate::context::{Env, Properties, Shell, Target};
|
||||
use std::io;
|
||||
use std::process::{ExitStatus, Output};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
+33
-1
@@ -73,6 +73,20 @@ pub fn prompt(args: Properties, target: Target) {
|
||||
let context = Context::new(args, target);
|
||||
let stdout = io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
|
||||
write!(handle, "{}", get_prompt(&context)).unwrap();
|
||||
}
|
||||
|
||||
pub fn prompt_with_claude_code(args: Properties, target: Target) {
|
||||
let claude_data = serde_json::from_reader(io::stdin())
|
||||
.inspect_err(|e| log::error!("Failed to read Claude Code JSON from stdin: {e}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut context = Context::new(args, target).with_claude_code_data(claude_data);
|
||||
context.shell = Shell::Unknown;
|
||||
let stdout = io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
|
||||
write!(handle, "{}", get_prompt(&context)).unwrap();
|
||||
}
|
||||
|
||||
@@ -446,7 +460,11 @@ fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>,
|
||||
let (left_format_str, right_format_str): (&str, &str) = match context.target {
|
||||
Target::Main | Target::Right => (&config.format, &config.right_format),
|
||||
Target::Profile(ref name) => {
|
||||
if let Some(lf) = config.profiles.get(name) {
|
||||
if let Some(lf) = config
|
||||
.user_profiles
|
||||
.get(name)
|
||||
.or_else(|| config.internal_profiles.get(name))
|
||||
{
|
||||
(lf, "")
|
||||
} else {
|
||||
log::error!("Profile {name:?} not found");
|
||||
@@ -812,4 +830,18 @@ mod test {
|
||||
assert_eq!(expected, actual);
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefer_user_profile() {
|
||||
let mut context = default_context().set_config(toml::toml! {
|
||||
add_newline = false
|
||||
[profiles]
|
||||
claude-code = "user profile"
|
||||
});
|
||||
context.target = Target::Profile("claude-code".to_string());
|
||||
|
||||
let expected = "user profile";
|
||||
let actual = get_prompt(&context);
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,5 +1,4 @@
|
||||
use crate::context::{Context, Properties, Shell, Target};
|
||||
use crate::context_env::Env;
|
||||
use crate::context::{Context, Env, Properties, Shell, Target};
|
||||
use crate::logger::StarshipLogger;
|
||||
use crate::{
|
||||
config::StarshipConfig,
|
||||
@@ -148,6 +147,11 @@ impl<'a> ModuleRenderer<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn claude_code_data(mut self, data: crate::context::ClaudeCodeData) -> Self {
|
||||
self.context.claude_code_data = Some(Box::new(data));
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
pub fn battery_info_provider(
|
||||
mut self,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
pub mod env;
|
||||
pub mod serde;
|
||||
pub mod statusline;
|
||||
|
||||
use process_control::{ChildExt, Control};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Debug;
|
||||
@@ -738,6 +742,34 @@ pub fn render_time(raw_millis: u128, show_millis: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an integer into a human-readable string using SI prefixes (k, M, G, T)
|
||||
pub fn humanize_int(n: u64) -> String {
|
||||
if n < 1000 {
|
||||
return n.to_string();
|
||||
}
|
||||
|
||||
let n = n as f64;
|
||||
let units = ["k", "M", "G", "T", "P", "E"];
|
||||
let mut unit_idx = 0;
|
||||
let mut val = n / 1000.0;
|
||||
|
||||
while val >= 1000.0 && unit_idx < units.len() - 1 {
|
||||
val /= 1000.0;
|
||||
unit_idx += 1;
|
||||
}
|
||||
|
||||
if val < 10.0 {
|
||||
let s = format!("{:.1}{}", val, units[unit_idx]);
|
||||
if s.contains(".0") {
|
||||
s.replace(".0", "")
|
||||
} else {
|
||||
s
|
||||
}
|
||||
} else {
|
||||
format!("{:.0}{}", val, units[unit_idx])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir()
|
||||
}
|
||||
@@ -822,6 +854,18 @@ mod tests {
|
||||
assert_eq!(render_time(86_400_000_u128, false), "1d0h0m0s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_humanize_int() {
|
||||
assert_eq!(humanize_int(0), "0");
|
||||
assert_eq!(humanize_int(999), "999");
|
||||
assert_eq!(humanize_int(1000), "1k");
|
||||
assert_eq!(humanize_int(1200), "1.2k");
|
||||
assert_eq!(humanize_int(10000), "10k");
|
||||
assert_eq!(humanize_int(100000), "100k");
|
||||
assert_eq!(humanize_int(1000000), "1M");
|
||||
assert_eq!(humanize_int(1500000), "1.5M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_mocked_command() {
|
||||
let result = exec_cmd(
|
||||
@@ -0,0 +1,58 @@
|
||||
/// Claude Code session data structures for statusline integration
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Claude Code session data
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ClaudeCodeData {
|
||||
pub cwd: Option<String>,
|
||||
pub model: ModelInfo,
|
||||
pub context_window: ContextWindow,
|
||||
pub cost: Option<CostInfo>,
|
||||
pub workspace: Option<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ModelInfo {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ContextWindow {
|
||||
pub context_window_size: u64,
|
||||
pub total_input_tokens: u64,
|
||||
pub total_output_tokens: u64,
|
||||
/// Pre-computed usage percentage (0–100) provided by Claude Code
|
||||
pub used_percentage: f32,
|
||||
/// Usage of the most recent API call
|
||||
pub current_usage: CurrentUsage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct CurrentUsage {
|
||||
pub input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub cache_creation_input_tokens: u64,
|
||||
pub cache_read_input_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct CostInfo {
|
||||
pub total_cost_usd: f64,
|
||||
pub total_duration_ms: u64,
|
||||
pub total_api_duration_ms: u64,
|
||||
pub total_lines_added: u64,
|
||||
pub total_lines_removed: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Workspace {
|
||||
pub current_dir: String,
|
||||
pub project_dir: String,
|
||||
}
|
||||
Reference in New Issue
Block a user