From 29e4ccc80638a5392c7f1e9fa056b449d91db61a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 3 Aug 2024 20:42:48 +0300 Subject: [PATCH] modularize --- src/colors.rs | 6 ++ src/desktop.rs | 13 +++ src/main.rs | 215 +++---------------------------------------------- src/release.rs | 54 +++++++++++++ src/system.rs | 96 ++++++++++++++++++++++ src/uptime.rs | 42 ++++++++++ 6 files changed, 223 insertions(+), 203 deletions(-) create mode 100644 src/colors.rs create mode 100644 src/desktop.rs create mode 100644 src/release.rs create mode 100644 src/system.rs create mode 100644 src/uptime.rs diff --git a/src/colors.rs b/src/colors.rs new file mode 100644 index 0000000..f9b8bbc --- /dev/null +++ b/src/colors.rs @@ -0,0 +1,6 @@ +pub const RESET: &str = "\x1b[0m"; +pub const BLUE: &str = "\x1b[34m"; +pub const CYAN: &str = "\x1b[36m"; +pub const GREEN: &str = "\x1b[32m"; +pub const YELLOW: &str = "\x1b[33m"; +pub const RED: &str = "\x1b[31m"; diff --git a/src/desktop.rs b/src/desktop.rs new file mode 100644 index 0000000..273516d --- /dev/null +++ b/src/desktop.rs @@ -0,0 +1,13 @@ +use std::{env, io}; + +pub fn get_desktop_info() -> Result { + let desktop_env = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + let display_backend = env::var("XDG_SESSION_TYPE").unwrap_or_default(); + + // Trim "none+" from the start of desktop_env if present + // XXX: This is a workaround for NixOS modules that set XDG_CURRENT_DESKTOP to "none+foo" + // instead of just "foo" + let desktop_env = desktop_env.trim_start_matches("none+"); + + Ok(format!("{} ({})", desktop_env, display_backend)) +} diff --git a/src/main.rs b/src/main.rs index f7c0027..6ccbd23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,16 @@ -use color_eyre::{Report, Result}; -use libc::statfs as libc_statfs_struct; -use std::env; -use std::ffi::CString; -use std::fs::{self, File}; -use std::io::{self, BufRead, Read}; -use std::path::Path; -use std::process::Command; +mod colors; +mod desktop; +mod release; +mod system; +mod uptime; -const RESET: &str = "\x1b[0m"; -const BLUE: &str = "\x1b[34m"; -const CYAN: &str = "\x1b[36m"; -const GREEN: &str = "\x1b[32m"; -const YELLOW: &str = "\x1b[33m"; -const RED: &str = "\x1b[31m"; +use color_eyre::{Report, Result}; + +use crate::colors::{BLUE, CYAN, RESET}; +use crate::desktop::get_desktop_info; +use crate::release::{get_os_pretty_name, get_system_info}; +use crate::system::{get_disk_usage, get_memory_usage, get_username_and_hostname}; +use crate::uptime::get_system_uptime; fn main() -> Result<(), Report> { color_eyre::install()?; @@ -42,192 +40,3 @@ fn main() -> Result<(), Report> { Ok(()) } - -fn get_username_and_hostname() -> Result { - let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_string()); - let output = Command::new("hostname").output()?; - let hostname = String::from_utf8_lossy(&output.stdout).trim().to_string(); - - Ok(format!("{YELLOW}{}{RED}@{GREEN}{}", username, hostname)) -} - -fn get_disk_usage() -> Result { - let path = CString::new("/").expect("CString::new failed"); - - let mut fs_stat: libc_statfs_struct = unsafe { std::mem::zeroed() }; - let result = unsafe { libc::statfs(path.as_ptr(), &mut fs_stat) }; - - if result != 0 { - return Err(io::Error::last_os_error()); - } - - let block_size = fs_stat.f_bsize as u64; - let total_blocks = fs_stat.f_blocks as u64; - let free_blocks = fs_stat.f_bfree as u64; - - let total_size_bytes = total_blocks * block_size; - let used_size_bytes = (total_blocks - free_blocks) * block_size; - - let total_size_gib = total_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0); - let used_size_gib = used_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0); - let percentage_used = (used_size_bytes as f64 / total_size_bytes as f64) * 100.0; - - let formatted_total_size = format!("{:.2}", total_size_gib); - let formatted_used_size = format!("{:.2}", used_size_gib); - let formatted_percentage_used = format!("{:.0}", percentage_used); - - Ok(format!( - "{} GiB / {} GiB ({CYAN}{}%{RESET})", - formatted_used_size, formatted_total_size, formatted_percentage_used - )) -} - -fn get_memory_usage() -> Result { - fn parse_memory_info() -> Result<(f64, f64), io::Error> { - let path = Path::new("/proc/meminfo"); - let file = File::open(path)?; - let reader = io::BufReader::new(file); - - let mut total_memory_kb = 0.0; - let mut available_memory_kb = 0.0; - - for line in reader.lines() { - let line = line?; - if line.starts_with("MemTotal:") { - total_memory_kb = line - .split_whitespace() - .nth(1) - .ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "Failed to parse MemTotal") - })? - .parse::() - .unwrap_or(0.0); - } else if line.starts_with("MemAvailable:") { - available_memory_kb = line - .split_whitespace() - .nth(1) - .ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "Failed to parse MemAvailable") - })? - .parse::() - .unwrap_or(0.0); - } - } - - let total_memory_gb = total_memory_kb / (1024.0 * 1024.0); - let available_memory_gb = available_memory_kb / (1024.0 * 1024.0); - let used_memory_gb = total_memory_gb - available_memory_gb; - - Ok((used_memory_gb, total_memory_gb)) - } - - let (used_memory, total_memory) = parse_memory_info()?; - let percentage_used = (used_memory / total_memory * 100.0).round() as u64; - - Ok(format!( - "{:.2} GiB / {:.2} GiB ({CYAN}{}%{RESET})", - used_memory, total_memory, percentage_used - )) -} - -fn get_desktop_info() -> Result { - let desktop_env = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); - let display_backend = env::var("XDG_SESSION_TYPE").unwrap_or_default(); - - // Trim "none+" from the start of desktop_env if present - // XXX: This is a workaround for NixOS modules that set XDG_CURRENT_DESKTOP to "none+foo" - // instead of just "foo" - let desktop_env = desktop_env.trim_start_matches("none+"); - - Ok(format!("{} ({})", desktop_env, display_backend)) -} - -fn get_os_pretty_name() -> Option { - let os_release_content = fs::read_to_string("/etc/os-release").ok()?; - let os_release_lines: Vec<&str> = os_release_content.lines().collect(); - let pretty_name = os_release_lines - .iter() - .find(|line| line.starts_with("PRETTY_NAME=")) - .map(|line| { - line.trim_start_matches("PRETTY_NAME=") - .trim_matches('"') - .to_string() - }); - - pretty_name -} - -fn get_system_uptime() -> Result { - let path = Path::new("/proc/uptime"); - let file = File::open(path)?; - let reader = io::BufReader::new(file); - - let line = reader - .lines() - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to read uptime"))??; - - let uptime_seconds: f64 = line - .split_whitespace() - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to parse uptime"))? - .parse() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - // calculate days, hours, and minutes - let total_minutes = (uptime_seconds / 60.0).round() as u64; - let days = total_minutes / (60 * 24); - let hours = (total_minutes % (60 * 24)) / 60; - let minutes = total_minutes % 60; - - let mut parts = Vec::new(); - if days > 0 { - parts.push(format!("{} days", days)); - } - - if hours > 0 || days > 0 { - parts.push(format!("{} hours", hours)); - } - - if minutes > 0 || hours > 0 || days > 0 { - parts.push(format!("{} minutes", minutes)); - } - - Ok(parts.join(", ")) -} - -fn get_system_info() -> Result { - // Try to detect OS type as accurately as possible and without depending on uname. - // /etc/os-release should generally imply Linux, and /etc/bsd-release would imply BSD system. - fn detect_os() -> Result { - if fs::metadata("/etc/os-release").is_ok() || fs::metadata("/usr/lib/os-release").is_ok() { - Ok("Linux".to_string()) - } else if fs::metadata("/etc/rc.conf").is_ok() || fs::metadata("/etc/bsd-release").is_ok() { - Ok("BSD".to_string()) - } else { - Ok("Unknown".to_string()) - } - } - - let system = detect_os()?; - - let mut kernel_release = String::new(); - fs::File::open("/proc/sys/kernel/osrelease")?.read_to_string(&mut kernel_release)?; - let kernel_release = kernel_release.trim().to_string(); // Remove any trailing newline - - let mut cpuinfo = String::new(); - fs::File::open("/proc/cpuinfo")?.read_to_string(&mut cpuinfo)?; - - let architecture = if let Some(line) = cpuinfo.lines().find(|line| line.contains("flags")) { - if line.contains("lm") { - "x86_64".to_string() - } else { - "unknown".to_string() - } - } else { - "unknown".to_string() - }; - - let result = format!("{} {} ({})", system, kernel_release, architecture); - Ok(result) -} diff --git a/src/release.rs b/src/release.rs new file mode 100644 index 0000000..d501352 --- /dev/null +++ b/src/release.rs @@ -0,0 +1,54 @@ +use color_eyre::Result; +use std::fs::{self, read_to_string}; +use std::io::{self, Read}; + +// Try to detect OS type as accurately as possible and without depending on uname. +// /etc/os-release should generally imply Linux, and /etc/bsd-release would imply BSD system. +fn detect_os() -> Result { + if fs::metadata("/etc/os-release").is_ok() || fs::metadata("/usr/lib/os-release").is_ok() { + Ok("Linux".to_string()) + } else if fs::metadata("/etc/rc.conf").is_ok() || fs::metadata("/etc/bsd-release").is_ok() { + Ok("BSD".to_string()) + } else { + Ok("Unknown".to_string()) + } +} + +fn get_architecture() -> Result { + // Read architecture from /proc/sys/kernel/arch + let mut arch = String::new(); + fs::File::open("/proc/sys/kernel/arch")?.read_to_string(&mut arch)?; + let arch = arch.trim().to_string(); + Ok(arch) +} + +pub fn get_system_info() -> Result { + let system = detect_os()?; + + let mut kernel_release = String::new(); + fs::File::open("/proc/sys/kernel/osrelease")?.read_to_string(&mut kernel_release)?; + let kernel_release = kernel_release.trim().to_string(); + + let mut cpuinfo = String::new(); + fs::File::open("/proc/cpuinfo")?.read_to_string(&mut cpuinfo)?; + + let architecture = get_architecture()?; + + let result = format!("{} {} ({})", system, kernel_release, architecture); + Ok(result) +} + +pub fn get_os_pretty_name() -> Option { + let os_release_content = read_to_string("/etc/os-release").ok()?; + let os_release_lines: Vec<&str> = os_release_content.lines().collect(); + let pretty_name = os_release_lines + .iter() + .find(|line| line.starts_with("PRETTY_NAME=")) + .map(|line| { + line.trim_start_matches("PRETTY_NAME=") + .trim_matches('"') + .to_string() + }); + + pretty_name +} diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..584f801 --- /dev/null +++ b/src/system.rs @@ -0,0 +1,96 @@ +use libc::statfs as libc_statfs_struct; +use std::env; +use std::ffi::CString; +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::Path; +use std::process::Command; + +use crate::colors::{CYAN, GREEN, RED, RESET, YELLOW}; + +pub fn get_username_and_hostname() -> Result { + let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_string()); + let output = Command::new("hostname").output()?; + let hostname = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + Ok(format!("{YELLOW}{}{RED}@{GREEN}{}", username, hostname)) +} + +pub fn get_disk_usage() -> Result { + let path = CString::new("/").expect("CString::new failed"); + + let mut fs_stat: libc_statfs_struct = unsafe { std::mem::zeroed() }; + let result = unsafe { libc::statfs(path.as_ptr(), &mut fs_stat) }; + + if result != 0 { + return Err(io::Error::last_os_error()); + } + + let block_size = fs_stat.f_bsize as u64; + let total_blocks = fs_stat.f_blocks as u64; + let free_blocks = fs_stat.f_bfree as u64; + + let total_size_bytes = total_blocks * block_size; + let used_size_bytes = (total_blocks - free_blocks) * block_size; + + let total_size_gib = total_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0); + let used_size_gib = used_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0); + let percentage_used = (used_size_bytes as f64 / total_size_bytes as f64) * 100.0; + + let formatted_total_size = format!("{:.2}", total_size_gib); + let formatted_used_size = format!("{:.2}", used_size_gib); + let formatted_percentage_used = format!("{:.0}", percentage_used); + + Ok(format!( + "{} GiB / {} GiB ({CYAN}{}%{RESET})", + formatted_used_size, formatted_total_size, formatted_percentage_used + )) +} + +pub fn get_memory_usage() -> Result { + fn parse_memory_info() -> Result<(f64, f64), io::Error> { + let path = Path::new("/proc/meminfo"); + let file = File::open(path)?; + let reader = io::BufReader::new(file); + + let mut total_memory_kb = 0.0; + let mut available_memory_kb = 0.0; + + for line in reader.lines() { + let line = line?; + if line.starts_with("MemTotal:") { + total_memory_kb = line + .split_whitespace() + .nth(1) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Failed to parse MemTotal") + })? + .parse::() + .unwrap_or(0.0); + } else if line.starts_with("MemAvailable:") { + available_memory_kb = line + .split_whitespace() + .nth(1) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Failed to parse MemAvailable") + })? + .parse::() + .unwrap_or(0.0); + } + } + + let total_memory_gb = total_memory_kb / (1024.0 * 1024.0); + let available_memory_gb = available_memory_kb / (1024.0 * 1024.0); + let used_memory_gb = total_memory_gb - available_memory_gb; + + Ok((used_memory_gb, total_memory_gb)) + } + + let (used_memory, total_memory) = parse_memory_info()?; + let percentage_used = (used_memory / total_memory * 100.0).round() as u64; + + Ok(format!( + "{:.2} GiB / {:.2} GiB ({CYAN}{}%{RESET})", + used_memory, total_memory, percentage_used + )) +} diff --git a/src/uptime.rs b/src/uptime.rs new file mode 100644 index 0000000..407ff64 --- /dev/null +++ b/src/uptime.rs @@ -0,0 +1,42 @@ +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::Path; + +pub fn get_system_uptime() -> Result { + let path = Path::new("/proc/uptime"); + let file = File::open(path)?; + let reader = io::BufReader::new(file); + + let line = reader + .lines() + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to read uptime"))??; + + let uptime_seconds: f64 = line + .split_whitespace() + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to parse uptime"))? + .parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // calculate days, hours, and minutes + let total_minutes = (uptime_seconds / 60.0).round() as u64; + let days = total_minutes / (60 * 24); + let hours = (total_minutes % (60 * 24)) / 60; + let minutes = total_minutes % 60; + + let mut parts = Vec::new(); + if days > 0 { + parts.push(format!("{} days", days)); + } + + if hours > 0 || days > 0 { + parts.push(format!("{} hours", hours)); + } + + if minutes > 0 || hours > 0 || days > 0 { + parts.push(format!("{} minutes", minutes)); + } + + Ok(parts.join(", ")) +}