use crate::badge::SessionVariables;
use crate::status_bar::config::{StatusBarSection, StatusBarWidgetConfig, WidgetId};
use crate::status_bar::system_monitor::{SystemMonitorData, format_bytes_per_sec, format_memory};
#[derive(Debug, Clone)]
pub struct WidgetContext {
pub session_vars: SessionVariables,
pub system_data: SystemMonitorData,
pub git_branch: Option<String>,
pub git_ahead: u32,
pub git_behind: u32,
pub git_dirty: bool,
pub git_show_status: bool,
pub time_format: String,
pub update_available_version: Option<String>,
}
pub fn widget_text(id: &WidgetId, ctx: &WidgetContext, format_override: Option<&str>) -> String {
if let Some(fmt) = format_override {
return interpolate_format(fmt, ctx);
}
match id {
WidgetId::Clock => chrono::Local::now().format(&ctx.time_format).to_string(),
WidgetId::UsernameHostname => {
format!(
"{}@{}",
ctx.session_vars.username, ctx.session_vars.hostname
)
}
WidgetId::CurrentDirectory => ctx.session_vars.path.clone(),
WidgetId::GitBranch => {
if let Some(ref branch) = ctx.git_branch {
let mut text = format!("\u{e0a0} {}", branch);
if ctx.git_show_status {
if ctx.git_ahead > 0 {
text.push_str(&format!(" \u{2191}{}", ctx.git_ahead));
}
if ctx.git_behind > 0 {
text.push_str(&format!(" \u{2193}{}", ctx.git_behind));
}
if ctx.git_dirty {
text.push_str(" \u{25cf}");
}
}
text
} else {
String::new()
}
}
WidgetId::CpuUsage => format!("CPU {:>5.1}%", ctx.system_data.cpu_usage),
WidgetId::MemoryUsage => {
format!(
"MEM {}",
format_memory(ctx.system_data.memory_used, ctx.system_data.memory_total)
)
}
WidgetId::NetworkStatus => {
format!(
"\u{2193} {} \u{2191} {}",
format_bytes_per_sec(ctx.system_data.network_rx_rate),
format_bytes_per_sec(ctx.system_data.network_tx_rate)
)
}
WidgetId::BellIndicator => {
if ctx.session_vars.bell_count > 0 {
format!("\u{1f514} {}", ctx.session_vars.bell_count)
} else {
String::new()
}
}
WidgetId::CurrentCommand => ctx.session_vars.current_command.clone().unwrap_or_default(),
WidgetId::UpdateAvailable => {
if let Some(ref version) = ctx.update_available_version {
format!("\u{2b06} v{}", version)
} else {
String::new()
}
}
WidgetId::Custom(_) => String::new(),
}
}
pub fn interpolate_format(fmt: &str, ctx: &WidgetContext) -> String {
let mut result = String::with_capacity(fmt.len());
let mut chars = fmt.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' && chars.peek() == Some(&'(') {
chars.next();
let mut var_name = String::new();
let mut found_close = false;
for c in chars.by_ref() {
if c == ')' {
found_close = true;
break;
}
var_name.push(c);
}
if found_close {
let value = resolve_variable(&var_name, ctx);
result.push_str(&value);
} else {
result.push_str("\\(");
result.push_str(&var_name);
}
} else {
result.push(ch);
}
}
result
}
fn resolve_variable(name: &str, ctx: &WidgetContext) -> String {
match name {
n if n.starts_with("session.") => ctx.session_vars.get(n).unwrap_or_default(),
"git.branch" => ctx.git_branch.clone().unwrap_or_default(),
"git.ahead" => ctx.git_ahead.to_string(),
"git.behind" => ctx.git_behind.to_string(),
"git.dirty" => if ctx.git_dirty { "\u{25cf}" } else { "" }.to_string(),
"system.cpu" => format!("{:.1}%", ctx.system_data.cpu_usage),
"system.memory" => format_memory(ctx.system_data.memory_used, ctx.system_data.memory_total),
_ => String::new(),
}
}
pub fn sorted_widgets_for_section(
widgets: &[StatusBarWidgetConfig],
section: StatusBarSection,
) -> Vec<&StatusBarWidgetConfig> {
let mut result: Vec<&StatusBarWidgetConfig> = widgets
.iter()
.filter(|w| w.enabled && w.section == section)
.collect();
result.sort_by_key(|w| w.order);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::status_bar::config::StatusBarSection;
fn make_ctx() -> WidgetContext {
let sv = SessionVariables {
username: "alice".to_string(),
hostname: "dev-box".to_string(),
path: "/home/alice/project".to_string(),
bell_count: 3,
current_command: Some("cargo build".to_string()),
..Default::default()
};
WidgetContext {
session_vars: sv,
system_data: SystemMonitorData {
cpu_usage: 42.5,
memory_used: 4_294_967_296, memory_total: 17_179_869_184, network_rx_rate: 1024,
network_tx_rate: 2048,
last_update: None,
},
git_branch: Some("main".to_string()),
git_ahead: 2,
git_behind: 1,
git_dirty: true,
git_show_status: true,
time_format: "%H:%M:%S".to_string(),
update_available_version: None,
}
}
#[test]
fn test_widget_text_clock() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::Clock, &ctx, None);
assert_eq!(text.len(), 8);
assert_eq!(text.as_bytes()[2], b':');
assert_eq!(text.as_bytes()[5], b':');
let mut ctx2 = make_ctx();
ctx2.time_format = "%H:%M".to_string();
let text = widget_text(&WidgetId::Clock, &ctx2, None);
assert_eq!(text.len(), 5);
assert_eq!(text.as_bytes()[2], b':');
let mut ctx3 = make_ctx();
ctx3.time_format = "%I:%M %p".to_string();
let text = widget_text(&WidgetId::Clock, &ctx3, None);
assert!(text.contains(':'));
assert!(text.contains("AM") || text.contains("PM"));
}
#[test]
fn test_widget_text_username_hostname() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::UsernameHostname, &ctx, None);
assert_eq!(text, "alice@dev-box");
}
#[test]
fn test_widget_text_current_directory() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::CurrentDirectory, &ctx, None);
assert_eq!(text, "/home/alice/project");
}
#[test]
fn test_widget_text_git_branch() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::GitBranch, &ctx, None);
assert_eq!(text, "\u{e0a0} main \u{2191}2 \u{2193}1 \u{25cf}");
let mut ctx_no_status = make_ctx();
ctx_no_status.git_show_status = false;
let text = widget_text(&WidgetId::GitBranch, &ctx_no_status, None);
assert_eq!(text, "\u{e0a0} main");
let mut ctx2 = make_ctx();
ctx2.git_branch = None;
let text = widget_text(&WidgetId::GitBranch, &ctx2, None);
assert!(text.is_empty());
let mut ctx_clean = make_ctx();
ctx_clean.git_ahead = 0;
ctx_clean.git_behind = 0;
ctx_clean.git_dirty = false;
let text = widget_text(&WidgetId::GitBranch, &ctx_clean, None);
assert_eq!(text, "\u{e0a0} main");
}
#[test]
fn test_widget_text_cpu_usage() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::CpuUsage, &ctx, None);
assert_eq!(text, "CPU 42.5%");
let mut ctx2 = make_ctx();
ctx2.system_data.cpu_usage = 5.0;
let text2 = widget_text(&WidgetId::CpuUsage, &ctx2, None);
assert_eq!(text2, "CPU 5.0%");
assert_eq!(text.len(), text2.len());
}
#[test]
fn test_widget_text_memory_usage() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::MemoryUsage, &ctx, None);
assert_eq!(text, "MEM 4.0 GB / 16.0 GB");
}
#[test]
fn test_widget_text_network_status() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::NetworkStatus, &ctx, None);
assert_eq!(text, "\u{2193} 1.0 KB/s \u{2191} 2.0 KB/s");
let mut ctx2 = make_ctx();
ctx2.system_data.network_rx_rate = 500; ctx2.system_data.network_tx_rate = 1_048_576; let text2 = widget_text(&WidgetId::NetworkStatus, &ctx2, None);
assert_eq!(text.len(), text2.len());
}
#[test]
fn test_widget_text_bell_indicator() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::BellIndicator, &ctx, None);
assert_eq!(text, "\u{1f514} 3");
let mut ctx2 = make_ctx();
ctx2.session_vars.bell_count = 0;
let text = widget_text(&WidgetId::BellIndicator, &ctx2, None);
assert!(text.is_empty());
}
#[test]
fn test_widget_text_current_command() {
let ctx = make_ctx();
let text = widget_text(&WidgetId::CurrentCommand, &ctx, None);
assert_eq!(text, "cargo build");
}
#[test]
fn test_widget_text_format_override() {
let ctx = make_ctx();
let text = widget_text(
&WidgetId::UsernameHostname,
&ctx,
Some("Host: \\(session.hostname) CPU: \\(system.cpu)"),
);
assert_eq!(text, "Host: dev-box CPU: 42.5%");
}
#[test]
fn test_interpolate_format() {
let ctx = make_ctx();
let result = interpolate_format(
"\\(session.username)@\\(session.hostname) [\\(git.branch)]",
&ctx,
);
assert_eq!(result, "alice@dev-box [main]");
}
#[test]
fn test_sorted_widgets_for_section() {
let widgets = vec![
StatusBarWidgetConfig {
id: WidgetId::Clock,
enabled: true,
section: StatusBarSection::Right,
order: 2,
format: None,
},
StatusBarWidgetConfig {
id: WidgetId::CpuUsage,
enabled: false,
section: StatusBarSection::Right,
order: 0,
format: None,
},
StatusBarWidgetConfig {
id: WidgetId::BellIndicator,
enabled: true,
section: StatusBarSection::Right,
order: 1,
format: None,
},
StatusBarWidgetConfig {
id: WidgetId::UsernameHostname,
enabled: true,
section: StatusBarSection::Left,
order: 0,
format: None,
},
];
let right = sorted_widgets_for_section(&widgets, StatusBarSection::Right);
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);
assert_eq!(left.len(), 1);
assert_eq!(left[0].id, WidgetId::UsernameHostname);
let center = sorted_widgets_for_section(&widgets, StatusBarSection::Center);
assert!(center.is_empty());
}
}