mermaid_cli/render/widgets/
status.rs1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::Style,
5 text::{Line, Span},
6 widgets::{Paragraph, Widget},
7};
8use unicode_width::UnicodeWidthStr;
9
10use crate::domain::{ContextUsageSnapshot, TokenUsageTotals};
11use crate::models::{ReasoningLevel, TokenUsageSource};
12use crate::render::theme::Theme;
13
14pub struct StatusWidget<'a> {
16 pub theme: &'a Theme,
17 pub working_dir: &'a str,
18 pub context_usage: Option<&'a ContextUsageSnapshot>,
19 pub last_usage: Option<TokenUsageTotals>,
20 pub session_usage: TokenUsageTotals,
21 pub model_name: &'a str,
22 pub reasoning_level: ReasoningLevel,
26 pub requested_level: Option<ReasoningLevel>,
30}
31
32impl<'a> Widget for StatusWidget<'a> {
33 fn render(self, area: Rect, buf: &mut Buffer) {
34 let hostname = std::env::var("HOSTNAME")
36 .or_else(|_| std::env::var("HOST"))
37 .unwrap_or_else(|_| "localhost".to_string());
38 let username = std::env::var("USER")
39 .or_else(|_| std::env::var("USERNAME"))
40 .unwrap_or_else(|_| "user".to_string());
41
42 let directory_text = format!("{}@{}:{}", username, hostname, self.working_dir);
44 let token_text =
45 format_token_status(self.context_usage, self.last_usage, self.session_usage);
46
47 let available_width = area.width as usize;
51 let directory_width = directory_text.width();
52 let token_width = token_text.width();
53 let padding_width = if available_width > directory_width + token_width + 1 {
54 available_width - directory_width - token_width
55 } else {
56 1
57 };
58
59 let line1_spans = vec![
60 Span::styled(
62 format!("{}@{}", username, hostname),
63 Style::new().fg(ratatui::style::Color::Green).bold(),
64 ),
65 Span::styled(
66 ":",
67 Style::new().fg(self.theme.colors.text_primary.to_color()),
68 ),
69 Span::styled(
70 self.working_dir,
71 Style::new().fg(ratatui::style::Color::Cyan),
72 ),
73 Span::raw(" ".repeat(padding_width)),
75 Span::styled(
77 token_text,
78 Style::new().fg(self.theme.colors.text_disabled.to_color()),
79 ),
80 ];
81
82 let reasoning_text = match self.requested_level {
86 Some(requested) => format!(
87 "reasoning: {} ({} requested)",
88 self.reasoning_level.as_str(),
89 requested.as_str()
90 ),
91 None => format!("reasoning: {}", self.reasoning_level.as_str()),
92 };
93 let model_display = self.model_name;
94
95 let left_content_width = reasoning_text.width();
97 let right_content_width = model_display.width();
98 let padding_width_line2 = if available_width > left_content_width + right_content_width {
99 available_width - left_content_width - right_content_width
100 } else {
101 1
102 };
103
104 let line2_spans = vec![
105 Span::styled(
107 reasoning_text,
108 Style::new().fg(self.theme.colors.text_disabled.to_color()),
109 ),
110 Span::raw(" ".repeat(padding_width_line2)),
112 Span::styled(
114 model_display,
115 Style::new().fg(self.theme.colors.text_disabled.to_color()),
116 ),
117 ];
118
119 let line1 = Line::from(line1_spans);
120 let line2 = Line::from(line2_spans);
121 let status_bar = Paragraph::new(vec![line1, line2]);
122
123 status_bar.render(area, buf);
124 }
125}
126
127pub(crate) fn format_token_status(
128 context_usage: Option<&ContextUsageSnapshot>,
129 last_usage: Option<TokenUsageTotals>,
130 session_usage: TokenUsageTotals,
131) -> String {
132 let session = format_compact_count(session_usage.total_tokens);
133 let context = match context_usage {
134 Some(snapshot) => format_context_snapshot(snapshot),
135 None => "context: n/a".to_string(),
136 };
137 match last_usage {
138 Some(usage) => format!(
139 "{} | last api: {} | session: {}",
140 context,
141 format_compact_count(usage.total_tokens),
142 session
143 ),
144 None => format!("{} | session: {}", context, session),
145 }
146}
147
148fn format_context_snapshot(snapshot: &ContextUsageSnapshot) -> String {
149 let used = format_compact_count(snapshot.used_tokens);
150 let source = match snapshot.source {
151 TokenUsageSource::Provider => "",
152 TokenUsageSource::Estimate => "~",
153 };
154 match (snapshot.max_tokens, snapshot.used_percent) {
155 (Some(max), Some(percent)) => format!(
156 "context: {}{} / {} ({}%)",
157 source,
158 used,
159 format_compact_count(max),
160 percent
161 ),
162 _ => format!("context: {}{} / unknown", source, used),
163 }
164}
165
166fn format_compact_count(value: usize) -> String {
167 if value >= 1_000_000 {
168 format_scaled(value, 1_000_000, "m")
169 } else if value >= 10_000 {
170 format_scaled(value, 1_000, "k")
171 } else {
172 value.to_string()
173 }
174}
175
176fn format_scaled(value: usize, divisor: usize, suffix: &str) -> String {
177 let whole = value / divisor;
178 let decimal = ((value % divisor) * 10) / divisor;
179 if decimal == 0 {
180 format!("{}{}", whole, suffix)
181 } else {
182 format!("{}.{}{}", whole, decimal, suffix)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn token_status_labels_last_and_session_usage() {
192 let context = ContextUsageSnapshot::from_usage(
193 &crate::models::TokenUsage::provider(12_000, 456, 12_456),
194 Some(128_000),
195 );
196 assert_eq!(
197 format_token_status(
198 Some(&context),
199 Some(TokenUsageTotals {
200 prompt_tokens: 12_000,
201 completion_tokens: 456,
202 total_tokens: 12_456,
203 ..TokenUsageTotals::default()
204 }),
205 TokenUsageTotals {
206 prompt_tokens: 500_000,
207 completion_tokens: 73_443,
208 total_tokens: 573_443,
209 ..TokenUsageTotals::default()
210 },
211 ),
212 "context: 12.4k / 128k (9%) | last api: 12.4k | session: 573.4k"
213 );
214 }
215
216 #[test]
217 fn token_status_handles_missing_last_usage() {
218 assert_eq!(
219 format_token_status(
220 None,
221 None,
222 TokenUsageTotals {
223 prompt_tokens: 900,
224 completion_tokens: 50,
225 total_tokens: 950,
226 ..TokenUsageTotals::default()
227 },
228 ),
229 "context: n/a | session: 950"
230 );
231 }
232
233 #[test]
234 fn token_status_marks_estimates() {
235 let context = ContextUsageSnapshot::from_estimate(
236 crate::domain::PromptTokenBreakdown {
237 system_tokens: 10,
238 instructions_tokens: 0,
239 message_tokens: 20,
240 tool_schema_tokens: 70,
241 image_count: 0,
242 message_count: 1,
243 tool_count: 4,
244 },
245 None,
246 );
247
248 assert_eq!(
249 format_token_status(Some(&context), None, TokenUsageTotals::default()),
250 "context: ~100 / unknown | session: 0"
251 );
252 }
253}