commit bdd52244e14c41456084ff20178c90855bb16942 from: Murilo Ijanc date: Sun Apr 5 02:32:16 2026 UTC add Linux support for ostatus, fix SigAction struct, and make Makefile portable The SigAction struct had the wrong layout for Linux (sigset_t is 128 bytes, not 4), which would cause memory corruption. ostatus now reads CPU from /proc/stat, memory from /proc/meminfo, and battery from /sys/class/power_supply/ on Linux. Makefile auto-detects doas or sudo. commit - 24205f2bf2d214fb410a04d3e30287d625cf3864 commit + bdd52244e14c41456084ff20178c90855bb16942 blob - 2db1edd5f7bf34e93bdd72ad4254cb88e7e6dbb3 blob + ea175b638a3b56d6cc7539d1620ed6a6cd7f6fbb --- Makefile +++ Makefile @@ -1,6 +1,7 @@ CARGO = cargo DISPLAY_NUM = :2 XEPHYR_SIZE = 1280x800 +PRIV ?= $(shell command -v doas 2>/dev/null || echo sudo) OWM = ./target/debug/owm OWM_RELEASE = ./target/release/owm @@ -19,8 +20,8 @@ clean: # Launch Xephyr + owm for testing inside your current X session. # Ctrl+Shift kills Xephyr when done. run: build - doas cp ./target/debug/omenu /usr/local/bin/ - doas cp ./target/debug/ostatus /usr/local/bin/ + $(PRIV) cp ./target/debug/omenu /usr/local/bin/ + $(PRIV) cp ./target/debug/ostatus /usr/local/bin/ @echo "Starting Xephyr on $(DISPLAY_NUM) ($(XEPHYR_SIZE))..." Xephyr $(DISPLAY_NUM) -screen $(XEPHYR_SIZE) -ac & @sleep 1 @@ -31,7 +32,7 @@ run: build # Same but with release build. run-release: release - doas cp ./target/release/omenu ./target/release/ostatus /usr/local/bin + $(PRIV) cp ./target/release/omenu ./target/release/ostatus /usr/local/bin Xephyr $(DISPLAY_NUM) -screen $(XEPHYR_SIZE) -ac & @sleep 1 DISPLAY=$(DISPLAY_NUM) RUST_LOG=info $(OWM_RELEASE) & @@ -39,10 +40,10 @@ run-release: release # DISPLAY=$(DISPLAY_NUM) xterm & install: release - doas install -m 755 target/release/owm /usr/local/bin/owm - doas install -m 755 target/release/owm-msg /usr/local/bin/owm-msg - doas install -m 755 target/release/omenu /usr/local/bin/omenu - doas install -m 755 target/release/ostatus /usr/local/bin/ostatus + $(PRIV) install -m 755 target/release/owm /usr/local/bin/owm + $(PRIV) install -m 755 target/release/owm-msg /usr/local/bin/owm-msg + $(PRIV) install -m 755 target/release/omenu /usr/local/bin/omenu + $(PRIV) install -m 755 target/release/ostatus /usr/local/bin/ostatus # Copy freshly built debug binaries without restarting. update: build blob - 38555e538370be37e2df9b4b3f655723fd3294cd blob + 5295e177b6292a72c999e379b84d6303fb632edd --- src/bin/ostatus.rs +++ src/bin/ostatus.rs @@ -5,129 +5,103 @@ //! 2. Repeated status lines: [{"text": "...", "name": "...", ...}, ...] //! //! Modules: cpu, memory, disk, network, battery, datetime. -//! Designed for OpenBSD; uses sysctl(2), statvfs(2), ioctl(2), getifaddrs(3). +//! Uses sysctl/APM on OpenBSD, /proc + /sys on Linux. use std::ffi::CString; use std::io::{self, Write}; -use std::mem::{self, MaybeUninit}; +use std::mem::MaybeUninit; use std::thread; use std::time::Duration; -// --------------------------------------------------------------------------- -// OpenBSD sysctl constants -// --------------------------------------------------------------------------- - -const CTL_KERN: libc::c_int = 1; -const CTL_HW: libc::c_int = 6; -const CTL_VM: libc::c_int = 2; - -const KERN_CPTIME: libc::c_int = 40; -const HW_PHYSMEM64: libc::c_int = 19; -const VM_UVMEXP: libc::c_int = 4; - -// CPU states (from sys/sched.h) -const _CP_USER: usize = 0; -const _CP_NICE: usize = 1; -const _CP_SYS: usize = 2; -const _CP_SPIN: usize = 3; -const _CP_INTR: usize = 4; -const CP_IDLE: usize = 5; -const CPUSTATES: usize = 6; - -// APM (battery) ioctl -// APM_IOC_GETPOWER = _IOR('A', 3, struct apm_power_info) -// sizeof(apm_power_info) = 32 -const APM_IOC_GETPOWER: libc::c_ulong = 0x40204103; - -const APM_AC_ON: u8 = 0x01; -const APM_BATT_CHARGING: u8 = 0x03; -const APM_BATT_ABSENT: u8 = 0x04; -const APM_BATT_UNKNOWN: u8 = 0xff; - -// --------------------------------------------------------------------------- -// Structs matching kernel definitions -// --------------------------------------------------------------------------- - -/// Partial uvmexp — we only need the first few fields. -#[repr(C)] -struct UvmExp { - pagesize: i32, - pagemask: i32, - pageshift: i32, - npages: i32, - free: i32, - active: i32, - inactive: i32, - paging: i32, - wired: i32, - // rest unused -} - -#[repr(C)] -struct ApmPowerInfo { - battery_state: u8, - ac_state: u8, - battery_life: u8, - spare1: u8, - minutes_left: u32, - spare2: [u32; 6], -} - -// --------------------------------------------------------------------------- -// sysctl helper -// --------------------------------------------------------------------------- - -unsafe fn sysctl_raw(mib: &[libc::c_int], buf: *mut u8, len: &mut usize) -> bool { - unsafe { - libc::sysctl( - mib.as_ptr(), - mib.len() as libc::c_uint, - buf as *mut libc::c_void, - len, - std::ptr::null_mut(), - 0, - ) == 0 - } -} - -// --------------------------------------------------------------------------- +// =========================================================================== // CPU -// --------------------------------------------------------------------------- +// =========================================================================== -type CpTime = [i64; CPUSTATES]; +#[cfg(target_os = "openbsd")] +mod cpu { + use std::mem; -fn read_cp_time() -> CpTime { - let mib = [CTL_KERN, KERN_CPTIME]; - let mut cp = [0i64; CPUSTATES]; - let mut len = mem::size_of_val(&cp); - unsafe { - sysctl_raw( - &mib, - cp.as_mut_ptr() as *mut u8, - &mut len, - ); + const CTL_KERN: libc::c_int = 1; + const KERN_CPTIME: libc::c_int = 40; + const CP_IDLE: usize = 5; + const CPUSTATES: usize = 6; + + pub type CpTime = [i64; CPUSTATES]; + + pub fn read() -> CpTime { + let mib = [CTL_KERN, KERN_CPTIME]; + let mut cp = [0i64; CPUSTATES]; + let mut len = mem::size_of_val(&cp); + unsafe { + libc::sysctl( + mib.as_ptr(), + mib.len() as libc::c_uint, + cp.as_mut_ptr() as *mut libc::c_void, + &mut len, + std::ptr::null_mut(), + 0, + ); + } + cp } - cp + + pub fn usage(prev: &CpTime, cur: &CpTime) -> u32 { + let mut total: i64 = 0; + let mut idle_delta: i64 = 0; + for i in 0..CPUSTATES { + let d = cur[i] - prev[i]; + total += d; + if i == CP_IDLE { + idle_delta = d; + } + } + if total == 0 { + return 0; + } + (((total - idle_delta) * 100) / total) as u32 + } } -fn cpu_usage(prev: &CpTime, cur: &CpTime) -> u32 { - let mut delta = [0i64; CPUSTATES]; - let mut total: i64 = 0; - for i in 0..CPUSTATES { - delta[i] = cur[i] - prev[i]; - total += delta[i]; +#[cfg(target_os = "linux")] +mod cpu { + /// Cumulative jiffies: [user, nice, system, idle, iowait, irq, softirq, ...] + pub type CpTime = Vec; + + pub fn read() -> CpTime { + let data = std::fs::read_to_string("/proc/stat").unwrap_or_default(); + // First line: "cpu user nice system idle iowait irq softirq ..." + let line = data.lines().next().unwrap_or(""); + line.split_whitespace() + .skip(1) // skip "cpu" + .filter_map(|s| s.parse::().ok()) + .collect() } - if total == 0 { - return 0; + + pub fn usage(prev: &CpTime, cur: &CpTime) -> u32 { + let len = prev.len().min(cur.len()); + if len < 4 { + return 0; + } + let mut total: u64 = 0; + let mut idle_delta: u64 = 0; + for i in 0..len { + let d = cur[i].saturating_sub(prev[i]); + total += d; + // index 3 = idle, index 4 = iowait + if i == 3 || i == 4 { + idle_delta += d; + } + } + if total == 0 { + return 0; + } + (((total - idle_delta) * 100) / total) as u32 } - let idle = delta[CP_IDLE]; - let busy = total - idle; - ((busy * 100) / total) as u32 } -// --------------------------------------------------------------------------- +// =========================================================================== // Memory -// --------------------------------------------------------------------------- +// =========================================================================== struct MemInfo { used_mb: u64, @@ -135,13 +109,41 @@ struct MemInfo { pct: u32, } +#[cfg(target_os = "openbsd")] fn read_memory() -> MemInfo { + use std::mem; + + const CTL_HW: libc::c_int = 6; + const CTL_VM: libc::c_int = 2; + const HW_PHYSMEM64: libc::c_int = 19; + const VM_UVMEXP: libc::c_int = 4; + + #[repr(C)] + struct UvmExp { + pagesize: i32, + pagemask: i32, + pageshift: i32, + npages: i32, + free: i32, + active: i32, + inactive: i32, + paging: i32, + wired: i32, + } + // Total physical memory via hw.physmem64 let mib_phys = [CTL_HW, HW_PHYSMEM64]; let mut physmem: i64 = 0; let mut len = mem::size_of_val(&physmem); unsafe { - sysctl_raw(&mib_phys, &mut physmem as *mut i64 as *mut u8, &mut len); + libc::sysctl( + mib_phys.as_ptr(), + mib_phys.len() as libc::c_uint, + &mut physmem as *mut i64 as *mut libc::c_void, + &mut len, + std::ptr::null_mut(), + 0, + ); } // Active/wired pages via vm.uvmexp @@ -149,11 +151,7 @@ fn read_memory() -> MemInfo { let mut uvm = MaybeUninit::::zeroed(); let uvm_len = mem::size_of::(); unsafe { - // uvmexp struct is larger than our partial definition, but sysctl - // will fill only up to uvm_len bytes, which is fine. - // We need to pass the full struct size the kernel expects. let mut full_len: usize = 0; - // First query the size libc::sysctl( mib_uvm.as_ptr(), mib_uvm.len() as libc::c_uint, @@ -162,7 +160,6 @@ fn read_memory() -> MemInfo { std::ptr::null_mut(), 0, ); - // Allocate enough space let mut buf = vec![0u8; full_len]; libc::sysctl( mib_uvm.as_ptr(), @@ -172,7 +169,6 @@ fn read_memory() -> MemInfo { std::ptr::null_mut(), 0, ); - // Copy the part we care about let copy_len = uvm_len.min(full_len); std::ptr::copy_nonoverlapping( buf.as_ptr(), @@ -201,10 +197,50 @@ fn read_memory() -> MemInfo { } } -// --------------------------------------------------------------------------- -// Disk -// --------------------------------------------------------------------------- +#[cfg(target_os = "linux")] +fn read_memory() -> MemInfo { + let data = std::fs::read_to_string("/proc/meminfo").unwrap_or_default(); + let mut total_kb: u64 = 0; + let mut available_kb: u64 = 0; + + for line in data.lines() { + if let Some(val) = line.strip_prefix("MemTotal:") { + total_kb = parse_meminfo_kb(val); + } else if let Some(val) = line.strip_prefix("MemAvailable:") { + available_kb = parse_meminfo_kb(val); + } + } + + let used_kb = total_kb.saturating_sub(available_kb); + let total_mb = total_kb / 1024; + let used_mb = used_kb / 1024; + let pct = if total_kb > 0 { + ((used_kb * 100) / total_kb) as u32 + } else { + 0 + }; + + MemInfo { + used_mb, + total_mb, + pct, + } +} + +#[cfg(target_os = "linux")] +fn parse_meminfo_kb(val: &str) -> u64 { + // " 12345 kB" + val.split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) +} + +// =========================================================================== +// Disk (portable — statvfs works on both) +// =========================================================================== + struct DiskInfo { used_gb: u64, total_gb: u64, @@ -223,9 +259,12 @@ fn read_disk(path: &str) -> DiskInfo { }; } let st = unsafe { st.assume_init() }; - let bsize = st.f_frsize; - let total = st.f_blocks * bsize; - let avail = st.f_bavail * bsize; + #[allow(clippy::unnecessary_cast)] + let bsize = st.f_frsize as u64; + #[allow(clippy::unnecessary_cast)] + let total = st.f_blocks as u64 * bsize; + #[allow(clippy::unnecessary_cast)] + let avail = st.f_bavail as u64 * bsize; let used = total - avail; let gb = 1024 * 1024 * 1024; @@ -240,9 +279,9 @@ fn read_disk(path: &str) -> DiskInfo { } } -// --------------------------------------------------------------------------- -// Network -// --------------------------------------------------------------------------- +// =========================================================================== +// Network (portable — getifaddrs works on both) +// =========================================================================== fn read_network() -> String { unsafe { @@ -257,11 +296,9 @@ fn read_network() -> String { let ifa = &*cur; cur = ifa.ifa_next; - // Skip loopback if (ifa.ifa_flags & libc::IFF_LOOPBACK as libc::c_uint) != 0 { continue; } - // Only UP interfaces if (ifa.ifa_flags & libc::IFF_UP as libc::c_uint) == 0 { continue; } @@ -270,7 +307,6 @@ fn read_network() -> String { if sa.is_null() { continue; } - // IPv4 only if (*sa).sa_family as i32 != libc::AF_INET { continue; } @@ -295,11 +331,34 @@ fn read_network() -> String { } } -// --------------------------------------------------------------------------- +// =========================================================================== // Battery -// --------------------------------------------------------------------------- +// =========================================================================== -fn read_battery() -> Option { +struct BatteryInfo { + pct: u8, + charging: bool, + minutes_left: Option, +} + +#[cfg(target_os = "openbsd")] +fn read_battery() -> Option { + const APM_IOC_GETPOWER: libc::c_ulong = 0x40204103; + const APM_AC_ON: u8 = 0x01; + const APM_BATT_CHARGING: u8 = 0x03; + const APM_BATT_ABSENT: u8 = 0x04; + const APM_BATT_UNKNOWN: u8 = 0xff; + + #[repr(C)] + struct ApmPowerInfo { + battery_state: u8, + ac_state: u8, + battery_life: u8, + spare1: u8, + minutes_left: u32, + spare2: [u32; 6], + } + let fd = unsafe { let path = CString::new("/dev/apm").unwrap(); libc::open(path.as_ptr(), libc::O_RDONLY) @@ -321,27 +380,95 @@ fn read_battery() -> Option { if info.battery_state == APM_BATT_ABSENT || info.battery_state == APM_BATT_UNKNOWN { return None; } - Some(info) -} -fn format_battery(info: &ApmPowerInfo) -> String { - let pct = info.battery_life; let charging = info.battery_state == APM_BATT_CHARGING || info.ac_state == APM_AC_ON; - let icon = if charging { "CHR" } else { "BAT" }; + let minutes = if !charging && info.minutes_left > 0 && info.minutes_left < 6000 { + Some(info.minutes_left) + } else { + None + }; + Some(BatteryInfo { + pct: info.battery_life, + charging, + minutes_left: minutes, + }) +} + +#[cfg(target_os = "linux")] +fn read_battery() -> Option { + // Try common battery paths + let base = find_battery_path()?; + + let capacity = read_sysfs_u32(&format!("{base}/capacity"))?; + let status = std::fs::read_to_string(format!("{base}/status")).unwrap_or_default(); + let charging = status.trim() == "Charging" || status.trim() == "Full"; + + let minutes_left = if !charging { + estimate_time_remaining(&base) + } else { + None + }; + + Some(BatteryInfo { + pct: capacity.min(100) as u8, + charging, + minutes_left, + }) +} + +#[cfg(target_os = "linux")] +fn find_battery_path() -> Option { + let dir = std::fs::read_dir("/sys/class/power_supply").ok()?; + for entry in dir.flatten() { + let path = entry.path(); + let type_path = path.join("type"); + if let Ok(t) = std::fs::read_to_string(&type_path) + && t.trim() == "Battery" + { + return Some(path.to_string_lossy().into_owned()); + } + } + None +} + +#[cfg(target_os = "linux")] +fn read_sysfs_u32(path: &str) -> Option { + std::fs::read_to_string(path) + .ok()? + .trim() + .parse() + .ok() +} + +#[cfg(target_os = "linux")] +fn estimate_time_remaining(base: &str) -> Option { + let energy_now = read_sysfs_u32(&format!("{base}/energy_now")) + .or_else(|| read_sysfs_u32(&format!("{base}/charge_now")))?; + let power_now = read_sysfs_u32(&format!("{base}/power_now")) + .or_else(|| read_sysfs_u32(&format!("{base}/current_now")))?; + if power_now == 0 { + return None; + } + // energy and power are in µWh and µW (or µAh and µA) + Some(((energy_now as u64 * 60) / power_now as u64) as u32) +} + +fn format_battery(info: &BatteryInfo) -> String { + let pct = info.pct; + let icon = if info.charging { "CHR" } else { "BAT" }; let mut s = format!("{icon}: {pct}%"); - // minutes_left is only meaningful when discharging and within sane range - if !charging && info.minutes_left > 0 && info.minutes_left < 6000 { - let h = info.minutes_left / 60; - let m = info.minutes_left % 60; + if let Some(min) = info.minutes_left { + let h = min / 60; + let m = min % 60; s.push_str(&format!(" ({h}:{m:02})")); } s } -// --------------------------------------------------------------------------- -// Date / Time -// --------------------------------------------------------------------------- +// =========================================================================== +// Date / Time (portable) +// =========================================================================== fn read_datetime() -> String { unsafe { @@ -364,12 +491,11 @@ fn read_datetime() -> String { } } -// --------------------------------------------------------------------------- +// =========================================================================== // JSON block formatting -// --------------------------------------------------------------------------- +// =========================================================================== fn block(name: &str, text: &str, color: Option<&str>) -> String { - // Escape text for JSON (handle quotes and backslashes) let text = text.replace('\\', "\\\\").replace('"', "\\\""); match color { Some(c) => { @@ -404,23 +530,22 @@ fn bat_color(pct: u8, charging: bool) -> Option<&'stat } } -// --------------------------------------------------------------------------- +// =========================================================================== // Main loop -// --------------------------------------------------------------------------- +// =========================================================================== fn main() { - // Protocol header println!(r#"{{"version":1}}"#); let mut stdout = io::stdout().lock(); - let mut prev_cp = read_cp_time(); + let mut prev_cp = cpu::read(); loop { thread::sleep(Duration::from_secs(1)); // CPU - let cur_cp = read_cp_time(); - let cpu_pct = cpu_usage(&prev_cp, &cur_cp); + let cur_cp = cpu::read(); + let cpu_pct = cpu::usage(&prev_cp, &cur_cp); prev_cp = cur_cp; // Memory @@ -457,14 +582,11 @@ fn main() { )); blocks.push(block("net", &net, None)); if let Some(ref info) = bat { - let charging = - info.battery_state == APM_BATT_CHARGING || info.ac_state == APM_AC_ON; let text = format_battery(info); - blocks.push(block("bat", &text, bat_color(info.battery_life, charging))); + blocks.push(block("bat", &text, bat_color(info.pct, info.charging))); } blocks.push(block("time", &dt, None)); - // Output JSON array let line = format!("[{}]", blocks.join(",")); if writeln!(stdout, "{line}").is_err() { break; blob - 288720471c38df1619997e0a2b160998fd81126f blob + e9026f3eff9cadacc1947c964296bde94767010c --- src/sys.rs +++ src/sys.rs @@ -29,7 +29,10 @@ type NfdsT = std::ffi::c_uint; #[cfg(target_os = "linux")] type NfdsT = std::ffi::c_ulong; -/// Minimal sigaction structure (POSIX). +/// Minimal sigaction structure. +/// +/// Layout differs between OpenBSD (sigset_t = u32) and Linux (sigset_t = 128 bytes). +#[cfg(target_os = "openbsd")] #[repr(C)] struct SigAction { sa_handler: extern "C" fn(std::ffi::c_int), @@ -37,6 +40,15 @@ struct SigAction { sa_flags: std::ffi::c_int, } +#[cfg(target_os = "linux")] +#[repr(C)] +struct SigAction { + sa_handler: extern "C" fn(std::ffi::c_int), + sa_mask: [std::ffi::c_ulong; 16], // sigset_t on Linux x86_64 + sa_flags: std::ffi::c_int, + sa_restorer: Option, +} + unsafe extern "C" { pub fn poll( fds: *mut pollfd, @@ -138,11 +150,19 @@ impl SignalPipe { } // Install signal handlers + #[cfg(target_os = "openbsd")] let sa = SigAction { sa_handler: signal_handler, sa_mask: 0, sa_flags: 0, }; + #[cfg(target_os = "linux")] + let sa = SigAction { + sa_handler: signal_handler, + sa_mask: [0; 16], + sa_flags: 0, + sa_restorer: None, + }; unsafe { sigaction(SIGHUP, &sa, std::ptr::null_mut()); sigaction(SIGTERM, &sa, std::ptr::null_mut());