use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use unicode_width::UnicodeWidthStr;
use crate::domain::{ContextUsageSnapshot, format_compact_count};
use crate::models::{ReasoningLevel, TokenUsageSource};
use crate::render::theme::Theme;
use crate::runtime::SafetyMode;
pub struct StatusWidget<'a> {
pub theme: &'a Theme,
pub working_dir: &'a str,
pub hostname: &'a str,
pub username: &'a str,
pub version: &'a str,
pub context_usage: Option<&'a ContextUsageSnapshot>,
pub model_name: &'a str,
pub reasoning_level: ReasoningLevel,
pub requested_level: Option<ReasoningLevel>,
pub safety_mode: SafetyMode,
pub plan_active: bool,
}
impl<'a> Widget for StatusWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let directory_text = format!("{}@{}:{}", self.username, self.hostname, self.working_dir);
let token_text = format_token_status(self.context_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!("{}@{}", self.username, self.hostname),
Style::new().fg(self.theme.colors.success.to_color()).bold(),
),
Span::styled(
":",
Style::new().fg(self.theme.colors.text_primary.to_color()),
),
Span::styled(
self.working_dir,
Style::new().fg(self.theme.colors.info.to_color()),
),
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 safety_segment = if self.plan_active {
format!(
"plan mode on (alt+p to toggle) - restores: {}",
self.safety_mode.as_str()
)
} else {
format!("safety: {}", self.safety_mode.as_str())
};
let left_text = status_line2_left(self.version, &safety_segment, &reasoning_text);
let model_display = self.model_name;
let left_content_width = left_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(
left_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>) -> String {
match context_usage {
Some(snapshot) => format_context_snapshot(snapshot),
None => "context: n/a".to_string(),
}
}
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 status_line2_left(version: &str, safety_segment: &str, reasoning_text: &str) -> String {
format!("mermaid v{version} · {safety_segment} · {reasoning_text}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_line2_left_shows_version_safety_and_reasoning() {
let s = status_line2_left(env!("CARGO_PKG_VERSION"), "safety: ask", "reasoning: high");
assert!(
s.contains(&format!("mermaid v{}", env!("CARGO_PKG_VERSION"))),
"status line must show the app version — got {s:?}"
);
assert!(s.contains("safety: ask"));
assert!(s.contains("reasoning: high"));
}
#[test]
fn status_line2_left_carries_plan_badge_segment() {
let s = status_line2_left(
"0.0.0",
"plan mode on (alt+p to toggle) - restores: auto",
"reasoning: high",
);
assert!(s.contains("plan mode on (alt+p to toggle) - restores: auto"));
assert!(!s.contains("safety:"));
}
#[test]
fn token_status_shows_context_gauge_only() {
let context = ContextUsageSnapshot::from_usage(
&crate::models::TokenUsage::provider(12_000, 456),
Some(128_000),
);
assert_eq!(
format_token_status(Some(&context)),
"context: 12.4k / 128k (9%)"
);
}
#[test]
fn token_status_handles_missing_context() {
assert_eq!(format_token_status(None), "context: n/a");
}
#[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)),
"context: ~100 / unknown"
);
}
}