use crate::config::CshipConfig;
use crate::context::Context;
const DEFAULT_EXCEEDS_SYMBOL: &str = ">200k";
fn is_disabled(cfg: &CshipConfig) -> bool {
cfg.context_window
.as_ref()
.and_then(|c| c.disabled)
.unwrap_or(false)
}
fn apply_cw_style(content: &str, cfg: &CshipConfig) -> String {
crate::ansi::apply_style(
content,
cfg.context_window.as_ref().and_then(|c| c.style.as_deref()),
)
}
pub fn render_used_percentage(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.used_percentage.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.used_percentage)
{
Some(v) => v,
None => {
tracing::warn!("cship.context_window.used_percentage: value absent from context");
return None;
}
};
let val_str = format!("{:.0}", val);
crate::format::render_styled_value(
&val_str,
Some(val),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_remaining_percentage(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.remaining_percentage.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.remaining_percentage)
{
Some(v) => v,
None => {
tracing::warn!("cship.context_window.remaining_percentage: value absent from context");
return None;
}
};
let val_str = format!("{:.0}", val);
crate::format::render_styled_value(
&val_str,
Some(val),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
#[allow(clippy::question_mark)]
pub fn render_used_tokens(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.used_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let cw = match ctx.context_window.as_ref() {
Some(cw) => cw,
None => {
tracing::warn!("cship.context_window.used_tokens: context_window absent from context");
return None;
}
};
let cu = match cw.current_usage.as_ref() {
Some(cu) => cu,
None => {
tracing::warn!(
"cship.context_window.used_tokens: current_usage absent from context_window"
);
return None;
}
};
let used = cu.input_tokens.unwrap_or(0)
+ cu.cache_creation_input_tokens.unwrap_or(0)
+ cu.cache_read_input_tokens.unwrap_or(0);
let size = match cw.context_window_size {
Some(v) => v,
None => {
tracing::warn!(
"cship.context_window.used_tokens: context_window_size absent from context"
);
return None;
}
};
let pct = cw.used_percentage.unwrap_or(0.0);
let val_str = format!(
"{:.0}%({}k/{}k)",
pct,
(used + 500) / 1000,
(size + 500) / 1000
);
crate::format::render_styled_value(
&val_str,
Some(pct),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_size(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.size.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.context_window_size)
{
Some(v) => v,
None => {
tracing::warn!("cship.context_window.size: context_window_size absent from context");
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_total_input_tokens(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.total_input_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.total_input_tokens)
{
Some(v) => v,
None => {
tracing::warn!("cship.context_window.total_input_tokens: value absent from context");
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_total_output_tokens(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.total_output_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.total_output_tokens)
{
Some(v) => v,
None => {
tracing::warn!("cship.context_window.total_output_tokens: value absent from context");
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_exceeds_200k(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let exceeds = ctx.exceeds_200k_tokens.unwrap_or(false);
if !exceeds {
return None; }
let cw_cfg = cfg.context_window.as_ref();
let symbol_str = cw_cfg
.and_then(|c| c.symbol.as_deref())
.unwrap_or(DEFAULT_EXCEEDS_SYMBOL);
if let Some(fmt) = cw_cfg.and_then(|c| c.format.as_deref()) {
let style = cw_cfg.and_then(|c| c.style.as_deref());
return crate::format::apply_module_format(fmt, Some(symbol_str), Some(symbol_str), style);
}
Some(apply_cw_style(symbol_str, cfg))
}
pub fn render_current_usage_input_tokens(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.current_usage_input_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.current_usage.as_ref())
.and_then(|cu| cu.input_tokens)
{
Some(v) => v,
None => {
tracing::warn!(
"cship.context_window.current_usage.input_tokens: value absent from context"
);
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_current_usage_output_tokens(ctx: &Context, cfg: &CshipConfig) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.current_usage_output_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.current_usage.as_ref())
.and_then(|cu| cu.output_tokens)
{
Some(v) => v,
None => {
tracing::warn!(
"cship.context_window.current_usage.output_tokens: value absent from context"
);
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_current_usage_cache_creation_input_tokens(
ctx: &Context,
cfg: &CshipConfig,
) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.current_usage_cache_creation_input_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.current_usage.as_ref())
.and_then(|cu| cu.cache_creation_input_tokens)
{
Some(v) => v,
None => {
tracing::warn!(
"cship.context_window.current_usage.cache_creation_input_tokens: value absent from context"
);
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
pub fn render_current_usage_cache_read_input_tokens(
ctx: &Context,
cfg: &CshipConfig,
) -> Option<String> {
if is_disabled(cfg) {
return None;
}
let cw_cfg = cfg.context_window.as_ref();
let sub_cfg = cw_cfg.and_then(|c| c.current_usage_cache_read_input_tokens.as_ref());
if sub_cfg.and_then(|c| c.disabled).unwrap_or(false) {
return None;
}
let val = match ctx
.context_window
.as_ref()
.and_then(|cw| cw.current_usage.as_ref())
.and_then(|cu| cu.cache_read_input_tokens)
{
Some(v) => v,
None => {
tracing::warn!(
"cship.context_window.current_usage.cache_read_input_tokens: value absent from context"
);
return None;
}
};
let val_str = val.to_string();
crate::format::render_styled_value(
&val_str,
Some(val as f64),
sub_cfg,
cw_cfg.map(|c| c as &dyn crate::config::HasThresholdStyle),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ContextWindowConfig, ContextWindowSubfieldConfig, CshipConfig};
use crate::context::{Context, ContextWindow, CurrentUsage};
fn ctx_full() -> Context {
Context {
exceeds_200k_tokens: Some(false),
context_window: Some(ContextWindow {
used_percentage: Some(35.0),
remaining_percentage: Some(65.0),
context_window_size: Some(200000),
total_input_tokens: Some(15234),
total_output_tokens: Some(4521),
current_usage: Some(CurrentUsage {
input_tokens: Some(8500),
output_tokens: Some(1200),
cache_creation_input_tokens: Some(5000),
cache_read_input_tokens: Some(2000),
}),
}),
..Default::default()
}
}
#[test]
fn test_used_percentage_renders_as_integer_no_percent_sign() {
let ctx = ctx_full();
assert_eq!(
render_used_percentage(&ctx, &CshipConfig::default()),
Some("35".to_string())
);
}
#[test]
fn test_remaining_percentage_renders_as_integer() {
let ctx = ctx_full();
assert_eq!(
render_remaining_percentage(&ctx, &CshipConfig::default()),
Some("65".to_string())
);
}
#[test]
fn test_size_reads_context_window_size_field() {
let ctx = ctx_full();
assert_eq!(
render_size(&ctx, &CshipConfig::default()),
Some("200000".to_string())
);
}
#[test]
fn test_total_input_tokens() {
let ctx = ctx_full();
assert_eq!(
render_total_input_tokens(&ctx, &CshipConfig::default()),
Some("15234".to_string())
);
}
#[test]
fn test_total_output_tokens() {
let ctx = ctx_full();
assert_eq!(
render_total_output_tokens(&ctx, &CshipConfig::default()),
Some("4521".to_string())
);
}
#[test]
fn test_exceeds_200k_false_returns_none_no_warn() {
let ctx = ctx_full(); assert_eq!(render_exceeds_200k(&ctx, &CshipConfig::default()), None);
}
#[test]
fn test_exceeds_200k_absent_treated_as_false() {
let ctx = Context::default(); assert_eq!(render_exceeds_200k(&ctx, &CshipConfig::default()), None);
}
#[test]
fn test_exceeds_200k_true_renders_default_symbol() {
let ctx = Context {
exceeds_200k_tokens: Some(true),
..Default::default()
};
let result = render_exceeds_200k(&ctx, &CshipConfig::default());
assert_eq!(result, Some(">200k".to_string()));
}
#[test]
fn test_current_usage_input_tokens() {
let ctx = ctx_full();
assert_eq!(
render_current_usage_input_tokens(&ctx, &CshipConfig::default()),
Some("8500".to_string())
);
}
#[test]
fn test_current_usage_output_tokens() {
let ctx = ctx_full();
assert_eq!(
render_current_usage_output_tokens(&ctx, &CshipConfig::default()),
Some("1200".to_string())
);
}
#[test]
fn test_current_usage_cache_creation_tokens() {
let ctx = ctx_full();
assert_eq!(
render_current_usage_cache_creation_input_tokens(&ctx, &CshipConfig::default()),
Some("5000".to_string())
);
}
#[test]
fn test_current_usage_cache_read_tokens() {
let ctx = ctx_full();
assert_eq!(
render_current_usage_cache_read_input_tokens(&ctx, &CshipConfig::default()),
Some("2000".to_string())
);
}
#[test]
fn test_exceeds_200k_true_renders_custom_symbol() {
let ctx = Context {
exceeds_200k_tokens: Some(true),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
symbol: Some("⚠".to_string()),
..Default::default()
}),
..Default::default()
};
let result = render_exceeds_200k(&ctx, &cfg);
assert_eq!(result, Some("⚠".to_string()));
}
#[test]
fn test_disabled_flag_suppresses_all_renders() {
let ctx = ctx_full();
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
disabled: Some(true),
..Default::default()
}),
..Default::default()
};
assert_eq!(render_used_percentage(&ctx, &cfg), None);
assert_eq!(render_size(&ctx, &cfg), None);
assert_eq!(render_exceeds_200k(&ctx, &cfg), None);
}
#[test]
fn test_absent_context_window_returns_none() {
let ctx = Context::default(); assert_eq!(render_used_percentage(&ctx, &CshipConfig::default()), None);
assert_eq!(render_size(&ctx, &CshipConfig::default()), None);
assert_eq!(
render_total_input_tokens(&ctx, &CshipConfig::default()),
None
);
assert_eq!(render_used_tokens(&ctx, &CshipConfig::default()), None);
}
#[test]
fn test_subfield_used_percentage_above_warn_applies_warn_style() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(85.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(95.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for warn: {result:?}"
);
assert!(result.contains("85"), "expected value: {result:?}");
assert!(
result.contains("\x1b[33m"),
"expected yellow ANSI code for warn: {result:?}"
);
}
#[test]
fn test_subfield_used_percentage_above_critical_applies_critical_style() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(97.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(95.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for critical: {result:?}"
);
assert!(result.contains("97"), "expected value: {result:?}");
assert!(
result.contains("\x1b[1;31m"),
"expected bold+red ANSI code for critical: {result:?}"
);
}
#[test]
fn test_subfield_used_percentage_below_warn_uses_base_style() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(50.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
!result.contains('\x1b'),
"no ANSI expected below warn: {result:?}"
);
assert!(result.contains("50"), "expected value: {result:?}");
}
#[test]
fn test_subfield_parent_threshold_used_as_fallback() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(85.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected parent threshold fallback ANSI: {result:?}"
);
}
#[test]
fn test_subfield_disabled_flag_suppresses_only_that_subfield() {
let ctx = ctx_full(); let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
disabled: Some(true),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert_eq!(render_used_percentage(&ctx, &cfg), None);
assert_eq!(
render_remaining_percentage(&ctx, &cfg),
Some("65".to_string())
);
}
#[test]
fn test_subfield_format_with_warn_threshold_uses_warn_style() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(85.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
format: Some("[$value%]($style)".to_string()),
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(95.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI in format path: {result:?}"
);
assert!(
result.contains("85"),
"expected value in format: {result:?}"
);
}
#[test]
fn test_subfield_size_above_warn_applies_warn_style() {
let ctx = Context {
context_window: Some(ContextWindow {
context_window_size: Some(200000),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
size: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(150000.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_size(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for size warn: {result:?}"
);
assert!(result.contains("200000"), "expected value: {result:?}");
}
#[test]
fn test_subfield_total_input_tokens_above_warn_applies_warn_style() {
let ctx = Context {
context_window: Some(ContextWindow {
total_input_tokens: Some(160000),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
total_input_tokens: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(150000.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_total_input_tokens(&ctx, &cfg).unwrap();
assert!(result.contains('\x1b'), "expected ANSI: {result:?}");
}
#[test]
fn test_subfield_style_overrides_parent_style() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(50.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
style: Some("green".to_string()),
used_percentage: Some(ContextWindowSubfieldConfig {
style: Some("blue".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains("\x1b[34m"),
"expected blue (sub-field) style, not green (parent): {result:?}"
);
assert!(
!result.contains("\x1b[32m"),
"parent green style should NOT appear: {result:?}"
);
}
#[test]
fn test_remaining_percentage_invert_threshold_fires_when_low() {
let ctx = Context {
context_window: Some(ContextWindow {
remaining_percentage: Some(15.0), ..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
remaining_percentage: Some(ContextWindowSubfieldConfig {
invert_threshold: Some(true),
warn_threshold: Some(20.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(10.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_remaining_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for low remaining: {result:?}"
);
assert!(result.contains("15"), "expected value: {result:?}");
}
#[test]
fn test_remaining_percentage_invert_threshold_no_fire_when_high() {
let ctx = Context {
context_window: Some(ContextWindow {
remaining_percentage: Some(85.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
remaining_percentage: Some(ContextWindowSubfieldConfig {
invert_threshold: Some(true),
warn_threshold: Some(20.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_remaining_percentage(&ctx, &cfg).unwrap();
assert!(
!result.contains('\x1b'),
"expected no ANSI for high remaining: {result:?}"
);
}
#[test]
fn test_remaining_percentage_invert_critical_fires_below_critical_threshold() {
let ctx = Context {
context_window: Some(ContextWindow {
remaining_percentage: Some(5.0), ..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
remaining_percentage: Some(ContextWindowSubfieldConfig {
invert_threshold: Some(true),
warn_threshold: Some(20.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(10.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_remaining_percentage(&ctx, &cfg).unwrap();
assert!(
result.contains("\x1b[1;31m"),
"expected bold red for critically low remaining: {result:?}"
);
}
#[test]
fn test_remaining_percentage_invert_does_not_inherit_parent_threshold() {
let ctx = Context {
context_window: Some(ContextWindow {
remaining_percentage: Some(85.0),
..Default::default()
}),
..Default::default()
};
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
warn_threshold: Some(80.0), warn_style: Some("yellow".to_string()),
remaining_percentage: Some(ContextWindowSubfieldConfig {
invert_threshold: Some(true),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_remaining_percentage(&ctx, &cfg).unwrap();
assert!(
!result.contains('\x1b'),
"parent threshold must not fire for remaining_percentage with invert_threshold: {result:?}"
);
}
#[test]
fn test_subfield_no_threshold_unchanged() {
let ctx = ctx_full();
let result_default = render_used_percentage(&ctx, &CshipConfig::default());
let cfg_no_thresh = CshipConfig {
context_window: Some(ContextWindowConfig {
used_percentage: Some(ContextWindowSubfieldConfig {
..Default::default() }),
..Default::default()
}),
..Default::default()
};
let result_explicit = render_used_percentage(&ctx, &cfg_no_thresh);
assert_eq!(result_default, result_explicit);
}
fn ctx_used_tokens(pct: f64) -> Context {
Context {
context_window: Some(ContextWindow {
used_percentage: Some(pct),
remaining_percentage: Some(100.0 - pct),
context_window_size: Some(200000),
current_usage: Some(CurrentUsage {
input_tokens: Some(8000),
cache_creation_input_tokens: Some(5000),
cache_read_input_tokens: Some(2000),
output_tokens: Some(1200),
}),
..Default::default()
}),
..Default::default()
}
}
#[test]
fn test_used_tokens_warn_threshold_applied() {
let ctx = ctx_used_tokens(85.0);
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_tokens: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(95.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_tokens(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for warn: {result:?}"
);
assert!(
result.contains("85"),
"expected percentage value in output: {result:?}"
);
assert!(
result.contains("\x1b[33m"),
"expected yellow ANSI code for warn: {result:?}"
);
}
#[test]
fn test_used_tokens_critical_threshold_applied() {
let ctx = ctx_used_tokens(97.0);
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
used_tokens: Some(ContextWindowSubfieldConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
critical_threshold: Some(95.0),
critical_style: Some("bold red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_tokens(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected ANSI for critical: {result:?}"
);
assert!(
result.contains("97"),
"expected percentage value in output: {result:?}"
);
assert!(
result.contains("\x1b[1;31m"),
"expected bold+red ANSI code for critical: {result:?}"
);
}
#[test]
fn test_used_tokens_no_threshold_config_no_regression() {
let ctx = ctx_used_tokens(35.0);
let result_default = render_used_tokens(&ctx, &CshipConfig::default()).unwrap();
let cfg_no_thresh = CshipConfig {
context_window: Some(ContextWindowConfig {
used_tokens: Some(ContextWindowSubfieldConfig {
..Default::default() }),
..Default::default()
}),
..Default::default()
};
let result_explicit = render_used_tokens(&ctx, &cfg_no_thresh).unwrap();
assert_eq!(
result_default, result_explicit,
"no-threshold config must not change output"
);
assert!(
!result_default.contains('\x1b'),
"no ANSI expected without threshold config: {result_default:?}"
);
}
#[test]
fn test_used_tokens_threshold_parent_fallback() {
let ctx = ctx_used_tokens(85.0);
let cfg = CshipConfig {
context_window: Some(ContextWindowConfig {
warn_threshold: Some(80.0),
warn_style: Some("yellow".to_string()),
..Default::default()
}),
..Default::default()
};
let result = render_used_tokens(&ctx, &cfg).unwrap();
assert!(
result.contains('\x1b'),
"expected parent threshold fallback ANSI: {result:?}"
);
assert!(
result.contains("\x1b[33m"),
"expected yellow from parent fallback: {result:?}"
);
}
#[test]
fn test_used_tokens_absent_current_usage_returns_none() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(50.0),
context_window_size: Some(200000),
current_usage: None,
..Default::default()
}),
..Default::default()
};
assert_eq!(render_used_tokens(&ctx, &CshipConfig::default()), None);
}
#[test]
fn test_used_tokens_absent_context_window_size_returns_none() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(50.0),
context_window_size: None,
current_usage: Some(CurrentUsage {
input_tokens: Some(8000),
cache_creation_input_tokens: Some(0),
cache_read_input_tokens: Some(0),
output_tokens: Some(0),
}),
..Default::default()
}),
..Default::default()
};
assert_eq!(render_used_tokens(&ctx, &CshipConfig::default()), None);
}
#[test]
fn test_used_tokens_rounding_500_shows_1k() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(0.25),
context_window_size: Some(200000),
current_usage: Some(CurrentUsage {
input_tokens: Some(500),
cache_creation_input_tokens: Some(0),
cache_read_input_tokens: Some(0),
output_tokens: Some(0),
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_tokens(&ctx, &CshipConfig::default()).unwrap();
assert!(
result.contains("1k/200k"),
"expected '1k/200k' in: {result:?}"
);
}
#[test]
fn test_used_tokens_rounding_499_shows_0k() {
let ctx = Context {
context_window: Some(ContextWindow {
used_percentage: Some(0.25),
context_window_size: Some(200000),
current_usage: Some(CurrentUsage {
input_tokens: Some(499),
cache_creation_input_tokens: Some(0),
cache_read_input_tokens: Some(0),
output_tokens: Some(0),
}),
..Default::default()
}),
..Default::default()
};
let result = render_used_tokens(&ctx, &CshipConfig::default()).unwrap();
assert!(
result.contains("0k/200k"),
"expected '0k/200k' in: {result:?}"
);
}
}