use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use unicode_width::UnicodeWidthStr;
use crate::domain::{ContextUsageSnapshot, TokenUsageTotals};
use crate::models::{ReasoningLevel, TokenUsageSource};
use crate::render::theme::Theme;
pub struct StatusWidget<'a> {
pub theme: &'a Theme,
pub working_dir: &'a str,
pub context_usage: Option<&'a ContextUsageSnapshot>,
pub last_usage: Option<TokenUsageTotals>,
pub session_usage: TokenUsageTotals,
pub model_name: &'a str,
pub reasoning_level: ReasoningLevel,
pub requested_level: Option<ReasoningLevel>,
}
impl<'a> Widget for StatusWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let hostname = std::env::var("HOSTNAME")
.or_else(|_| std::env::var("HOST"))
.unwrap_or_else(|_| "localhost".to_string());
let username = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "user".to_string());
let directory_text = format!("{}@{}:{}", username, hostname, self.working_dir);
let token_text =
format_token_status(self.context_usage, self.last_usage, self.session_usage);
let available_width = area.width as usize;
let directory_width = directory_text.width();
let token_width = token_text.width();
let padding_width = if available_width > directory_width + token_width + 1 {
available_width - directory_width - token_width
} else {
1
};
let line1_spans = vec![
Span::styled(
format!("{}@{}", username, hostname),
Style::new().fg(ratatui::style::Color::Green).bold(),
),
Span::styled(
":",
Style::new().fg(self.theme.colors.text_primary.to_color()),
),
Span::styled(
self.working_dir,
Style::new().fg(ratatui::style::Color::Cyan),
),
Span::raw(" ".repeat(padding_width)),
Span::styled(
token_text,
Style::new().fg(self.theme.colors.text_disabled.to_color()),
),
];
let reasoning_text = match self.requested_level {
Some(requested) => format!(
"reasoning: {} ({} requested)",
self.reasoning_level.as_str(),
requested.as_str()
),
None => format!("reasoning: {}", self.reasoning_level.as_str()),
};
let model_display = self.model_name;
let left_content_width = reasoning_text.width();
let right_content_width = model_display.width();
let padding_width_line2 = if available_width > left_content_width + right_content_width {
available_width - left_content_width - right_content_width
} else {
1
};
let line2_spans = vec![
Span::styled(
reasoning_text,
Style::new().fg(self.theme.colors.text_disabled.to_color()),
),
Span::raw(" ".repeat(padding_width_line2)),
Span::styled(
model_display,
Style::new().fg(self.theme.colors.text_disabled.to_color()),
),
];
let line1 = Line::from(line1_spans);
let line2 = Line::from(line2_spans);
let status_bar = Paragraph::new(vec![line1, line2]);
status_bar.render(area, buf);
}
}
pub(crate) fn format_token_status(
context_usage: Option<&ContextUsageSnapshot>,
last_usage: Option<TokenUsageTotals>,
session_usage: TokenUsageTotals,
) -> String {
let session = format_compact_count(session_usage.total_tokens);
let context = match context_usage {
Some(snapshot) => format_context_snapshot(snapshot),
None => "context: n/a".to_string(),
};
match last_usage {
Some(usage) => format!(
"{} | last api: {} | session: {}",
context,
format_compact_count(usage.total_tokens),
session
),
None => format!("{} | session: {}", context, session),
}
}
fn format_context_snapshot(snapshot: &ContextUsageSnapshot) -> String {
let used = format_compact_count(snapshot.used_tokens);
let source = match snapshot.source {
TokenUsageSource::Provider => "",
TokenUsageSource::Estimate => "~",
};
match (snapshot.max_tokens, snapshot.used_percent) {
(Some(max), Some(percent)) => format!(
"context: {}{} / {} ({}%)",
source,
used,
format_compact_count(max),
percent
),
_ => format!("context: {}{} / unknown", source, used),
}
}
fn format_compact_count(value: usize) -> String {
if value >= 1_000_000 {
format_scaled(value, 1_000_000, "m")
} else if value >= 10_000 {
format_scaled(value, 1_000, "k")
} else {
value.to_string()
}
}
fn format_scaled(value: usize, divisor: usize, suffix: &str) -> String {
let whole = value / divisor;
let decimal = ((value % divisor) * 10) / divisor;
if decimal == 0 {
format!("{}{}", whole, suffix)
} else {
format!("{}.{}{}", whole, decimal, suffix)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_status_labels_last_and_session_usage() {
let context = ContextUsageSnapshot::from_usage(
&crate::models::TokenUsage::provider(12_000, 456, 12_456),
Some(128_000),
);
assert_eq!(
format_token_status(
Some(&context),
Some(TokenUsageTotals {
prompt_tokens: 12_000,
completion_tokens: 456,
total_tokens: 12_456,
..TokenUsageTotals::default()
}),
TokenUsageTotals {
prompt_tokens: 500_000,
completion_tokens: 73_443,
total_tokens: 573_443,
..TokenUsageTotals::default()
},
),
"context: 12.4k / 128k (9%) | last api: 12.4k | session: 573.4k"
);
}
#[test]
fn token_status_handles_missing_last_usage() {
assert_eq!(
format_token_status(
None,
None,
TokenUsageTotals {
prompt_tokens: 900,
completion_tokens: 50,
total_tokens: 950,
..TokenUsageTotals::default()
},
),
"context: n/a | session: 950"
);
}
#[test]
fn token_status_marks_estimates() {
let context = ContextUsageSnapshot::from_estimate(
crate::domain::PromptTokenBreakdown {
system_tokens: 10,
instructions_tokens: 0,
message_tokens: 20,
tool_schema_tokens: 70,
image_count: 0,
message_count: 1,
tool_count: 4,
},
None,
);
assert_eq!(
format_token_status(Some(&context), None, TokenUsageTotals::default()),
"context: ~100 / unknown | session: 0"
);
}
}