commit - 24205f2bf2d214fb410a04d3e30287d625cf3864
commit + bdd52244e14c41456084ff20178c90855bb16942
blob - 2db1edd5f7bf34e93bdd72ad4254cb88e7e6dbb3
blob + ea175b638a3b56d6cc7539d1620ed6a6cd7f6fbb
--- Makefile
+++ Makefile
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
# 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
# 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) &
# 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
//! 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<u64>;
+
+ 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::<u64>().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,
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
let mut uvm = MaybeUninit::<UvmExp>::zeroed();
let uvm_len = mem::size_of::<UvmExp>();
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,
std::ptr::null_mut(),
0,
);
- // Allocate enough space
let mut buf = vec![0u8; full_len];
libc::sysctl(
mib_uvm.as_ptr(),
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(),
}
}
-// ---------------------------------------------------------------------------
-// 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,
};
}
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;
}
}
-// ---------------------------------------------------------------------------
-// Network
-// ---------------------------------------------------------------------------
+// ===========================================================================
+// Network (portable — getifaddrs works on both)
+// ===========================================================================
fn read_network() -> String {
unsafe {
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;
}
if sa.is_null() {
continue;
}
- // IPv4 only
if (*sa).sa_family as i32 != libc::AF_INET {
continue;
}
}
}
-// ---------------------------------------------------------------------------
+// ===========================================================================
// Battery
-// ---------------------------------------------------------------------------
+// ===========================================================================
-fn read_battery() -> Option<ApmPowerInfo> {
+struct BatteryInfo {
+ pct: u8,
+ charging: bool,
+ minutes_left: Option<u32>,
+}
+
+#[cfg(target_os = "openbsd")]
+fn read_battery() -> Option<BatteryInfo> {
+ 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)
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<BatteryInfo> {
+ // 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<String> {
+ 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<u32> {
+ std::fs::read_to_string(path)
+ .ok()?
+ .trim()
+ .parse()
+ .ok()
+}
+
+#[cfg(target_os = "linux")]
+fn estimate_time_remaining(base: &str) -> Option<u32> {
+ 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 {
}
}
-// ---------------------------------------------------------------------------
+// ===========================================================================
// 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) => {
}
}
-// ---------------------------------------------------------------------------
+// ===========================================================================
// 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
));
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
#[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),
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<extern "C" fn()>,
+}
+
unsafe extern "C" {
pub fn poll(
fds: *mut pollfd,
}
// 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());