use crate::extensions::p10k::config::{p9k_global, p9k_param};
use crate::extensions::p10k::icons;
use crate::extensions::p10k::render::Segment;
use crate::ported::params::getsparam;
use crate::ported::utils::getkeystring;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
pub fn build_segment(name: &str) -> Option<Vec<Segment>> {
match name {
"weather" => Some(weather_segments()), "uptime" => Some(uptime_segments()), "now_playing" => Some(now_playing_segments()), "network_load" => Some(network_load_segments()), "hg" => Some(hg_segments()), "svn" => Some(svn_segments()), "bzr" => Some(bzr_segments()), "fossil" => Some(fossil_segments()), _ => None,
}
}
fn color1() -> &'static str {
if p9k_global("COLOR_SCHEME", "dark") == "light" {
"7"
} else {
"0"
}
}
fn env_or_param(name: &str) -> String {
if let Some(v) = getsparam(name) {
return v;
}
std::env::var(name).unwrap_or_default()
}
fn global_int(name: &str, default: i64) -> i64 {
getsparam(&format!("POWERLEVEL9K_{name}"))
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(default)
}
fn esc_pct(s: &str) -> String {
s.replace('%', "%%")
}
fn decode_g(s: &str) -> String {
getkeystring(s).0
}
fn seg_icon(segment: &str, state: Option<&str>, key: &str) -> String {
let probed = p9k_param(segment, state, key, "\u{1}");
if probed == "\u{1}" {
return icons::icon(key).to_string();
}
let decoded = decode_g(&probed);
let mut ch = decoded.chars();
if ch.next() == Some('\u{8}') && ch.next().is_some() && ch.next().is_none() {
return format!("%{{{decoded}%}}");
}
decoded
}
fn apply_visual_identifier(segment: &str, state: Option<&str>, icon: String) -> Option<String> {
let exp = p9k_param(
segment,
state,
"VISUAL_IDENTIFIER_EXPANSION",
"${P9K_VISUAL_IDENTIFIER}",
);
let resolved = if exp == "${P9K_VISUAL_IDENTIFIER}" {
icon
} else if !exp.contains('$') {
exp
} else {
tracing::debug!(
target: "p10k",
segment,
?state,
%exp,
"dynamic VISUAL_IDENTIFIER_EXPANSION unported — using resolved icon"
);
icon
};
if resolved.is_empty() {
None
} else {
Some(resolved)
}
}
fn apply_content_expansion(segment: &str, state: Option<&str>, content: String) -> String {
let exp = p9k_param(segment, state, "CONTENT_EXPANSION", "${P9K_CONTENT}");
if exp == "${P9K_CONTENT}" {
return content;
}
if !exp.contains('$') {
return exp;
}
if exp.contains("${P9K_CONTENT}") {
let out = exp.replace("${P9K_CONTENT}", &content);
if out.contains('$') {
tracing::debug!(
target: "p10k",
segment, ?state, %exp,
"CONTENT_EXPANSION has unevaluated expansions beyond ${{P9K_CONTENT}}"
);
}
return out;
}
tracing::debug!(
target: "p10k",
segment, ?state, %exp,
"dynamic CONTENT_EXPANSION unported — using segment content"
);
content
}
fn make_segment(
name: &str,
state: Option<&str>,
default_bg: &str,
default_fg: &str,
icon_key: &str,
content: String,
) -> Segment {
let bg = p9k_param(name, state, "BACKGROUND", default_bg);
let fg = p9k_param(name, state, "FOREGROUND", default_fg);
let icon_glyph = if icon_key.is_empty() {
String::new()
} else {
seg_icon(name, state, icon_key)
};
let icon = apply_visual_identifier(name, state, icon_glyph);
let content = apply_content_expansion(name, state, content);
Segment {
name: name.to_string(),
state: state.map(|s| s.to_string()),
content,
icon,
fg,
bg,
}
}
fn cmd_on_path(name: &str) -> Option<PathBuf> {
let path_var = env_or_param("PATH");
for dir in path_var.split(':').filter(|d| !d.is_empty()) {
let cand = Path::new(dir).join(name);
let is_exec = std::fs::metadata(&cand)
.map(|md| {
use std::os::unix::fs::PermissionsExt;
md.is_file() && md.permissions().mode() & 0o111 != 0
})
.unwrap_or(false);
if is_exec {
return Some(cand);
}
}
None
}
fn cwd() -> PathBuf {
let pwd = env_or_param("PWD");
if !pwd.is_empty() {
let p = PathBuf::from(&pwd);
if p.is_absolute() {
return p;
}
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
}
fn upfind(name: &str) -> Option<PathBuf> {
let start = cwd();
let mut dir: &Path = &start;
loop {
if dir.join(name).exists() {
return Some(dir.to_path_buf());
}
match dir.parent() {
Some(p) => dir = p,
None => return None,
}
}
}
static TTL_CACHE: OnceLock<Mutex<HashMap<String, (Instant, Option<String>)>>> = OnceLock::new();
fn cached_ttl(key: &str, run: impl FnOnce() -> (Duration, Option<String>)) -> Option<String> {
let m = TTL_CACHE.get_or_init(Default::default);
if let Ok(guard) = m.lock() {
if let Some((expiry, val)) = guard.get(key) {
if Instant::now() < *expiry {
return val.clone();
}
}
}
let (ttl, val) = run();
let expiry = Instant::now()
.checked_add(ttl)
.unwrap_or_else(|| Instant::now() + Duration::from_secs(3600));
if let Ok(mut guard) = m.lock() {
guard.insert(key.to_string(), (expiry, val.clone()));
}
val
}
fn run_tool_budget(
bin: &Path,
args: &[&str],
dir: Option<&Path>,
budget: Duration,
require_success: bool,
) -> Option<String> {
let mut cmd = std::process::Command::new(bin);
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
if let Some(d) = dir {
cmd.current_dir(d);
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
tracing::debug!(target: "p10k", bin = %bin.display(), %e, "tool spawn failed");
return None;
}
};
let mut stdout = child.stdout.take()?;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut out = Vec::new();
use std::io::Read;
let _ = stdout.read_to_end(&mut out);
let _ = tx.send(out);
});
match rx.recv_timeout(budget) {
Ok(out) => {
let ok = child.wait().map(|s| s.success()).unwrap_or(false);
if require_success && !ok {
return None;
}
Some(String::from_utf8_lossy(&out).into_owned())
}
Err(_) => {
tracing::debug!(
target: "p10k",
bin = %bin.display(), ?budget,
"tool exceeded budget — killed"
);
let _ = child.kill();
let _ = child.wait();
None
}
}
}
fn humanize_bytes(num: f64, suffix: &str, si_prefix: bool) -> String {
const UNIT_LIST: [(&str, usize); 6] = [("", 0), ("k", 0), ("M", 1), ("G", 2), ("T", 2), ("P", 2)];
if num == 0.0 {
return format!("0 {suffix}");
}
let div: f64 = if si_prefix { 1000.0 } else { 1024.0 };
let exponent = ((num.ln() / div.ln()).trunc() as i64).clamp(0, 5) as usize;
let quotient = num / div.powi(exponent as i32);
let (unit, decimals) = UNIT_LIST[exponent];
let unit = if !unit.is_empty() && !si_prefix {
format!("{}i", unit.to_uppercase())
} else {
unit.to_string()
};
format!("{quotient:.decimals$} {unit}{suffix}")
}
fn format_uptime(total_seconds: i64, shorten_len: usize) -> String {
let (minutes, seconds) = (total_seconds / 60, total_seconds % 60);
let (hours, minutes) = (minutes / 60, minutes % 60);
let (days, hours) = (hours / 24, hours % 24);
let mut parts: Vec<String> = Vec::new();
if days != 0 {
parts.push(format!("{days}d")); }
if hours != 0 {
parts.push(format!(" {hours}h")); }
if minutes != 0 {
parts.push(format!(" {minutes}m")); }
if seconds != 0 {
parts.push(format!(" {seconds}s")); }
parts
.into_iter()
.take(shorten_len)
.collect::<String>()
.trim()
.to_string()
}
fn convert_seconds(seconds: f64) -> String {
let minutes = (seconds / 60.0).floor();
let rem = seconds - minutes * 60.0;
format!("{minutes:.0}:{rem:02.0}")
}
fn convert_state(state: &str) -> &'static str {
let state = state.to_lowercase();
if state.contains("play") {
"play"
} else if state.contains("pause") {
"pause"
} else if state.contains("stop") {
"stop"
} else {
"fallback"
}
}
fn state_symbol(state: &str) -> &'static str {
match state {
"play" => ">",
"pause" => "~",
"stop" => "X",
_ => "", }
}
fn valid_weather(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 40
&& !s.contains('<')
&& !s.contains('\n')
&& s.contains('°')
&& s.bytes().any(|b| b.is_ascii_digit())
}
fn encode_location(loc: &str) -> String {
let mut out = String::new();
for b in loc.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b',' | b'+' => {
out.push(b as char)
}
b' ' => out.push('+'),
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
fn weather_segments() -> Vec<Segment> {
let location = encode_location(&p9k_global("WEATHER_LOCATION", ""));
let units = p9k_global("WEATHER_UNITS", "");
let unit_q = match units.as_str() {
"F" | "f" | "u" | "imperial" => "&u",
"C" | "c" | "m" | "metric" => "&m",
_ => "",
};
let Some(curl) = cmd_on_path("curl") else {
return vec![]; };
let key = format!("weather {location} {unit_q}");
let text = cached_ttl(&key, || {
let url = format!("https://wttr.in/{location}?format=%c%t{unit_q}");
let fetched = run_tool_budget(
&curl,
&["-fsS", "--max-time", "5", &url],
None,
Duration::from_secs(6),
true,
)
.map(|s| s.trim().to_string())
.filter(|s| valid_weather(s));
match fetched {
Some(v) => (Duration::from_secs(900), Some(v)),
None => (Duration::from_secs(60), None),
}
});
let Some(text) = text else {
return vec![]; };
vec![make_segment(
"weather",
None,
"blue",
"white",
"WEATHER_ICON",
esc_pct(&text),
)]
}
#[cfg(target_os = "macos")]
fn uptime_seconds() -> Option<i64> {
let name = std::ffi::CString::new("kern.boottime").ok()?;
let mut tv: libc::timeval = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::timeval>();
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
&mut tv as *mut _ as *mut libc::c_void,
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || tv.tv_sec <= 0 {
return None;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs() as i64;
let up = now - tv.tv_sec;
(up > 0).then_some(up)
}
#[cfg(not(target_os = "macos"))]
fn uptime_seconds() -> Option<i64> {
let content = std::fs::read_to_string("/proc/uptime").ok()?;
let first = content.split_whitespace().next()?;
Some(first.parse::<f64>().ok()? as i64)
}
fn uptime_segments() -> Vec<Segment> {
let Some(secs) = uptime_seconds() else {
return vec![]; };
let shorten = global_int("UPTIME_SHORTEN_LEN", 3).clamp(1, 4) as usize;
let text = format_uptime(secs, shorten);
if text.is_empty() {
return vec![]; }
vec![make_segment("uptime", None, color1(), "cyan", "UPTIME_ICON", text)]
}
#[cfg(target_os = "macos")]
fn player_status() -> Option<(String, String, String, Option<f64>)> {
cached_ttl("now_playing.pgrep", || {
let out = cmd_on_path("pgrep").and_then(|bin| {
run_tool_budget(&bin, &["-x", "Music"], None, Duration::from_secs(2), true)
});
(Duration::from_secs(10), out)
})?;
const DELIM: &str = "-~`/=";
let script = r#"
tell application "System Events"
set process_list to (name of every process)
end tell
if process_list contains "Music" then
tell application "Music"
if player state is playing then
set t_title to name of current track
set t_artist to artist of current track
set t_album to album of current track
set t_duration to duration of current track
set t_elapsed to player position
set t_state to player state
return t_title & "-~`/=" & t_artist & "-~`/=" & t_album & "-~`/=" & t_elapsed & "-~`/=" & t_duration & "-~`/=" & t_state
end if
end tell
end if
"#;
let out = cached_ttl("now_playing.osascript", || {
let out = cmd_on_path("osascript").and_then(|bin| {
run_tool_budget(&bin, &["-e", script], None, Duration::from_secs(5), true)
});
(Duration::from_secs(10), out)
})?;
let line = out.trim_end_matches('\n');
if line.is_empty() {
return None; }
let fields: Vec<&str> = line.split(DELIM).collect();
if fields.len() < 6 {
tracing::debug!(target: "p10k", "now_playing: unexpected osascript field count");
return None;
}
let total = fields[4].trim().parse::<f64>().ok();
Some((
convert_state(fields[5]).to_string(),
fields[1].to_string(),
fields[0].to_string(),
total,
))
}
#[cfg(not(target_os = "macos"))]
fn player_status() -> Option<(String, String, String, Option<f64>)> {
let out = cached_ttl("now_playing.playerctl", || {
let out = cmd_on_path("playerctl").and_then(|bin| {
run_tool_budget(
&bin,
&[
"metadata",
"--format",
"{{status}}\t{{artist}}\t{{title}}\t{{mpris:length}}",
],
None,
Duration::from_secs(2),
true,
)
});
(Duration::from_secs(10), out)
})?;
let line = out.lines().next()?;
let mut f = line.split('\t');
let state = convert_state(f.next()?);
let artist = f.next().unwrap_or("").to_string();
let title = f.next().unwrap_or("").to_string();
let total = f
.next()
.and_then(|v| v.trim().parse::<f64>().ok())
.map(|us| us / 1_000_000.0);
if state == "stop" {
return None; }
Some((state.to_string(), artist, title, total))
}
fn now_playing_segments() -> Vec<Segment> {
let Some((state, artist, title, total)) = player_status() else {
return vec![]; };
let sym = state_symbol(&state);
let total_s = total.map(|t| convert_seconds(t)).unwrap_or_default();
let mut content = format!("{sym} {artist} - {title}");
if !total_s.is_empty() {
content.push_str(&format!(" ({total_s})"));
}
vec![make_segment(
"now_playing",
None,
color1(),
"white",
"NOW_PLAYING_ICON",
esc_pct(content.trim()),
)]
}
#[cfg(target_os = "macos")]
fn iface_counters() -> Vec<(String, u64, u64)> {
let mut out: Vec<(String, u64, u64)> = Vec::new();
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
if unsafe { libc::getifaddrs(&mut ifap) } != 0 {
tracing::warn!(target: "p10k", "getifaddrs failed for network_load scan");
return out;
}
let mut cur = ifap;
while !cur.is_null() {
let ifa = unsafe { &*cur };
cur = ifa.ifa_next;
if ifa.ifa_addr.is_null() || ifa.ifa_data.is_null() {
continue;
}
let sa = unsafe { &*ifa.ifa_addr };
if i32::from(sa.sa_family) != libc::AF_LINK {
continue;
}
let data = unsafe { &*(ifa.ifa_data as *const libc::if_data) };
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }
.to_string_lossy()
.into_owned();
out.push((name, u64::from(data.ifi_ibytes), u64::from(data.ifi_obytes)));
}
unsafe { libc::freeifaddrs(ifap) };
out
}
#[cfg(not(target_os = "macos"))]
fn iface_counters() -> Vec<(String, u64, u64)> {
let mut out: Vec<(String, u64, u64)> = Vec::new();
let Ok(rd) = std::fs::read_dir("/sys/class/net") else {
return out;
};
for entry in rd.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let stat = |leaf: &str| -> Option<u64> {
std::fs::read_to_string(entry.path().join("statistics").join(leaf))
.ok()?
.trim()
.parse()
.ok()
};
if let (Some(rx), Some(tx)) = (stat("rx_bytes"), stat("tx_bytes")) {
out.push((name, rx, tx));
}
}
out
}
fn auto_interface(counters: &[(String, u64, u64)]) -> String {
if let Ok(routes) = std::fs::read_to_string("/proc/net/route") {
for line in routes.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() > 1 && !parts[1].is_empty() && parts[1].bytes().all(|b| b == b'0') {
return parts[0].to_string();
}
}
}
let mut best = ("eth0".to_string(), None::<u64>);
for (name, rx, tx) in counters {
let base: String = name.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
if base.is_empty() || matches!(base.as_str(), "lo" | "vmnet" | "sit") {
continue;
}
let activity = rx + tx;
if best.1.is_none_or(|b| activity > b) {
best = (name.clone(), Some(activity));
}
}
best.0
}
fn counter_delta(prev: u64, last: u64) -> u64 {
if last >= prev {
last - prev
} else {
last + (1u64 << 32) - prev
}
}
type NetSample = (Instant, u64, u64);
static NET_SAMPLES: OnceLock<Mutex<HashMap<String, (NetSample, Option<NetSample>)>>> =
OnceLock::new();
fn network_load_segments() -> Vec<Segment> {
let mut iface = p9k_global("NETWORK_LOAD_INTERFACE", "auto");
let counters = iface_counters();
if iface == "auto" {
iface = auto_interface(&counters);
}
let Some(&(_, rx, tx)) = counters.iter().find(|(n, _, _)| *n == iface) else {
return vec![]; };
let now = Instant::now();
let (last, prev) = {
let m = NET_SAMPLES.get_or_init(Default::default);
let Ok(mut guard) = m.lock() else {
return vec![];
};
let entry = guard.entry(iface.clone()).or_insert(((now, rx, tx), None));
if now.duration_since(entry.0.0) >= Duration::from_secs(1) {
entry.1 = Some(entry.0);
entry.0 = (now, rx, tx);
}
*entry
};
let Some(prev) = prev else {
return vec![];
};
let dt = last.0.duration_since(prev.0).as_secs_f64();
if dt <= 0.0 {
return vec![];
}
let rx_rate = counter_delta(prev.1, last.1) as f64 / dt;
let tx_rate = counter_delta(prev.2, last.2) as f64 / dt;
let content = format!(
"\u{21e3}{} \u{21e1}{}",
humanize_bytes(rx_rate, "B/s", false),
humanize_bytes(tx_rate, "B/s", false)
);
vec![make_segment(
"network_load",
None,
"cyan",
color1(),
"NETWORK_ICON",
content,
)]
}
fn vcs_cached(
tool: &str,
root: &Path,
probe: impl FnOnce() -> Option<(String, bool)>,
) -> Option<(String, bool)> {
let key = format!("vcs.{tool} {}", root.display());
let joined = cached_ttl(&key, || {
(
Duration::from_secs(5),
probe().map(|(b, d)| format!("{b}\u{1f}{}", u8::from(d))),
)
})?;
let (b, d) = joined.split_once('\u{1f}')?;
Some((b.to_string(), d == "1"))
}
fn vcs_segment(seg: &str, icon_key: &str, branch: &str, dirty: bool) -> Vec<Segment> {
let (state, bg) = if dirty {
("DIRTY", "yellow")
} else {
("CLEAN", "green")
};
vec![make_segment(
seg,
Some(state),
bg,
color1(),
icon_key,
esc_pct(branch),
)]
}
fn hg_segments() -> Vec<Segment> {
let Some(bin) = cmd_on_path("hg") else {
return vec![];
};
let Some(root) = upfind(".hg") else {
return vec![];
};
let Some((branch, dirty)) = vcs_cached("hg", &root, || {
let out = run_tool_budget(&bin, &["id", "-b", "-i"], Some(&root), Duration::from_secs(5), true)?;
let line = out.lines().next()?;
let mut it = line.split_whitespace();
let node = it.next()?;
let branch = it.next()?.to_string();
Some((branch, node.ends_with('+')))
}) else {
return vec![];
};
vcs_segment("hg", "VCS_HG_ICON", &branch, dirty)
}
fn svn_segments() -> Vec<Segment> {
let Some(bin) = cmd_on_path("svn") else {
return vec![];
};
let Some(root) = upfind(".svn") else {
return vec![];
};
let Some((rev, dirty)) = vcs_cached("svn", &root, || {
let info = run_tool_budget(&bin, &["info"], Some(&root), Duration::from_secs(5), true)?;
let rev = info
.lines()
.find_map(|l| l.strip_prefix("Revision: "))?
.trim()
.to_string();
if rev.is_empty() {
return None;
}
let status =
run_tool_budget(&bin, &["status", "-q"], Some(&root), Duration::from_secs(5), true)?;
Some((format!("r{rev}"), status.lines().any(|l| !l.trim().is_empty())))
}) else {
return vec![];
};
vcs_segment("svn", "VCS_SVN_ICON", &rev, dirty)
}
fn bzr_segments() -> Vec<Segment> {
let Some(bin) = cmd_on_path("bzr") else {
return vec![];
};
let Some(root) = upfind(".bzr") else {
return vec![];
};
let Some((nick, dirty)) = vcs_cached("bzr", &root, || {
let out = run_tool_budget(&bin, &["nick"], Some(&root), Duration::from_secs(5), true)?;
let nick = out.trim().to_string();
if nick.is_empty() {
return None;
}
let status =
run_tool_budget(&bin, &["status", "-S"], Some(&root), Duration::from_secs(5), true)?;
Some((nick, !status.trim().is_empty()))
}) else {
return vec![];
};
vcs_segment("bzr", "VCS_BRANCH_ICON", &nick, dirty)
}
fn fossil_segments() -> Vec<Segment> {
let Some(bin) = cmd_on_path("fossil") else {
return vec![];
};
let Some(root) = upfind(".fslckout") else {
return vec![];
};
let Some((branch, dirty)) = vcs_cached("fossil", &root, || {
let out = run_tool_budget(
&bin,
&["branch", "current"],
Some(&root),
Duration::from_secs(5),
true,
)?;
let branch = out.trim().to_string();
if branch.is_empty() {
return None;
}
let status =
run_tool_budget(&bin, &["changes"], Some(&root), Duration::from_secs(5), true)?;
Some((branch, !status.trim().is_empty()))
}) else {
return vec![];
};
vcs_segment("fossil", "VCS_BRANCH_ICON", &branch, dirty)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn humanize_bytes_forms() {
assert_eq!(humanize_bytes(0.0, "B/s", false), "0 B/s"); assert_eq!(humanize_bytes(512.0, "B/s", false), "512 B/s"); assert_eq!(humanize_bytes(2048.0, "B/s", false), "2 KiB/s"); assert_eq!(
humanize_bytes(3.0 * 1024.0 * 1024.0, "B/s", false),
"3.0 MiB/s" );
assert_eq!(
humanize_bytes(5.0 * 1024.0 * 1024.0 * 1024.0, "B/s", false),
"5.00 GiB/s" );
assert_eq!(humanize_bytes(2000.0, "B/s", true), "2 kB/s"); assert_eq!(humanize_bytes(0.5, "B/s", false), "0 B/s"); }
#[test]
fn format_uptime_forms() {
assert_eq!(format_uptime(90061, 3), "1d 1h 1m"); assert_eq!(format_uptime(3661, 3), "1h 1m 1s");
assert_eq!(format_uptime(61, 3), "1m 1s");
assert_eq!(format_uptime(86_700, 3), "1d 5m"); assert_eq!(format_uptime(59, 1), "59s");
assert_eq!(format_uptime(0, 3), ""); }
#[test]
fn convert_seconds_forms() {
assert_eq!(convert_seconds(203.417), "3:23");
assert_eq!(convert_seconds(0.0), "0:00");
assert_eq!(convert_seconds(59.6), "0:60"); }
#[test]
fn convert_state_and_symbols() {
assert_eq!(convert_state("Playing"), "play");
assert_eq!(convert_state("paused"), "pause");
assert_eq!(convert_state("Stopped"), "stop");
assert_eq!(convert_state("kNowhere"), "fallback");
assert_eq!(state_symbol("play"), ">");
assert_eq!(state_symbol("pause"), "~");
assert_eq!(state_symbol("fallback"), "");
}
#[test]
fn counter_delta_wrap() {
assert_eq!(counter_delta(100, 350), 250);
assert_eq!(counter_delta(u32::MAX as u64 - 10, 20), 31); }
#[test]
fn auto_interface_selection() {
let counters = vec![
("lo0".to_string(), 900_000u64, 900_000u64),
("en0".to_string(), 5_000, 4_000),
("utun3".to_string(), 100, 100),
];
assert_eq!(auto_interface(&counters), "en0");
assert_eq!(auto_interface(&[]), "eth0"); }
#[test]
fn valid_weather_forms() {
assert!(valid_weather("☀️ +33°C"));
assert!(valid_weather("🌦 +12°F"));
assert!(!valid_weather(""));
assert!(!valid_weather("<html>rate limited</html>"));
assert!(!valid_weather("Unknown location; please try again"));
assert!(!valid_weather("line1\nline2 +3°C")); }
#[test]
fn encode_location_forms() {
assert_eq!(encode_location("Berlin"), "Berlin");
assert_eq!(encode_location("New York"), "New+York");
assert_eq!(encode_location("så"), "s%C3%A5");
}
}