Commit Diff


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<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,
@@ -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::<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,
@@ -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<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)
@@ -321,27 +380,95 @@ fn read_battery() -> Option<ApmPowerInfo> {
     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 {
@@ -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<extern "C" fn()>,
+}
+
 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());