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 contents = fmt.replace("{value}", &humanize_bytes(value, suffix, si_prefix));
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 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))
}
#[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_sysfs_returns_none_for_invalid_input() {
assert!(_get_bytes_sysfs("not-a-number", "5678").is_none());
assert!(_get_bytes_sysfs("1234", "abc").is_none());
}
}