1use crate::badge::SessionVariables;
8use crate::config::{StatusBarSection, StatusBarWidgetConfig, WidgetId};
9use crate::status_bar::system_monitor::{SystemMonitorData, format_bytes_per_sec, format_memory};
10
11#[derive(Debug, Clone)]
13pub struct WidgetContext {
14 pub session_vars: SessionVariables,
16 pub system_data: SystemMonitorData,
18 pub git_branch: Option<String>,
20 pub git_ahead: u32,
22 pub git_behind: u32,
24 pub git_dirty: bool,
26 pub git_show_status: bool,
28 pub time_format: String,
30 pub update_available_version: Option<String>,
32}
33
34pub fn widget_text(id: &WidgetId, ctx: &WidgetContext, format_override: Option<&str>) -> String {
39 if let Some(fmt) = format_override {
40 return interpolate_format(fmt, ctx);
41 }
42
43 match id {
44 WidgetId::Clock => chrono::Local::now().format(&ctx.time_format).to_string(),
45 WidgetId::UsernameHostname => {
46 format!(
47 "{}@{}",
48 ctx.session_vars.username, ctx.session_vars.hostname
49 )
50 }
51 WidgetId::CurrentDirectory => ctx.session_vars.path.clone(),
52 WidgetId::GitBranch => {
53 if let Some(ref branch) = ctx.git_branch {
54 let mut text = format!("\u{e0a0} {}", branch);
55 if ctx.git_show_status {
56 if ctx.git_ahead > 0 {
57 text.push_str(&format!(" \u{2191}{}", ctx.git_ahead));
58 }
59 if ctx.git_behind > 0 {
60 text.push_str(&format!(" \u{2193}{}", ctx.git_behind));
61 }
62 if ctx.git_dirty {
63 text.push_str(" \u{25cf}");
64 }
65 }
66 text
67 } else {
68 String::new()
69 }
70 }
71 WidgetId::CpuUsage => format!("CPU {:>5.1}%", ctx.system_data.cpu_usage),
72 WidgetId::MemoryUsage => {
73 format!(
74 "MEM {}",
75 format_memory(ctx.system_data.memory_used, ctx.system_data.memory_total)
76 )
77 }
78 WidgetId::NetworkStatus => {
79 format!(
80 "\u{2193} {} \u{2191} {}",
81 format_bytes_per_sec(ctx.system_data.network_rx_rate),
82 format_bytes_per_sec(ctx.system_data.network_tx_rate)
83 )
84 }
85 WidgetId::BellIndicator => {
86 if ctx.session_vars.bell_count > 0 {
87 format!("\u{1f514} {}", ctx.session_vars.bell_count)
88 } else {
89 String::new()
90 }
91 }
92 WidgetId::CurrentCommand => ctx.session_vars.current_command.clone().unwrap_or_default(),
93 WidgetId::UpdateAvailable => {
94 if let Some(ref version) = ctx.update_available_version {
95 format!("\u{2b06} v{}", version)
96 } else {
97 String::new()
98 }
99 }
100 WidgetId::Custom(_) => String::new(),
101 }
102}
103
104pub fn interpolate_format(fmt: &str, ctx: &WidgetContext) -> String {
111 let mut result = String::with_capacity(fmt.len());
112 let mut chars = fmt.chars().peekable();
113
114 while let Some(ch) = chars.next() {
115 if ch == '\\' && chars.peek() == Some(&'(') {
116 chars.next();
118 let mut var_name = String::new();
120 let mut found_close = false;
121 for c in chars.by_ref() {
122 if c == ')' {
123 found_close = true;
124 break;
125 }
126 var_name.push(c);
127 }
128 if found_close {
129 let value = resolve_variable(&var_name, ctx);
131 result.push_str(&value);
132 } else {
133 result.push_str("\\(");
135 result.push_str(&var_name);
136 }
137 } else {
138 result.push(ch);
139 }
140 }
141
142 result
143}
144
145fn resolve_variable(name: &str, ctx: &WidgetContext) -> String {
147 match name {
148 n if n.starts_with("session.") => ctx.session_vars.get(n).unwrap_or_default(),
150 "git.branch" => ctx.git_branch.clone().unwrap_or_default(),
151 "git.ahead" => ctx.git_ahead.to_string(),
152 "git.behind" => ctx.git_behind.to_string(),
153 "git.dirty" => if ctx.git_dirty { "\u{25cf}" } else { "" }.to_string(),
154 "system.cpu" => format!("{:.1}%", ctx.system_data.cpu_usage),
155 "system.memory" => format_memory(ctx.system_data.memory_used, ctx.system_data.memory_total),
156 _ => String::new(),
157 }
158}
159
160pub fn sorted_widgets_for_section(
162 widgets: &[StatusBarWidgetConfig],
163 section: StatusBarSection,
164) -> Vec<&StatusBarWidgetConfig> {
165 let mut result: Vec<&StatusBarWidgetConfig> = widgets
166 .iter()
167 .filter(|w| w.enabled && w.section == section)
168 .collect();
169 result.sort_by_key(|w| w.order);
170 result
171}
172
173#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::config::StatusBarSection;
181
182 fn make_ctx() -> WidgetContext {
183 let sv = SessionVariables {
184 username: "alice".to_string(),
185 hostname: "dev-box".to_string(),
186 path: "/home/alice/project".to_string(),
187 bell_count: 3,
188 current_command: Some("cargo build".to_string()),
189 ..Default::default()
190 };
191
192 WidgetContext {
193 session_vars: sv,
194 system_data: SystemMonitorData {
195 cpu_usage: 42.5,
196 memory_used: 4_294_967_296, memory_total: 17_179_869_184, network_rx_rate: 1024,
199 network_tx_rate: 2048,
200 last_update: None,
201 },
202 git_branch: Some("main".to_string()),
203 git_ahead: 2,
204 git_behind: 1,
205 git_dirty: true,
206 git_show_status: true,
207 time_format: "%H:%M:%S".to_string(),
208 update_available_version: None,
209 }
210 }
211
212 #[test]
213 fn test_widget_text_clock() {
214 let ctx = make_ctx();
215 let text = widget_text(&WidgetId::Clock, &ctx, None);
216 assert_eq!(text.len(), 8);
218 assert_eq!(text.as_bytes()[2], b':');
219 assert_eq!(text.as_bytes()[5], b':');
220
221 let mut ctx2 = make_ctx();
223 ctx2.time_format = "%H:%M".to_string();
224 let text = widget_text(&WidgetId::Clock, &ctx2, None);
225 assert_eq!(text.len(), 5);
227 assert_eq!(text.as_bytes()[2], b':');
228
229 let mut ctx3 = make_ctx();
231 ctx3.time_format = "%I:%M %p".to_string();
232 let text = widget_text(&WidgetId::Clock, &ctx3, None);
233 assert!(text.contains(':'));
234 assert!(text.contains("AM") || text.contains("PM"));
235 }
236
237 #[test]
238 fn test_widget_text_username_hostname() {
239 let ctx = make_ctx();
240 let text = widget_text(&WidgetId::UsernameHostname, &ctx, None);
241 assert_eq!(text, "alice@dev-box");
242 }
243
244 #[test]
245 fn test_widget_text_current_directory() {
246 let ctx = make_ctx();
247 let text = widget_text(&WidgetId::CurrentDirectory, &ctx, None);
248 assert_eq!(text, "/home/alice/project");
249 }
250
251 #[test]
252 fn test_widget_text_git_branch() {
253 let ctx = make_ctx();
254 let text = widget_text(&WidgetId::GitBranch, &ctx, None);
255 assert_eq!(text, "\u{e0a0} main \u{2191}2 \u{2193}1 \u{25cf}");
257
258 let mut ctx_no_status = make_ctx();
260 ctx_no_status.git_show_status = false;
261 let text = widget_text(&WidgetId::GitBranch, &ctx_no_status, None);
262 assert_eq!(text, "\u{e0a0} main");
263
264 let mut ctx2 = make_ctx();
266 ctx2.git_branch = None;
267 let text = widget_text(&WidgetId::GitBranch, &ctx2, None);
268 assert!(text.is_empty());
269
270 let mut ctx_clean = make_ctx();
272 ctx_clean.git_ahead = 0;
273 ctx_clean.git_behind = 0;
274 ctx_clean.git_dirty = false;
275 let text = widget_text(&WidgetId::GitBranch, &ctx_clean, None);
276 assert_eq!(text, "\u{e0a0} main");
277 }
278
279 #[test]
280 fn test_widget_text_cpu_usage() {
281 let ctx = make_ctx();
282 let text = widget_text(&WidgetId::CpuUsage, &ctx, None);
283 assert_eq!(text, "CPU 42.5%");
284
285 let mut ctx2 = make_ctx();
287 ctx2.system_data.cpu_usage = 5.0;
288 let text2 = widget_text(&WidgetId::CpuUsage, &ctx2, None);
289 assert_eq!(text2, "CPU 5.0%");
290 assert_eq!(text.len(), text2.len());
292 }
293
294 #[test]
295 fn test_widget_text_memory_usage() {
296 let ctx = make_ctx();
297 let text = widget_text(&WidgetId::MemoryUsage, &ctx, None);
298 assert_eq!(text, "MEM 4.0 GB / 16.0 GB");
299 }
300
301 #[test]
302 fn test_widget_text_network_status() {
303 let ctx = make_ctx();
304 let text = widget_text(&WidgetId::NetworkStatus, &ctx, None);
305 assert_eq!(text, "\u{2193} 1.0 KB/s \u{2191} 2.0 KB/s");
306
307 let mut ctx2 = make_ctx();
309 ctx2.system_data.network_rx_rate = 500; ctx2.system_data.network_tx_rate = 1_048_576; let text2 = widget_text(&WidgetId::NetworkStatus, &ctx2, None);
312 assert_eq!(text.len(), text2.len());
313 }
314
315 #[test]
316 fn test_widget_text_bell_indicator() {
317 let ctx = make_ctx();
318 let text = widget_text(&WidgetId::BellIndicator, &ctx, None);
319 assert_eq!(text, "\u{1f514} 3");
320
321 let mut ctx2 = make_ctx();
322 ctx2.session_vars.bell_count = 0;
323 let text = widget_text(&WidgetId::BellIndicator, &ctx2, None);
324 assert!(text.is_empty());
325 }
326
327 #[test]
328 fn test_widget_text_current_command() {
329 let ctx = make_ctx();
330 let text = widget_text(&WidgetId::CurrentCommand, &ctx, None);
331 assert_eq!(text, "cargo build");
332 }
333
334 #[test]
335 fn test_widget_text_format_override() {
336 let ctx = make_ctx();
337 let text = widget_text(
338 &WidgetId::UsernameHostname,
339 &ctx,
340 Some("Host: \\(session.hostname) CPU: \\(system.cpu)"),
341 );
342 assert_eq!(text, "Host: dev-box CPU: 42.5%");
343 }
344
345 #[test]
346 fn test_interpolate_format() {
347 let ctx = make_ctx();
348 let result = interpolate_format(
349 "\\(session.username)@\\(session.hostname) [\\(git.branch)]",
350 &ctx,
351 );
352 assert_eq!(result, "alice@dev-box [main]");
353 }
354
355 #[test]
356 fn test_sorted_widgets_for_section() {
357 let widgets = vec![
358 StatusBarWidgetConfig {
359 id: WidgetId::Clock,
360 enabled: true,
361 section: StatusBarSection::Right,
362 order: 2,
363 format: None,
364 },
365 StatusBarWidgetConfig {
366 id: WidgetId::CpuUsage,
367 enabled: false,
368 section: StatusBarSection::Right,
369 order: 0,
370 format: None,
371 },
372 StatusBarWidgetConfig {
373 id: WidgetId::BellIndicator,
374 enabled: true,
375 section: StatusBarSection::Right,
376 order: 1,
377 format: None,
378 },
379 StatusBarWidgetConfig {
380 id: WidgetId::UsernameHostname,
381 enabled: true,
382 section: StatusBarSection::Left,
383 order: 0,
384 format: None,
385 },
386 ];
387
388 let right = sorted_widgets_for_section(&widgets, StatusBarSection::Right);
389 assert_eq!(right.len(), 2); assert_eq!(right[0].id, WidgetId::BellIndicator); assert_eq!(right[1].id, WidgetId::Clock); let left = sorted_widgets_for_section(&widgets, StatusBarSection::Left);
394 assert_eq!(left.len(), 1);
395 assert_eq!(left[0].id, WidgetId::UsernameHostname);
396
397 let center = sorted_widgets_for_section(&widgets, StatusBarSection::Center);
398 assert!(center.is_empty());
399 }
400}