use serde_json::{json, Value};
pub fn _sizeof_fmt(num: f64, short: bool, suffix: &str) -> String {
let suffix: &str = if short { "" } else { suffix };
let units: &[(&str, &str)] = &[
("", ""),
("Ki", "K"),
("Mi", "M"),
("Gi", "G"),
("Ti", "T"),
("Pi", "P"),
("Ei", "E"),
("Zi", "Z"),
];
let mut n = num;
for (long, short_u) in units {
if n.abs() < 1024.0 {
let unit = if short { *short_u } else { *long };
if n != 0.0 {
return format!("{:3.1}{}{}", n, unit, suffix);
} else {
return format!("{}{}{}", n as i64, unit, suffix);
}
}
n /= 1024.0;
}
let unit = if short { "Y" } else { "Yi" };
if n != 0.0 {
format!("{:.1}{}{}", n, unit, suffix)
} else {
format!("{}{}{}", n as i64, unit, suffix)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MemStats {
pub total: u64,
pub used: u64,
pub free: u64,
pub available: u64,
}
impl MemStats {
pub fn _get_mem_used(&self, mem_type: &str) -> u64 {
match mem_type {
"used" => self.used,
"free" => self.free,
"available" => self.available,
"total" => self.total,
_ => self.used,
}
}
}
pub fn virtual_memory() -> MemStats {
#[cfg(target_os = "macos")]
{
read_vm_stat().unwrap_or_default()
}
#[cfg(target_os = "linux")]
{
read_proc_meminfo().unwrap_or_default()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
MemStats::default()
}
pub fn swap_memory() -> MemStats {
#[cfg(target_os = "macos")]
{
read_swapusage().unwrap_or_default()
}
#[cfg(target_os = "linux")]
{
read_proc_meminfo_swap().unwrap_or_default()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
MemStats::default()
}
#[cfg(target_os = "macos")]
fn read_vm_stat() -> Option<MemStats> {
let out = std::process::Command::new("vm_stat").output().ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let mut free: u64 = 0;
let mut active: u64 = 0;
let mut inactive: u64 = 0;
let mut speculative: u64 = 0;
let mut wired: u64 = 0;
let mut compressed: u64 = 0;
let mut page_size: u64 = 4096;
for line in text.lines() {
if let Some(p) = line.strip_prefix("Mach Virtual Memory Statistics: (page size of ") {
if let Some(n) = p.split(' ').next().and_then(|s| s.parse().ok()) {
page_size = n;
}
}
let parse = |label: &str| -> Option<u64> {
line.strip_prefix(label)
.and_then(|r| r.trim().trim_end_matches('.').parse().ok())
};
if let Some(n) = parse("Pages free:") {
free = n;
}
if let Some(n) = parse("Pages active:") {
active = n;
}
if let Some(n) = parse("Pages inactive:") {
inactive = n;
}
if let Some(n) = parse("Pages speculative:") {
speculative = n;
}
if let Some(n) = parse("Pages wired down:") {
wired = n;
}
if let Some(n) = parse("Pages occupied by compressor:") {
compressed = n;
}
}
let total = std::process::Command::new("sysctl")
.args(["-n", "hw.memsize"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or_else(|| {
(free + active + inactive + speculative + wired + compressed) * page_size
});
let used_pages = active + wired + compressed;
let free_bytes = (free + speculative) * page_size;
let available_bytes = (free + inactive + speculative) * page_size;
Some(MemStats {
total,
used: used_pages * page_size,
free: free_bytes,
available: available_bytes,
})
}
#[cfg(target_os = "macos")]
fn read_swapusage() -> Option<MemStats> {
let out = std::process::Command::new("sysctl")
.args(["-n", "vm.swapusage"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let parse_mb = |label: &str| -> Option<u64> {
let idx = text.find(label)?;
let rest = &text[idx + label.len()..];
let val: f64 = rest.trim_start().split('M').next()?.trim().parse().ok()?;
Some((val * 1024.0 * 1024.0) as u64)
};
let total = parse_mb("total = ").unwrap_or(0);
let used = parse_mb("used = ").unwrap_or(0);
let free = parse_mb("free = ").unwrap_or(0);
Some(MemStats {
total,
used,
free,
available: free,
})
}
#[cfg(target_os = "linux")]
fn read_proc_meminfo() -> Option<MemStats> {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
let mut total_kb: u64 = 0;
let mut free_kb: u64 = 0;
let mut available_kb: u64 = 0;
for line in text.lines() {
let mut parts = line.split_whitespace();
let key = parts.next().unwrap_or("");
let val: u64 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
match key {
"MemTotal:" => total_kb = val,
"MemFree:" => free_kb = val,
"MemAvailable:" => available_kb = val,
_ => {}
}
}
let kb = 1024_u64;
Some(MemStats {
total: total_kb * kb,
used: total_kb.saturating_sub(available_kb) * kb,
free: free_kb * kb,
available: available_kb * kb,
})
}
#[cfg(target_os = "linux")]
fn read_proc_meminfo_swap() -> Option<MemStats> {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
let mut total_kb: u64 = 0;
let mut free_kb: u64 = 0;
for line in text.lines() {
let mut parts = line.split_whitespace();
let key = parts.next().unwrap_or("");
let val: u64 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
match key {
"SwapTotal:" => total_kb = val,
"SwapFree:" => free_kb = val,
_ => {}
}
}
let kb = 1024_u64;
let total = total_kb * kb;
let free = free_kb * kb;
Some(MemStats {
total,
used: total.saturating_sub(free),
free,
available: free,
})
}
pub fn mem_usage(format: &str, mem_type: &str, short: bool) -> Vec<Value> {
let mem_data = virtual_memory();
let mem_used = mem_data._get_mem_used(mem_type);
let mem_percentage = if mem_data.total > 0 {
(mem_used as f64 / mem_data.total as f64) * 100.0
} else {
0.0
};
let contents = sprintf_two_strings(
format,
&_sizeof_fmt(mem_used as f64, short, "B"),
&_sizeof_fmt(mem_data.total as f64, short, "B"),
);
vec![json!({
"contents": contents,
"gradient_level": mem_percentage,
"highlight_groups": ["mem_usage_gradient", "mem_usage"],
"divider_highlight_group": "background:divider",
})]
}
pub fn mem_usage_percent(format: &str, mem_type: &str) -> Vec<Value> {
let mem_data = virtual_memory();
let mem_used = mem_data._get_mem_used(mem_type);
let mem_percentage = if mem_data.total > 0 {
(mem_used as f64 / mem_data.total as f64) * 100.0
} else {
0.0
};
let contents = sprintf_one_float(format, mem_percentage);
vec![json!({
"contents": contents,
"gradient_level": mem_percentage,
"highlight_groups": ["mem_usage_gradient", "mem_usage"],
"divider_highlight_group": "background:divider",
})]
}
pub fn mem_swap(format: &str, mem_type: &str, short: bool) -> Vec<Value> {
let mem_data = swap_memory();
let mem_used = mem_data._get_mem_used(mem_type);
let mem_percentage = if mem_data.total > 0 {
(mem_used as f64 / mem_data.total as f64) * 100.0
} else {
0.0
};
let used_str = _sizeof_fmt(mem_used as f64, short, "B");
let total_str = _sizeof_fmt(mem_data.total as f64, short, "B");
let formatted = if format.matches("%s").count() >= 2 {
sprintf_two_strings(format, &used_str, &total_str)
} else {
sprintf_two_strings(format, &used_str, "")
};
vec![json!({
"contents": formatted,
"gradient_level": mem_percentage,
"highlight_groups": ["mem_usage_gradient", "mem_usage"],
"divider_highlight_group": "background:divider",
})]
}
pub fn mem_swap_percentage(format: &str, mem_type: &str) -> Vec<Value> {
let mem_data = swap_memory();
let mem_used = mem_data._get_mem_used(mem_type);
let mem_percentage = if mem_data.total > 0 {
(mem_used as f64 / mem_data.total as f64) * 100.0
} else {
0.0
};
let contents = sprintf_one_float(format, mem_percentage);
vec![json!({
"contents": contents,
"gradient_level": mem_percentage,
"highlight_groups": ["mem_usage_gradient", "mem_usage"],
"divider_highlight_group": "background:divider",
})]
}
fn sprintf_two_strings(format: &str, a: &str, b: &str) -> String {
let mut out = String::with_capacity(format.len() + a.len() + b.len());
let mut chars = format.chars().peekable();
let mut subs = [a, b].into_iter();
while let Some(c) = chars.next() {
if c == '%' {
match chars.peek() {
Some('s') => {
chars.next();
if let Some(arg) = subs.next() {
out.push_str(arg);
}
}
Some('%') => {
chars.next();
out.push('%');
}
_ => out.push(c),
}
} else {
out.push(c);
}
}
out
}
fn sprintf_one_float(format: &str, n: f64) -> String {
let bytes = format.as_bytes();
let mut out = String::with_capacity(format.len() + 8);
let mut i = 0;
let mut consumed = false;
while i < bytes.len() {
if bytes[i] == b'%' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'd' => {
if !consumed {
out.push_str(&format!("{}", n as i64));
consumed = true;
}
i += 2;
continue;
}
b's' => {
if !consumed {
out.push_str(&format!("{}", n));
consumed = true;
}
i += 2;
continue;
}
b'%' => {
out.push('%');
i += 2;
continue;
}
b'.' => {
let mut j = i + 2;
let mut prec_str = String::new();
while j < bytes.len() && bytes[j].is_ascii_digit() {
prec_str.push(bytes[j] as char);
j += 1;
}
if j < bytes.len() && bytes[j] == b'f' {
let prec: usize = prec_str.parse().unwrap_or(1);
if !consumed {
out.push_str(&format!("{:.*}", prec, n));
consumed = true;
}
i = j + 1;
continue;
}
out.push('%');
i += 1;
continue;
}
_ => {
out.push('%');
i += 1;
continue;
}
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sizeof_fmt_zero_uses_d_format() {
assert_eq!(_sizeof_fmt(0.0, false, "B"), "0B");
assert_eq!(_sizeof_fmt(0.0, true, "B"), "0");
}
#[test]
fn sizeof_fmt_one_byte_under_kib() {
assert_eq!(_sizeof_fmt(512.0, false, "B"), "512.0B");
}
#[test]
fn sizeof_fmt_one_kib_long_form() {
assert_eq!(_sizeof_fmt(1024.0, false, "B"), "1.0KiB");
}
#[test]
fn sizeof_fmt_one_mib_short_form() {
assert_eq!(_sizeof_fmt(1024.0 * 1024.0, true, "B"), "1.0M");
}
#[test]
fn get_mem_used_dispatches_on_field_name() {
let s = MemStats {
total: 1000,
used: 700,
free: 200,
available: 300,
};
assert_eq!(s._get_mem_used("used"), 700);
assert_eq!(s._get_mem_used("free"), 200);
assert_eq!(s._get_mem_used("available"), 300);
assert_eq!(s._get_mem_used("total"), 1000);
assert_eq!(s._get_mem_used("frobnicate"), 700);
}
#[test]
fn sprintf_two_strings_handles_default_format() {
assert_eq!(
sprintf_two_strings("%s/%s", "512MiB", "8GiB"),
"512MiB/8GiB"
);
}
#[test]
fn sprintf_one_float_handles_percent_d_percent() {
assert_eq!(sprintf_one_float("%d%%", 73.0), "73%");
}
#[test]
fn sprintf_one_float_handles_precision_f() {
assert_eq!(sprintf_one_float("%.1f", 73.5), "73.5");
}
#[test]
fn sprintf_one_float_uses_bankers_rounding_matching_python() {
assert_eq!(sprintf_one_float("%.1f", 73.25), "73.2");
}
}