From f2f476d246b08902817b53c2bdfffcc7a39e2991 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Sat, 31 Oct 2026 02:07:56 -0400 Subject: [PATCH] lib: add machine and swap rows Signed-off-by: Amaan Qureshi Change-Id: I20d4bab4d29af803ba43b012b373f8846a6a6964 --- crates/asm/src/darwin.rs | 29 ++++ crates/benchmarks/benches/microfetch.rs | 13 +- crates/lib/src/lib.rs | 80 ++++++----- crates/lib/src/release.rs | 60 +++++++++ crates/lib/src/system.rs | 168 +++++++++++++----------- 5 files changed, 238 insertions(+), 112 deletions(-) diff --git a/crates/asm/src/darwin.rs b/crates/asm/src/darwin.rs index be4c6ca..9159199 100644 --- a/crates/asm/src/darwin.rs +++ b/crates/asm/src/darwin.rs @@ -213,6 +213,35 @@ pub fn macos_meminfo() -> Option<(u64, u64)> { Some((used.min(total), total)) } +/// Mirror of `struct xsw_usage` (``), the `vm.swapusage` value. +#[repr(C)] +#[derive(Default)] +struct XswUsage { + total: u64, + avail: u64, + used: u64, + pagesize: u32, + encrypted: i32, +} + +/// Returns `(used_bytes, total_bytes)` of swap from `vm.swapusage`. +#[must_use = "this only queries the kernel"] +pub fn macos_swapinfo() -> Option<(u64, u64)> { + let mut usage = XswUsage::default(); + let mut len = size_of::(); + let ret = unsafe { + sysctlbyname( + c"vm.swapusage".as_ptr(), + ptr::from_mut(&mut usage).cast::(), + &raw mut len, + ptr::null(), + 0, + ) + }; + (ret == 0_i32 && len == size_of::()) + .then_some((usage.used, usage.total)) +} + /// `struct timeval` (`time_t` is 64-bit, `suseconds_t` is 32-bit on macOS). #[repr(C)] #[derive(Default)] diff --git a/crates/benchmarks/benches/microfetch.rs b/crates/benchmarks/benches/microfetch.rs index df863b2..2bd0b46 100644 --- a/crates/benchmarks/benches/microfetch.rs +++ b/crates/benchmarks/benches/microfetch.rs @@ -8,7 +8,7 @@ use microfetch_lib::{ cpu, desktop, release, - system, + system::{self, Memory}, uptime, }; @@ -36,6 +36,14 @@ fn main_benchmark(criterion: &mut Criterion) { black_box(writer.written()); }); }); + criterion.bench_function("machine", |bencher| { + bencher.iter(|| { + let mut buf = [MaybeUninit::uninit(); 256]; + let mut writer = StackWriter::new(&mut buf); + release::write_machine(&mut writer); + black_box(writer.written()); + }); + }); criterion.bench_function("kernel_version", |bencher| { bencher.iter(|| { let mut buf = [MaybeUninit::uninit(); 256]; @@ -88,7 +96,8 @@ fn main_benchmark(criterion: &mut Criterion) { bencher.iter(|| { let mut buf = [MaybeUninit::uninit(); 256]; let mut writer = StackWriter::new(&mut buf); - system::write_memory_usage(&mut writer, &colors); + let memory = Memory::read(); + system::write_memory_usage(&mut writer, &colors, memory.as_ref()); black_box(writer.written()); }); }); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 1e0efc8..99ac2cf 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -22,6 +22,8 @@ use core::{ use microfetch_asm::{UtsNameBuf, sys_uname, sys_write}; +use crate::system::Memory; + /// A simple error type for microfetch operations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { @@ -252,7 +254,7 @@ impl<'buf> StackWriter<'buf> { } /// Custom logo art embedded at compile time via the `MICROFETCH_LOGO` -/// environment variable. Set it to 11 newline-separated lines of ASCII/Unicode +/// environment variable. Set it to 13 newline-separated lines of ASCII/Unicode /// art when building to replace the default NixOS logo: /// /// ```sh @@ -268,17 +270,19 @@ const CUSTOM_LOGO: &str = match option_env!("MICROFETCH_LOGO") { /// Packed logo rows. `0` separates rows; `1` and `2` select colors. const LOGO: &[u8] = concat!( - "\x01⠀⠀⠀⠀⠀⠀⢼⣿⣄⠀⠀⠀\x02⠹⣿⣷⡀⠀⣠⣿⡧⠀⠀⠀⠀⠀⠀\0", - "\x01⠀⠀⠀⠀⠀⠀⠈⢿⣿⣆⠀⠀⠀\x02⠘⣿⣿⣴⣿⡿⠁⠀⠀⠀⠀⠀⠀\0", - "\x01⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡜\x02⢿⣿⣟⠀⠀⠀\x01⢀⡄⠀⠀⠀\0", - "\x01⠀⠀⠀⠉⠉⠉⠉\x02⣩⣭⡭\x01⠉⠉⠉⠉⠉\x02⠈⢿⣿⣆⠀\x01⢠⣿⣿⠂⠀⠀\0", - "\x02⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢻⡟\x01⣡⣿⣿⠃⠀⠀⠀\0", - "\x02⢸⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\x01⣰⣿⣿⣿⣿⣿⣿⡇\0", - "\x02⠀⠀⠀⢠⣿⣿⢋\x01⣼⣧⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀\0", - "\x02⠀⠀⠠⣿⣿⠃⠀\x01⠹⣿⣷⡀\x02⣀⣀⣀⣀⣀\x01⣚⣛⣋\x02⣀⣀⣀⣀⠀⠀⠀\0", - "\x02⠀⠀⠀⠘⠁⠀⠀⠀\x01⣽⣿⣷⡜\x02⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀\0", - "\x01⠀⠀⠀⠀⠀⠀⢀⣾⣿⠟⣿⣿⡄⠀⠀⠀\x02⠹⣿⣷⡀⠀⠀⠀⠀⠀⠀\0", - "\x01⠀⠀⠀⠀⠀⠀⢺⣿⠋⠀⠈⢿⣿⣆⠀⠀⠀\x02⠙⣿⡗⠀⠀⠀⠀⠀⠀", + "\x01⠀⠀⠀⠀⠀⠀⠀⣼⣿⣦⠀⠀⠀⠀\x02⢿⣿⣷⡄⠀⢀⣴⣿⣦⠀⠀⠀⠀⠀⠀⠀\0", + "\x01⠀⠀⠀⠀⠀⠀⠀⠙⣿⣿⣷⡀⠀⠀\x02⠈⢻⣿⣿⣆⣾⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀\0", + "\x01⠀⠀⠀⠀⢠⣤⣤⣤⣼⣿⣿⣷⣤⣤⣤⣤\x02⢻⣿⣿⣿⡿⠁⠀⠀⠀\x01⡀⠀⠀⠀⠀\0", + "\x01⠀⠀⠀⠠⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠷\x02⠹⣿⣿⣧⠀⠀\x01⢀⣼⣷⡄⠀⠀⠀\0", + "\x02⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠃⠀⠀⠀⠀⠀⠀⠹⣿⣿⣧\x01⢀⣾⣿⣿⠃⠀⠀⠀\0", + "\x02⠀⣀⣀⣀⣀⣀⣠⣾⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⠏\x01⣿⣿⣿⣁⣀⣀⣀⠀\0", + "\x02⠰⣿⣿⣿⣿⣿⣿⣿⡿\x01⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⠂\0", + "\x02⠀⠈⠉⠉⣹⣿⣿⡟\x01⣼⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀\0", + "\x02⠀⠀⠀⣰⣿⣿⡟⠀\x01⢻⣿⣿⣆⠀⠀⠀⠀⠀⠀⠴⠿⠿⠟⠀⠀⠀⠀⠀⠀⠀⠀\0", + "\x02⠀⠀⠀⠈⢿⠏⠀⠀⠀\x01⢻⣿⣿⣧\x02⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀\0", + "\x01⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣧\x02⠉⠉⠉⠉⢿⣿⣿⣏⠉⠉⠉⠀⠀⠀⠀⠀\0", + "\x01⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠙⣿⣿⣷⡀⠀⠀⠀\x02⢻⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀\0", + "\x01⠀⠀⠀⠀⠀⠀⠀⠹⠿⠏⠀⠀⠘⠿⠿⠷⠀⠀⠀⠀\x02⠻⠿⠏⠀⠀⠀⠀⠀⠀⠀", ) .as_bytes(); @@ -313,9 +317,10 @@ fn write_styled_row( } } -const ROW_COUNT: usize = 11; +const ROW_COUNT: usize = 13; const ROW_LABELS: &[u8] = concat!( "\x02\u{F313} \x01System\x03 \u{E621} \0", + "\x02\u{F108} \x01Machine\x03 \u{E621} \0", "\x02\u{E712} \x01Kernel\x03 \u{E621} \0", "\x02\u{F2DB} \x01CPU\x03 \u{E621} \0", "\x02\u{F4BC} \x01Topology\x03 \u{E621} \0", @@ -323,29 +328,22 @@ const ROW_LABELS: &[u8] = concat!( "\x02\u{F017} \x01Uptime\x03 \u{E621} \0", "\x02\u{F2D2} \x01Desktop\x03 \u{E621} \0", "\x02\u{F035B} \x01Memory\x03 \u{E621} \0", + "\x02\u{F04E1} \x01Swap\x03 \u{E621} \0", "\x02\u{F194E} \x01Storage (/)\x03 \u{E621} \0", "\x02\u{E22B} \x01Colors\x03 \u{E621} \0", ) .as_bytes(); -/// Write one row: logo + label + value. +/// Write the label and value of one row, after its logo line. #[inline(never)] fn write_row( writer: &mut StackWriter, colors: &colors::Colors, row: usize, - custom_logo: Option<&str>, - logo: &mut &[u8], labels: &mut &[u8], utsname: &UtsName, + memory: Option<&Memory>, ) { - writer.push_str(" "); - if let Some(logo_text) = custom_logo { - writer.push_str(colors.cyan); - writer.push_str(logo_text); - } else { - write_styled_row(writer, colors, logo); - } writer.push_str(colors.reset); writer.push_str(" "); if row != 0 { @@ -358,14 +356,16 @@ fn write_row( writer.push_str(colors.reset); }, 1 => release::write_os_pretty_name(writer), - 2 => release::write_system_info(writer, utsname), - 3 => cpu::write_cpu_name(writer), - 4 => cpu::write_cpu_cores(writer), - 5 => system::write_shell(writer), - 6 => uptime::write(writer), - 7 => desktop::write_desktop_info(writer), - 8 => system::write_memory_usage(writer, colors), - 9 => system::write_root_disk_usage(writer, colors), + 2 => release::write_machine(writer), + 3 => release::write_system_info(writer, utsname), + 4 => cpu::write_cpu_name(writer), + 5 => cpu::write_cpu_cores(writer), + 6 => system::write_shell(writer), + 7 => uptime::write(writer), + 8 => desktop::write_desktop_info(writer), + 9 => system::write_memory_usage(writer, colors, memory), + 10 => system::write_swap_usage(writer, colors, memory), + 11 => system::write_root_disk_usage(writer, colors), _ => colors::write_dots(writer, colors), } writer.push_byte(b'\n'); @@ -429,14 +429,15 @@ pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { } let utsname = UtsName::uname()?; + let memory = Memory::read(); let no_color = colors::is_no_color(); let colors = colors::Colors::new(no_color); - let mut buf = [MaybeUninit::uninit(); 2560]; + let mut buf = [MaybeUninit::uninit(); 3072]; let mut writer = StackWriter::new(&mut buf); - // Custom logo is 11 lines from MICROFETCH_LOGO env var, one per info row. - // Lines beyond 11 are ignored; missing lines render as empty. + // Custom logo is 13 lines from MICROFETCH_LOGO env var, one per info row. + // Lines beyond 13 are ignored; missing lines render as empty. let custom_lines: [&str; ROW_COUNT]; let use_custom = !CUSTOM_LOGO.is_empty(); let logo_lines = if use_custom { @@ -452,14 +453,21 @@ pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { writer.push_byte(b'\n'); for (row, &line) in logo_lines.iter().enumerate() { + writer.push_str(" "); + if use_custom { + writer.push_str(colors.cyan); + writer.push_str(line); + } else { + write_styled_row(&mut writer, &colors, &mut logo); + } + write_row( &mut writer, &colors, row, - use_custom.then_some(line), - &mut logo, &mut labels, &utsname, + memory.as_ref(), ); } diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index eb87b65..3705d8c 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -85,3 +85,63 @@ pub fn write_os_pretty_name(writer: &mut StackWriter) { writer.push_str("Unknown"); } + +/// Writes the hardware model from `hw.model` (macOS), e.g. `Mac14,2`. +#[cfg(target_os = "macos")] +#[cfg_attr(feature = "hotpath", hotpath::measure)] +pub fn write_machine(writer: &mut StackWriter) { + use microfetch_asm::darwin::macos_sysctl_str; + + let mut buf = [0_u8; 64]; + match macos_sysctl_str(b"hw.model\0", &mut buf) { + Some(len) if len > 0 => writer.push_bytes(&buf[..len]), + _ => writer.push_str("Unknown"), + } +} + +/// Writes the hardware model from the DMI product or board name, or the +/// devicetree model on boards without DMI. +#[cfg(target_os = "linux")] +#[cfg_attr(feature = "hotpath", hotpath::measure)] +pub fn write_machine(writer: &mut StackWriter) { + // https://github.com/fastfetch-cli/fastfetch/blob/2.70.0/src/common/impl/smbios.c#L7-L47 + const PLACEHOLDERS: &[u8] = concat!( + "*to be filled\0*to be set\0*oem\0*o.e.m.\0*system product\0", + "*unknown product\0none\0system name\0system version\0default string\0", + "undefined\0not specified\0not applicable\0not defined\0not available\0", + "invalid\0type1productconfigid\0all series\0n/a\0unknown\0standard", + ) + .as_bytes(); + + let mut buffer = [0_u8; 128]; + + for path in [ + "/sys/devices/virtual/dmi/id/product_name", + "/sys/devices/virtual/dmi/id/board_name", + "/sys/firmware/devicetree/base/model", + ] { + let Ok(len) = read_file_fast(path, &mut buffer) else { + continue; + }; + let raw = &buffer[..len]; + let value = raw.strip_suffix(b"\0").unwrap_or(raw).trim_ascii(); + + let placeholder = value.is_empty() + || PLACEHOLDERS.split(|&byte| byte == 0).any(|entry| { + let (pattern, is_prefix) = entry + .strip_prefix(b"*") + .map_or((entry, false), |pattern| (pattern, true)); + (is_prefix || value.len() == pattern.len()) + && value + .get(..pattern.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(pattern)) + }); + + if !placeholder { + writer.push_bytes(value); + return; + } + } + + writer.push_str("Unknown"); +} diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index 19c2e13..d5abb51 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -78,11 +78,6 @@ fn write_gib(writer: &mut StackWriter, bytes: u64) { write_centi_gib(writer, (bytes * 100 + (1 << 29)) >> 30); } -#[cfg(target_os = "linux")] -fn write_kb_as_gib(writer: &mut StackWriter, kb: u64) { - write_centi_gib(writer, (kb * 100 + (1 << 19)) >> 20); -} - /// Fast integer parsing without stdlib overhead. #[cfg(target_os = "linux")] #[inline] @@ -98,86 +93,82 @@ fn parse_u64_fast(bytes: &[u8]) -> u64 { result } -/// Writes the system memory usage information via `sysctl`/Mach (macOS). -/// Writes nothing if the memory statistics cannot be retrieved. -#[cfg(target_os = "macos")] -#[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn write_memory_usage(writer: &mut StackWriter, colors: &Colors) { - use microfetch_asm::darwin::macos_meminfo; - - let Some((used_bytes, total_bytes)) = macos_meminfo() else { - return; - }; - let percentage_used = (used_bytes * 100 + total_bytes / 2) - .checked_div(total_bytes) - .unwrap_or(0); - - write_gib(writer, used_bytes); - writer.push_str(" GiB / "); - write_gib(writer, total_bytes); - writer.push_str(" GiB ("); - writer.push_str(colors.cyan); - writer.push_u64(percentage_used); - writer.push_byte(b'%'); - writer.push_str(colors.reset); - writer.push_byte(b')'); +/// RAM and swap usage in bytes. +pub struct Memory { + used: u64, + total: u64, + swap_used: u64, + swap_total: u64, } -/// Gets the system memory usage information. -/// Writes nothing if `/proc/meminfo` cannot be read. -#[cfg(target_os = "linux")] -#[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn write_memory_usage(writer: &mut StackWriter, colors: &Colors) { +impl Memory { + /// Reads RAM and swap usage via `sysctl`/Mach (macOS). + #[cfg(target_os = "macos")] #[cfg_attr(feature = "hotpath", hotpath::measure)] - fn parse_memory_info() -> Option<(u64, u64)> { - let mut total_memory_kb = 0_u64; - let mut available_memory_kb = 0_u64; - let mut buffer = [0_u8; 1024]; + #[must_use = "this only queries the kernel"] + pub fn read() -> Option { + use microfetch_asm::darwin::{macos_meminfo, macos_swapinfo}; + + let (used, total) = macos_meminfo()?; + let (swap_used, swap_total) = macos_swapinfo().unwrap_or((0, 0)); + Some(Self { + used, + total, + swap_used, + swap_total, + }) + } - // Use fast syscall-based file reading + /// Reads RAM and swap usage from `/proc/meminfo`. + #[cfg(target_os = "linux")] + #[cfg_attr(feature = "hotpath", hotpath::measure)] + #[must_use = "this only reads /proc/meminfo"] + pub fn read() -> Option { + let mut buffer = [0_u8; 1024]; let bytes_read = read_file_fast("/proc/meminfo", &mut buffer).ok()?; - let meminfo = &buffer[..bytes_read]; - - // Fast scanning for MemTotal and MemAvailable - let mut offset = 0; - let mut found_total = false; - let mut found_available = false; - - while offset < meminfo.len() && (!found_total || !found_available) { - let remaining = &meminfo[offset..]; - - // Find newline or end - let line_end = remaining - .iter() - .position(|&byte| byte == b'\n') - .unwrap_or(remaining.len()); - let line = &remaining[..line_end]; - - if let Some(value) = line.strip_prefix(b"MemTotal:") { - total_memory_kb = parse_u64_fast(value.trim_ascii_start()); - found_total = true; - } - if let Some(value) = line.strip_prefix(b"MemAvailable:") { - available_memory_kb = parse_u64_fast(value.trim_ascii_start()); - found_available = true; - } - - offset += line_end + 1; + + let mut mem_total = 0_u64; + let mut mem_available = 0_u64; + let mut swap_total = 0_u64; + let mut swap_free = 0_u64; + + for line in buffer[..bytes_read].split(|&byte| byte == b'\n') { + let (field, rest) = if let Some(rest) = line.strip_prefix(b"MemTotal:") { + (&mut mem_total, rest) + } else if let Some(rest) = line.strip_prefix(b"MemAvailable:") { + (&mut mem_available, rest) + } else if let Some(rest) = line.strip_prefix(b"SwapTotal:") { + (&mut swap_total, rest) + } else if let Some(rest) = line.strip_prefix(b"SwapFree:") { + swap_free = parse_u64_fast(rest.trim_ascii_start()); + break; + } else { + continue; + }; + *field = parse_u64_fast(rest.trim_ascii_start()); } - Some((total_memory_kb - available_memory_kb, total_memory_kb)) + Some(Self { + used: (mem_total - mem_available) << 10, + total: mem_total << 10, + swap_used: (swap_total - swap_free) << 10, + swap_total: swap_total << 10, + }) } +} - let Some((used_kb, total_kb)) = parse_memory_info() else { - return; - }; - let percentage_used = (used_kb * 100 + total_kb / 2) - .checked_div(total_kb) - .unwrap_or(0); +fn write_usage( + writer: &mut StackWriter, + colors: &Colors, + used: u64, + total: u64, +) { + let percentage_used = + (used * 100 + total / 2).checked_div(total).unwrap_or(0); - write_kb_as_gib(writer, used_kb); + write_gib(writer, used); writer.push_str(" GiB / "); - write_kb_as_gib(writer, total_kb); + write_gib(writer, total); writer.push_str(" GiB ("); writer.push_str(colors.cyan); writer.push_u64(percentage_used); @@ -185,3 +176,32 @@ pub fn write_memory_usage(writer: &mut StackWriter, colors: &Colors) { writer.push_str(colors.reset); writer.push_byte(b')'); } + +/// Writes RAM usage, or nothing if it could not be read. +pub fn write_memory_usage( + writer: &mut StackWriter, + colors: &Colors, + memory: Option<&Memory>, +) { + let Some(usage) = memory else { + return; + }; + write_usage(writer, colors, usage.used, usage.total); +} + +/// Writes swap usage, `Disabled` when there is no swap, or nothing if it could +/// not be read. +pub fn write_swap_usage( + writer: &mut StackWriter, + colors: &Colors, + memory: Option<&Memory>, +) { + let Some(usage) = memory else { + return; + }; + if usage.swap_total == 0 { + writer.push_str("Disabled"); + return; + } + write_usage(writer, colors, usage.swap_used, usage.swap_total); +}