exc-launcher 0.2.0

A fast, lightweight terminal menu launcher for automating everyday commands and project-specific tasks
pub mod panel;

use sysinfo::{Disks, System};

#[derive(Debug, Clone)]
pub struct SystemInfo {
    pub user_at_host: String,
    pub os_display: String,
    pub host_model: String,
    pub kernel_version: String,
    pub uptime_secs: u64,
    pub mem_used_mb: u64,
    pub mem_total_mb: u64,
    pub disk_used_gb: u64,
    pub disk_total_gb: u64,
    pub disk_pct: u8,
    pub process_count: usize,
}

fn host_model() -> String {
    if cfg!(target_os = "macos")
        && let Ok(out) = std::process::Command::new("sysctl").args(["-n", "hw.model"]).output()
            && out.status.success() {
                let model = String::from_utf8_lossy(&out.stdout).trim().to_string();
                if !model.is_empty() {
                    return model;
                }
            }
    System::host_name().unwrap_or_else(|| "unknown".to_string())
}

fn current_user() -> String {
    std::env::var("USER")
        .or_else(|_| std::env::var("USERNAME"))
        .unwrap_or_else(|_| "user".to_string())
}

pub fn gather() -> SystemInfo {
    let mut sys = System::new_all();
    sys.refresh_all();

    let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
    let user_at_host = format!("{}@{}", current_user(), hostname);

    let mem_total_mb = sys.total_memory() / 1024 / 1024;
    let mem_used_mb = sys.used_memory() / 1024 / 1024;

    let disks = Disks::new_with_refreshed_list();
    let root_disk = disks
        .list()
        .iter()
        .find(|d| d.mount_point().to_string_lossy() == "/")
        .or_else(|| disks.list().first());

    let (disk_total_gb, disk_used_gb, disk_pct) = match root_disk {
        Some(d) => {
            let total = d.total_space();
            let available = d.available_space();
            let used = total.saturating_sub(available);
            let total_gb = total / 1024 / 1024 / 1024;
            let used_gb = used / 1024 / 1024 / 1024;
            let pct = if total > 0 { ((used as f64 / total as f64) * 100.0).round() as u8 } else { 0 };
            (total_gb, used_gb, pct)
        }
        None => (0, 0, 0),
    };

    let os_display = System::long_os_version().unwrap_or_else(|| {
        format!(
            "{} {}",
            System::name().unwrap_or_else(|| "unknown OS".to_string()),
            System::os_version().unwrap_or_default()
        )
        .trim()
        .to_string()
    });

    SystemInfo {
        user_at_host,
        os_display,
        host_model: host_model(),
        kernel_version: System::kernel_version().unwrap_or_default(),
        uptime_secs: System::uptime(),
        mem_used_mb,
        mem_total_mb,
        disk_used_gb,
        disk_total_gb,
        disk_pct,
        process_count: sys.processes().len(),
    }
}

pub fn format_uptime(secs: u64) -> String {
    let days = secs / 86400;
    let hours = (secs % 86400) / 3600;
    let minutes = (secs % 3600) / 60;
    if days > 0 {
        format!("{days}d {hours}h {minutes}m")
    } else if hours > 0 {
        format!("{hours}h {minutes}m")
    } else {
        format!("{minutes}m")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn format_uptime_various() {
        assert_eq!(format_uptime(59), "0m");
        assert_eq!(format_uptime(3661), "1h 1m");
        assert_eq!(format_uptime(2 * 86400 + 4 * 3600 + 24 * 60), "2d 4h 24m");
    }

    #[test]
    fn gather_produces_nonempty_fields() {
        let info = gather();
        assert!(!info.user_at_host.is_empty());
        assert!(!info.os_display.is_empty());
    }
}