use crate::ported::lib::humanize_bytes::humanize_bytes;
use regex::Regex;
use serde_json::{json, Map, Value};
use std::sync::OnceLock;
pub const POWERLINE_TEST_HOSTNAME_UUID: &str = "ee5bcdc6-b749-11e7-9456-50465d597777";
pub fn hostname<F>(
environ: &Map<String, Value>,
only_if_ssh: bool,
exclude_domain: bool,
hostname_lookup: F,
) -> Option<String>
where
F: FnOnce() -> String,
{
if let Some(test_uuid) = environ
.get("_POWERLINE_RUNNING_SHELL_TESTS")
.and_then(|v| v.as_str())
{
if test_uuid == POWERLINE_TEST_HOSTNAME_UUID {
return Some("hostname".to_string());
}
}
if only_if_ssh
&& !environ
.get("SSH_CLIENT")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false)
{
return None;
}
let h = hostname_lookup();
if exclude_domain {
Some(h.split('.').next().unwrap_or(&h).to_string())
} else {
Some(h)
}
}
pub fn _external_ip<F>(read: F) -> Option<String>
where
F: FnOnce() -> Option<String>,
{
read().map(|s| s.trim().to_string())
}
pub fn external_ip_render(ip: Option<&str>) -> Option<Vec<Value>> {
let ip = ip.filter(|s| !s.is_empty())?;
Some(vec![json!({
"contents": ip,
"divider_highlight_group": "background:divider",
})])
}
pub fn interface_starts() -> &'static [(&'static str, i32)] {
&[
("eth", 10),
("enp", 10),
("en", 10),
("ath", 9),
("wlan", 9),
("wlp", 9),
("teredo", 1),
("lo", -10),
("docker", -5),
("vmnet", -5),
("vboxnet", -5),
]
}
pub fn _interface_start_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"^([a-z]+?)(\d|$)").unwrap())
}
pub fn _interface_key(interface: &str) -> i64 {
let caps = match _interface_start_re().captures(interface) {
Some(c) => c,
None => return 0,
};
let prefix = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let base = match interface_starts().iter().find(|(p, _)| *p == prefix) {
Some((_, v)) => (*v as i64) * 100,
None => 500,
};
let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if let Ok(n) = suffix.parse::<i64>() {
base - n
} else {
base
}
}
pub fn replace_num_pat() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"[a-zA-Z]+").unwrap())
}
#[allow(clippy::too_many_arguments)]
pub fn render_one(
prev: Option<(f64, (u64, u64))>,
last: Option<(f64, (u64, u64))>,
recv_format: &str,
sent_format: &str,
suffix: &str,
si_prefix: bool,
recv_max: Option<f64>,
sent_max: Option<f64>,
) -> Option<Vec<Value>> {
let (t1, b1) = prev?;
let (t2, b2) = last?;
let interval = t2 - t1;
let mut out: Vec<Value> = Vec::new();
for (i, key) in [(0usize, "recv"), (1usize, "sent")] {
let fmt = if key == "recv" {
recv_format
} else {
sent_format
};
let bytes_delta = if i == 0 { b2.0 - b1.0 } else { b2.1 - b1.1 };
let value = if interval == 0.0 {
0.0
} else {
bytes_delta as f64 / interval
};
let max = if key == "recv" { recv_max } else { sent_max };
let is_gradient = max.is_some();
let mut hl_groups: Vec<String> =
vec![format!("network_load_{}", key), "network_load".to_string()];
if is_gradient {
let gradient: Vec<String> = hl_groups
.iter()
.map(|g| format!("{}_gradient", g))
.collect();
let mut new_groups = gradient;
new_groups.extend(hl_groups);
hl_groups = new_groups;
}
let rendered_value = humanize_bytes(value, suffix, si_prefix);
let mut contents = String::with_capacity(fmt.len());
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c != '{' {
contents.push(c);
continue;
}
let mut name = String::new();
while let Some(&p) = chars.peek() {
if p == '}' || p == ':' {
break;
}
name.push(p);
chars.next();
}
let mut spec = String::new();
if chars.peek() == Some(&':') {
chars.next();
while let Some(&p) = chars.peek() {
if p == '}' {
break;
}
spec.push(p);
chars.next();
}
}
if chars.peek() == Some(&'}') {
chars.next();
}
if name == "value" {
let (align, width) = if let Some(rest) = spec.strip_prefix('>') {
('>', rest.parse::<usize>().unwrap_or(0))
} else if let Some(rest) = spec.strip_prefix('<') {
('<', rest.parse::<usize>().unwrap_or(0))
} else if let Ok(w) = spec.parse::<usize>() {
('>', w)
} else {
(' ', 0)
};
if width == 0 {
contents.push_str(&rendered_value);
} else if align == '<' {
contents.push_str(&format!("{:<width$}", rendered_value, width = width));
} else {
contents.push_str(&format!("{:>width$}", rendered_value, width = width));
}
} else {
contents.push('{');
contents.push_str(&name);
if !spec.is_empty() {
contents.push(':');
contents.push_str(&spec);
}
contents.push('}');
}
}
let mut entry = json!({
"contents": contents,
"divider_highlight_group": "network_load:divider",
"highlight_groups": hl_groups,
});
if let Some(m) = max {
let level = if value >= m { 100.0 } else { value * 100.0 / m };
entry["gradient_level"] = json!(level);
}
out.push(entry);
}
Some(out)
}
#[derive(Debug, Clone)]
pub struct ExternalIpSegment {
pub query_url: String,
}
impl ExternalIpSegment {
pub const INTERVAL: u64 = 300;
pub const DEFAULT_QUERY_URL: &'static str = "http://ipv4.icanhazip.com/";
pub fn new() -> Self {
Self {
query_url: Self::DEFAULT_QUERY_URL.to_string(),
}
}
pub fn set_state(&mut self, query_url: Option<&str>) {
if let Some(url) = query_url {
self.query_url = url.to_string();
}
}
pub fn update<F>(&self, read: F) -> Option<String>
where
F: FnOnce(&str) -> Option<String>,
{
_external_ip(|| read(&self.query_url))
}
pub fn render(&self, ip: Option<&str>) -> Option<Vec<Value>> {
external_ip_render(ip)
}
}
impl Default for ExternalIpSegment {
fn default() -> Self {
Self::new()
}
}
pub fn internal_ip(_interface: &str, _ipv: u8) -> Option<String> {
None
}
pub fn network_load_key(interface: &str) -> String {
interface.to_string()
}
pub fn key(interface: &str) -> String {
network_load_key(interface)
}
pub fn compute_state(
interface: &str,
interfaces: &mut std::collections::HashMap<
String,
(Option<(f64, (u64, u64))>, Option<(f64, (u64, u64))>),
>,
proc_route_content: Option<&str>,
) -> Option<(Option<(f64, (u64, u64))>, Option<(f64, (u64, u64))>)> {
use crate::ported::lib::monotonic::monotonic;
let resolved_interface = if interface == "auto" {
let from_proc = proc_route_content.and_then(parse_proc_net_route_default);
match from_proc {
Some(iface) => iface,
None => {
let entries = _get_interfaces();
let borrowed: Vec<(String, u64, u64)> = entries;
let iter = borrowed.iter().map(|(n, r, t)| (n.as_str(), *r, *t));
pick_active_interface(iter)
}
}
} else {
interface.to_string()
};
let entry = interfaces
.entry(resolved_interface.clone())
.or_insert((None, None));
let (prev, last) = *entry;
let new_prev = last;
let new_last = _get_bytes(&resolved_interface).map(|b| (monotonic(), b));
*entry = (new_prev, new_last);
let _ = prev;
Some((new_prev, new_last))
}
pub fn parse_proc_net_route_default(content: &str) -> Option<String> {
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let iface = parts[0];
let destination = parts[1];
if destination.chars().all(|c| c == '0') {
return Some(iface.to_string());
}
}
None
}
pub fn pick_active_interface<'a, I>(interfaces: I) -> String
where
I: IntoIterator<Item = (&'a str, u64, u64)>,
{
let mut interface = "eth0".to_string();
let mut total: i64 = -1;
let re = replace_num_pat();
for (name, rx, tx) in interfaces {
let base = match re.find(name) {
Some(m) => m.as_str(),
None => continue,
};
if matches!(base, "lo" | "vmnet" | "sit") {
continue;
}
let activity = (rx as i64) + (tx as i64);
if activity > total {
total = activity;
interface = name.to_string();
}
}
interface
}
pub fn sysfs_rx_path(interface: &str) -> String {
format!("/sys/class/net/{}/statistics/rx_bytes", interface)
}
pub fn sysfs_tx_path(interface: &str) -> String {
format!("/sys/class/net/{}/statistics/tx_bytes", interface)
}
pub fn _get_bytes_sysfs(rx_content: &str, tx_content: &str) -> Option<(u64, u64)> {
let rx: u64 = rx_content.trim().parse().ok()?;
let tx: u64 = tx_content.trim().parse().ok()?;
Some((rx, tx))
}
pub fn _get_bytes(interface: &str) -> Option<(u64, u64)> {
let rx_path = sysfs_rx_path(interface);
let tx_path = sysfs_tx_path(interface);
let rx_content = std::fs::read_to_string(rx_path).ok()?;
let tx_content = std::fs::read_to_string(tx_path).ok()?;
_get_bytes_sysfs(&rx_content, &tx_content)
}
pub fn _get_interfaces() -> Vec<(String, u64, u64)> {
let mut out: Vec<(String, u64, u64)> = Vec::new();
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
return out;
};
for entry in entries.flatten() {
let Some(name) = entry.file_name().to_str().map(String::from) else {
continue;
};
if let Some((rx, tx)) = _get_bytes(&name) {
out.push((name, rx, tx));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn env_with(pairs: &[(&str, &str)]) -> Map<String, Value> {
let mut m = Map::new();
for (k, v) in pairs {
m.insert(k.to_string(), Value::String((*v).into()));
}
m
}
#[test]
fn hostname_uuid_returns_literal_hostname() {
let env = env_with(&[(
"_POWERLINE_RUNNING_SHELL_TESTS",
POWERLINE_TEST_HOSTNAME_UUID,
)]);
let r = hostname(&env, false, false, || "myhost".to_string());
assert_eq!(r, Some("hostname".to_string()));
}
#[test]
fn hostname_returns_lookup_when_no_ssh_constraint() {
let env = Map::new();
let r = hostname(&env, false, false, || "myhost.local".to_string());
assert_eq!(r, Some("myhost.local".to_string()));
}
#[test]
fn hostname_exclude_domain_strips_dot_suffix() {
let env = Map::new();
let r = hostname(&env, false, true, || "myhost.lan.example.com".to_string());
assert_eq!(r, Some("myhost".to_string()));
}
#[test]
fn hostname_only_if_ssh_returns_none_without_ssh_client() {
let env = Map::new();
let r = hostname(&env, true, false, || "myhost".to_string());
assert!(r.is_none());
}
#[test]
fn hostname_only_if_ssh_returns_hostname_when_ssh_client_set() {
let env = env_with(&[("SSH_CLIENT", "127.0.0.1 22 22")]);
let r = hostname(&env, true, false, || "myhost".to_string());
assert_eq!(r, Some("myhost".to_string()));
}
#[test]
fn external_ip_trims_whitespace() {
let r = _external_ip(|| Some(" 1.2.3.4\n".to_string()));
assert_eq!(r, Some("1.2.3.4".to_string()));
}
#[test]
fn external_ip_none_when_read_fails() {
let r = _external_ip(|| None);
assert!(r.is_none());
}
#[test]
fn external_ip_render_emits_segment_with_background_divider() {
let r = external_ip_render(Some("1.2.3.4")).unwrap();
assert_eq!(r[0]["contents"], "1.2.3.4");
assert_eq!(r[0]["divider_highlight_group"], "background:divider");
}
#[test]
fn external_ip_render_none_for_empty_or_none() {
assert!(external_ip_render(None).is_none());
assert!(external_ip_render(Some("")).is_none());
}
#[test]
fn interface_starts_contains_known_prefixes() {
let table = interface_starts();
let lookup =
|key: &str| -> Option<i32> { table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v) };
assert_eq!(lookup("eth"), Some(10));
assert_eq!(lookup("enp"), Some(10));
assert_eq!(lookup("en"), Some(10));
assert_eq!(lookup("lo"), Some(-10));
assert_eq!(lookup("teredo"), Some(1));
assert_eq!(lookup("docker"), Some(-5));
}
#[test]
fn interface_key_eth0_returns_priority() {
assert_eq!(_interface_key("eth0"), 1000);
}
#[test]
fn interface_key_eth1_lower_than_eth0() {
assert_eq!(_interface_key("eth1"), 999);
assert_eq!(_interface_key("eth2"), 998);
assert!(_interface_key("eth0") > _interface_key("eth1"));
}
#[test]
fn interface_key_lo_lower_than_eth() {
assert_eq!(_interface_key("lo"), -1000);
assert!(_interface_key("eth0") > _interface_key("lo"));
}
#[test]
fn interface_key_unknown_prefix_defaults_to_500() {
assert_eq!(_interface_key("custom0"), 500);
}
#[test]
fn interface_key_no_alpha_prefix_returns_zero() {
assert_eq!(_interface_key(""), 0);
assert_eq!(_interface_key("0"), 0);
}
#[test]
fn interface_start_re_matches_prefix_only() {
let r = _interface_start_re();
let c = r.captures("eth0").unwrap();
assert_eq!(&c[1], "eth");
assert_eq!(&c[2], "0");
}
#[test]
fn replace_num_pat_extracts_alpha_only() {
let r = replace_num_pat();
let m = r.find("eth0").unwrap();
assert_eq!(m.as_str(), "eth");
}
#[test]
fn render_one_no_samples_returns_none() {
let r = render_one(
None,
None,
"DL {value}",
"UL {value}",
"B/s",
false,
None,
None,
);
assert!(r.is_none());
}
#[test]
fn render_one_computes_recv_and_sent_rates() {
let prev = Some((0.0, (0u64, 0u64)));
let last = Some((10.0, (100u64, 200u64)));
let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
false,
None,
None,
)
.unwrap();
assert_eq!(r.len(), 2);
assert_eq!(r[0]["contents"], "DL 10 B/s");
assert_eq!(r[1]["contents"], "UL 20 B/s");
}
#[test]
fn render_one_zero_interval_returns_zero_rate() {
let prev = Some((5.0, (0u64, 0u64)));
let last = Some((5.0, (100u64, 200u64)));
let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
false,
None,
None,
)
.unwrap();
assert_eq!(r[0]["contents"], "DL 0 B/s");
assert_eq!(r[1]["contents"], "UL 0 B/s");
}
#[test]
fn render_one_with_gradient_appends_gradient_groups_and_level() {
let prev = Some((0.0, (0u64, 0u64)));
let last = Some((10.0, (100u64, 200u64))); let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
false,
Some(20.0),
None,
)
.unwrap();
let recv = &r[0];
let groups = recv["highlight_groups"].as_array().unwrap();
assert_eq!(groups[0], "network_load_recv_gradient");
assert_eq!(groups[1], "network_load_gradient");
assert_eq!(groups[2], "network_load_recv");
assert_eq!(groups[3], "network_load");
assert_eq!(recv["gradient_level"], 50.0);
let sent = &r[1];
assert!(sent.get("gradient_level").is_none());
}
#[test]
fn render_one_gradient_clamps_to_100_at_max() {
let prev = Some((0.0, (0u64, 0u64)));
let last = Some((1.0, (200u64, 0u64))); let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
false,
Some(100.0),
None,
)
.unwrap();
assert_eq!(r[0]["gradient_level"], 100.0);
}
#[test]
fn render_one_emits_network_load_divider_group() {
let prev = Some((0.0, (0u64, 0u64)));
let last = Some((1.0, (10u64, 20u64)));
let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
false,
None,
None,
)
.unwrap();
for s in &r {
assert_eq!(s["divider_highlight_group"], "network_load:divider");
}
}
#[test]
fn render_one_si_prefix_uses_decimal_units() {
let prev = Some((0.0, (0u64, 0u64)));
let last = Some((1.0, (1000u64, 0u64)));
let r = render_one(
prev,
last,
"DL {value}",
"UL {value}",
"B/s",
true,
None,
None,
)
.unwrap();
let contents = r[0]["contents"].as_str().unwrap();
assert!(contents.contains("k") || contents.contains("K"));
}
#[test]
fn powerline_test_hostname_uuid_matches_upstream() {
assert_eq!(
POWERLINE_TEST_HOSTNAME_UUID,
"ee5bcdc6-b749-11e7-9456-50465d597777"
);
}
#[test]
fn external_ip_segment_interval_matches_upstream() {
assert_eq!(ExternalIpSegment::INTERVAL, 300);
}
#[test]
fn external_ip_segment_default_query_url() {
assert_eq!(
ExternalIpSegment::DEFAULT_QUERY_URL,
"http://ipv4.icanhazip.com/"
);
let s = ExternalIpSegment::new();
assert_eq!(s.query_url, ExternalIpSegment::DEFAULT_QUERY_URL);
}
#[test]
fn external_ip_segment_set_state_overrides_query_url() {
let mut s = ExternalIpSegment::new();
s.set_state(Some("http://ipv6.icanhazip.com/"));
assert_eq!(s.query_url, "http://ipv6.icanhazip.com/");
}
#[test]
fn external_ip_segment_set_state_no_arg_preserves_url() {
let mut s = ExternalIpSegment::new();
s.set_state(None);
assert_eq!(s.query_url, ExternalIpSegment::DEFAULT_QUERY_URL);
}
#[test]
fn external_ip_segment_update_dispatches_to_query_url() {
let mut s = ExternalIpSegment::new();
s.query_url = "http://test.example/ip".to_string();
let ip = s.update(|url| {
assert_eq!(url, "http://test.example/ip");
Some(" 4.3.2.1\n".to_string())
});
assert_eq!(ip, Some("4.3.2.1".to_string()));
}
#[test]
fn external_ip_segment_render_delegates_to_external_ip_render() {
let s = ExternalIpSegment::new();
let r = s.render(Some("1.2.3.4")).unwrap();
assert_eq!(r[0]["contents"], "1.2.3.4");
}
#[test]
fn internal_ip_fallback_returns_none() {
assert!(internal_ip("eth0", 4).is_none());
assert!(internal_ip("auto", 6).is_none());
}
#[test]
fn network_load_key_returns_interface_name() {
assert_eq!(network_load_key("eth0"), "eth0");
assert_eq!(network_load_key("auto"), "auto");
}
#[test]
fn parse_proc_net_route_default_finds_zero_destination_row() {
let content = concat!(
"Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n",
"eth0\t00000000\t0102A8C0\t0003\t0\t0\t0\t00000000\t0\t0\t0\n",
"eth0\t000000FE\t00000000\t0001\t0\t0\t0\t000000FF\t0\t0\t0\n",
);
assert_eq!(
parse_proc_net_route_default(content),
Some("eth0".to_string())
);
}
#[test]
fn parse_proc_net_route_default_returns_none_when_no_default_route() {
let content = "Iface\tDestination\nlo\tFFFFFFFF\n";
assert!(parse_proc_net_route_default(content).is_none());
}
#[test]
fn parse_proc_net_route_default_handles_blank_lines() {
let content = "\n\neth0\t00000000\tx\n";
assert_eq!(
parse_proc_net_route_default(content),
Some("eth0".to_string())
);
}
#[test]
fn pick_active_interface_picks_highest_total() {
let ifaces = vec![("eth0", 100u64, 50u64), ("wlan0", 1000u64, 500u64)];
assert_eq!(pick_active_interface(ifaces), "wlan0");
}
#[test]
fn pick_active_interface_excludes_lo_and_vmnet_and_sit() {
let ifaces = vec![
("lo", 1_000_000u64, 1_000_000u64),
("vmnet0", 999u64, 999u64),
("sit0", 500u64, 500u64),
("eth0", 100u64, 100u64),
];
assert_eq!(pick_active_interface(ifaces), "eth0");
}
#[test]
fn pick_active_interface_empty_input_defaults_to_eth0() {
let empty: Vec<(&str, u64, u64)> = vec![];
assert_eq!(pick_active_interface(empty), "eth0");
}
#[test]
fn pick_active_interface_only_excluded_defaults_to_eth0() {
let ifaces = vec![("lo", 1u64, 1u64)];
assert_eq!(pick_active_interface(ifaces), "eth0");
}
#[test]
fn sysfs_rx_path_builds_correct_path() {
assert_eq!(
sysfs_rx_path("eth0"),
"/sys/class/net/eth0/statistics/rx_bytes"
);
}
#[test]
fn sysfs_tx_path_builds_correct_path() {
assert_eq!(
sysfs_tx_path("eth0"),
"/sys/class/net/eth0/statistics/tx_bytes"
);
}
#[test]
fn get_bytes_sysfs_parses_integers() {
let r = _get_bytes_sysfs("1234\n", "5678\n");
assert_eq!(r, Some((1234, 5678)));
}
#[test]
fn get_bytes_no_such_interface_returns_none() {
let r = _get_bytes("nonexistent_iface_zz9999");
assert!(r.is_none(), "expected None for missing iface, got {r:?}");
}
#[test]
fn key_alias_delegates_to_network_load_key() {
assert_eq!(key("eth0"), "eth0");
assert_eq!(key("wlan0"), "wlan0");
}
#[test]
fn compute_state_explicit_interface_skips_route_walk() {
let mut interfaces = std::collections::HashMap::new();
let result = compute_state("nonexistent_iface_zz", &mut interfaces, None);
assert!(result.is_some());
let (prev, _last) = result.unwrap();
assert!(prev.is_none());
}
#[test]
fn compute_state_auto_uses_proc_route_default() {
let proc_content = "Iface\tDestination\nlocalif\t00000000\n";
let mut interfaces = std::collections::HashMap::new();
let result = compute_state("auto", &mut interfaces, Some(proc_content));
assert!(result.is_some());
assert!(
interfaces.contains_key("localif"),
"expected `localif` key in interfaces map"
);
}
#[test]
fn compute_state_second_call_shifts_last_to_prev() {
let mut interfaces = std::collections::HashMap::new();
interfaces.insert(
"fakeif".to_string(),
(None, Some((100.0_f64, (5000_u64, 6000_u64)))),
);
let result = compute_state("fakeif", &mut interfaces, None);
let (prev, _last) = result.unwrap();
assert_eq!(prev, Some((100.0, (5000, 6000))));
}
#[test]
fn get_interfaces_returns_empty_or_real_data() {
let entries = _get_interfaces();
for (name, rx, tx) in &entries {
assert!(!name.is_empty(), "empty iface name");
let _ = (rx, tx);
}
}
#[test]
fn get_bytes_sysfs_returns_none_for_invalid_input() {
assert!(_get_bytes_sysfs("not-a-number", "5678").is_none());
assert!(_get_bytes_sysfs("1234", "abc").is_none());
}
}