feat: add statusline subcommand for Claude Code integration (#7234)

This commit is contained in:
David Knaack
2026-04-03 18:02:39 +02:00
committed by GitHub
parent 9a48087137
commit b8ee82167c
24 changed files with 1747 additions and 16 deletions
+211 -1
View File
@@ -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": {
+310
View File
@@ -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
View File
@@ -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,
+79
View File
@@ -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,
}
}
}
+45
View File
@@ -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,
}
}
}
+29
View File
@@ -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
View File
@@ -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>,
+14 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-2
View File
@@ -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
View File
@@ -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(),
}
+3
View File
@@ -15,6 +15,9 @@ pub const ALL_MODULES: &[&str] = &[
"bun",
"c",
"character",
"claude_context",
"claude_cost",
"claude_model",
"cmake",
"cmd_duration",
"cobol",
+419
View File
@@ -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);
}
}
+262
View File
@@ -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);
}
}
+171
View File
@@ -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);
}
}
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+44
View File
@@ -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(
+58
View File
@@ -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 (0100) 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,
}