feat: get all info

This commit is contained in:
neoarz
2026-01-06 20:58:05 -05:00
parent a1b7e755b2
commit f9937109b5
14 changed files with 624 additions and 21 deletions

100
src/helpers/battery.rs Normal file
View File

@@ -0,0 +1,100 @@
use std::process::Command;
pub fn get_battery_info() -> String {
let output = Command::new("ioreg")
.args(["-l", "-w0", "-r", "-c", "AppleSmartBattery"])
.output();
let stdout = match output {
Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(),
Err(_) => return "<unknown>".to_string(),
};
let mut device_name = "Built-in".to_string();
let mut current_capacity: Option<i32> = None;
let mut external_connected = false;
let mut is_charging = false;
let mut avg_time_to_empty: Option<i32> = None;
for line in stdout.lines() {
if line.contains("\"DeviceName\"") {
if let Some(equals_pos) = line.find('=') {
let value_part = &line[equals_pos + 1..].trim();
let value = value_part.trim_matches('"').trim_matches(';').trim();
if !value.is_empty() {
device_name = value.to_string();
}
}
} else if line.contains("\"CurrentCapacity\"") {
if let Some(equals_pos) = line.find('=') {
let value_part = &line[equals_pos + 1..].trim();
let value = value_part.trim_matches(';').trim();
if let Ok(capacity) = value.parse::<i32>() {
current_capacity = Some(capacity);
}
}
} else if line.contains("\"ExternalConnected\"") {
if let Some(equals_pos) = line.find('=') {
let value_part = &line[equals_pos + 1..].trim();
let value = value_part.trim_matches(';').trim();
external_connected = value == "Yes";
}
} else if line.contains("\"IsCharging\"") {
if let Some(equals_pos) = line.find('=') {
let value_part = &line[equals_pos + 1..].trim();
let value = value_part.trim_matches(';').trim();
is_charging = value == "Yes";
}
} else if line.contains("\"AvgTimeToEmpty\"") {
if let Some(equals_pos) = line.find('=') {
let value_part = &line[equals_pos + 1..].trim();
let value = value_part.trim_matches(';').trim();
if let Ok(time) = value.parse::<i32>() {
avg_time_to_empty = Some(time);
}
}
}
}
let percentage = if let Some(capacity) = current_capacity {
if capacity >= 0 && capacity <= 100 {
capacity as u32
} else {
return "<unknown>".to_string();
}
} else {
return "<unknown>".to_string();
};
let mut status = String::new();
if external_connected {
status.push_str("AC connected");
} else if is_charging {
status.push_str("Charging");
} else {
status.push_str("Discharging");
}
let mut result = format!("Battery ({}): {}%", device_name, percentage);
if !external_connected && !is_charging {
if let Some(time_mins) = avg_time_to_empty {
if time_mins > 0 && time_mins < 0xFFFF {
let hours = time_mins / 60;
let mins = time_mins % 60;
if hours > 0 && mins > 0 {
result.push_str(&format!(" ({} hours, {} mins remaining)", hours, mins));
} else if hours > 0 {
result.push_str(&format!(" ({} hours remaining)", hours));
} else if mins > 0 {
result.push_str(&format!(" ({} mins remaining)", mins));
}
}
}
}
result.push_str(&format!(" [{}]", status));
result
}

63
src/helpers/cpu.rs Normal file
View File

@@ -0,0 +1,63 @@
// https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/cpu/cpu_apple.c
use std::process::Command;
pub fn get_cpu_info() -> String {
let brand = Command::new("sysctl")
.args(["-n", "machdep.cpu.brand_string"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "Unknown CPU".to_string());
let cores = Command::new("sysctl")
.args(["-n", "hw.physicalcpu"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "0".to_string());
let ioreg_output = Command::new("ioreg")
.args(["-p", "IODeviceTree", "-n", "pmgr", "-l"])
.output();
let mut max_freq = 0u64;
if let Ok(output) = ioreg_output {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("voltage-states5-sram") {
let hex_data = line
.split('<')
.nth(1)
.and_then(|s| s.split('>').next())
.unwrap_or("");
for i in (0..hex_data.len()).step_by(16) {
if i + 8 <= hex_data.len() {
let chunk = &hex_data[i..i + 8];
let mut bytes = [0u8; 4];
for j in 0..4 {
if let Ok(byte) = u8::from_str_radix(&chunk[j * 2..j * 2 + 2], 16) {
bytes[j] = byte;
}
}
let freq = u32::from_le_bytes(bytes) as u64;
if freq > max_freq {
max_freq = freq;
}
}
}
}
}
}
if max_freq == 0 {
return format!("{} ({})", brand, cores);
}
let ghz = if max_freq > 100_000_000 {
max_freq as f64 / 1_000_000_000.0
} else {
max_freq as f64 / 1_000_000.0
};
format!("{} ({}) @ {:.2} GHz", brand, cores, ghz)
}

View File

@@ -4,25 +4,6 @@ use plist::Value;
use std::env;
use std::path::PathBuf;
fn format_color(dict: &plist::dictionary::Dictionary) -> String {
let r = (dict.get("red").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let g = (dict.get("green").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let b = (dict.get("blue").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let a = (dict.get("alpha").and_then(|v| v.as_real()).unwrap_or(1.0) * 255.0 + 0.5) as u32;
let color_hex = (r << 24) | (g << 16) | (b << 8) | a;
match color_hex {
0x000000FF => "Black".to_string(),
0xFFFFFFFF => "White".to_string(),
0xFF2600FF => "Red".to_string(),
0x0433FFFF => "Blue".to_string(),
0x00F900FF => "Green".to_string(),
0xFFFB00FF => "Yellow".to_string(),
_ => format!("#{:08X}", color_hex),
}
}
pub fn get_cursor_info() -> String {
let mut path = PathBuf::from(env::var("HOME").unwrap_or_default());
path.push("Library/Preferences/com.apple.universalaccess.plist");
@@ -34,10 +15,37 @@ pub fn get_cursor_info() -> String {
if let Ok(value) = Value::from_file(path) {
if let Some(dict) = value.as_dictionary() {
if let Some(f_dict) = dict.get("cursorFill").and_then(|v| v.as_dictionary()) {
fill = format_color(f_dict);
let r = (f_dict.get("red").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let g = (f_dict.get("green").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let b = (f_dict.get("blue").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let a = (f_dict.get("alpha").and_then(|v| v.as_real()).unwrap_or(1.0) * 255.0 + 0.5) as u32;
let color_hex = (r << 24) | (g << 16) | (b << 8) | a;
fill = match color_hex {
0x000000FF => "Black".to_string(),
0xFFFFFFFF => "White".to_string(),
0xFF2600FF => "Red".to_string(),
0x0433FFFF => "Blue".to_string(),
0x00F900FF => "Green".to_string(),
0xFFFB00FF => "Yellow".to_string(),
_ => format!("#{:08X}", color_hex),
};
}
if let Some(o_dict) = dict.get("cursorOutline").and_then(|v| v.as_dictionary()) {
outline = format_color(o_dict);
let r = (o_dict.get("red").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let g = (o_dict.get("green").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let b = (o_dict.get("blue").and_then(|v| v.as_real()).unwrap_or(0.0) * 255.0 + 0.5) as u32;
let a = (o_dict.get("alpha").and_then(|v| v.as_real()).unwrap_or(1.0) * 255.0 + 0.5) as u32;
let color_hex = (r << 24) | (g << 16) | (b << 8) | a;
outline = match color_hex {
0x000000FF => "Black".to_string(),
0xFFFFFFFF => "White".to_string(),
0xFF2600FF => "Red".to_string(),
0x0433FFFF => "Blue".to_string(),
0x00F900FF => "Green".to_string(),
0xFFFB00FF => "Yellow".to_string(),
_ => format!("#{:08X}", color_hex),
};
}
if let Some(s_val) = dict.get("mouseDriverCursorSize").and_then(|v| v.as_real()) {

100
src/helpers/gpu.rs Normal file
View File

@@ -0,0 +1,100 @@
// https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/gpu/gpu_apple.c
use std::process::Command;
pub fn get_gpu_info() -> String {
let ioreg_accel = Command::new("ioreg")
.args(["-rc", "IOAccelerator", "-d", "1"])
.output();
let mut model = String::new();
let mut cores = String::new();
let mut vendor_id = String::new();
if let Ok(output) = ioreg_accel {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("\"model\"") {
model = line
.split('=')
.nth(1)
.unwrap_or("")
.trim()
.replace('"', "")
.replace('<', "")
.replace('>', "");
}
if line.contains("\"gpu-core-count\"") {
cores = line.split('=').nth(1).unwrap_or("").trim().to_string();
}
if line.contains("\"vendor-id\"") {
vendor_id = line.split('=').nth(1).unwrap_or("").trim().to_string();
}
}
}
// Apple (0x106b) or Intel (0x8086) = Integrated
let type_str = match vendor_id.to_lowercase().as_str() {
"0x106b" | "4203" => "[Integrated]",
"0x8086" | "32902" => "[Integrated]",
"0x1002" | "4098" => "[Discrete]",
"0x10de" | "4318" => "[Discrete]",
_ => "[Integrated]", // Default for Apple Silicon if vendor-id is weird
};
let ioreg_pmgr = Command::new("ioreg")
.args(["-p", "IODeviceTree", "-n", "pmgr", "-l"])
.output();
let mut max_freq = 0u64;
if let Ok(output) = ioreg_pmgr {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("voltage-states9-sram") {
let hex_data = line
.split('<')
.nth(1)
.and_then(|s| s.split('>').next())
.unwrap_or("");
for i in (0..hex_data.len()).step_by(16) {
if i + 8 <= hex_data.len() {
let chunk = &hex_data[i..i + 8];
let mut bytes = [0u8; 4];
for j in 0..4 {
if let Ok(byte) = u8::from_str_radix(&chunk[j * 2..j * 2 + 2], 16) {
bytes[j] = byte;
}
}
let freq = u32::from_le_bytes(bytes) as u64;
if freq > max_freq {
max_freq = freq;
}
}
}
}
}
}
let freq_str = if max_freq > 0 {
let ghz = if max_freq > 100_000_000 {
max_freq as f64 / 1_000_000_000.0
} else {
max_freq as f64 / 1_000_000.0
};
format!(" @ {:.2} GHz", ghz)
} else {
"".to_string()
};
let core_str = if !cores.is_empty() {
format!(" ({})", cores)
} else {
"".to_string()
};
if model.is_empty() {
"Unknown GPU".to_string()
} else {
format!("{}{} {} {}", model, core_str, freq_str, type_str).replace(" ", " ")
}
}

55
src/helpers/ip.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::process::Command;
pub fn get_ip_info() -> String {
let mut interface = "en0".to_string();
let route_output = Command::new("route")
.args(["get", "default"])
.output();
if let Ok(output) = route_output {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.trim().starts_with("interface:") {
if let Some(iface) = line.split_whitespace().nth(1) {
interface = iface.to_string();
break;
}
}
}
}
let ifconfig_output = Command::new("ifconfig")
.arg(&interface)
.output();
if let Ok(output) = ifconfig_output {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("inet ") && !line.contains("127.0.0.1") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let ip = parts[1];
let mut ip_with_cidr = ip.to_string();
if let Some(netmask_idx) = parts.iter().position(|&x| x == "netmask") {
if netmask_idx + 1 < parts.len() {
let netmask_hex = parts[netmask_idx + 1];
if netmask_hex.starts_with("0x") {
if let Ok(num) = u32::from_str_radix(&netmask_hex[2..], 16) {
let cidr = num.count_ones();
ip_with_cidr = format!("{}/{}", ip, cidr);
}
}
}
}
return format!("Local IP ({}): {}", interface, ip_with_cidr);
}
}
}
}
"<unknown>".to_string()
}

18
src/helpers/locale.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::env;
pub fn get_locale_info() -> String {
if let Ok(locale) = env::var("LC_ALL") {
if !locale.is_empty() {
return format!("Locale: {}", locale);
}
}
if let Ok(locale) = env::var("LANG") {
if !locale.is_empty() {
return format!("Locale: {}", locale);
}
}
"<unknown>".to_string()
}

79
src/helpers/memory.rs Normal file
View File

@@ -0,0 +1,79 @@
// https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/memory/memory_apple.c
use libc::{
HOST_VM_INFO64, HOST_VM_INFO64_COUNT, host_statistics64, mach_host_self, vm_statistics64_data_t,
};
use std::mem;
use std::process::Command;
pub fn get_memory_info() -> String {
let total_bytes = Command::new("sysctl")
.args(["-n", "hw.memsize"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u64>()
.unwrap_or(0)
})
.unwrap_or(0);
let usable_bytes = Command::new("sysctl")
.args(["-n", "hw.memsize_usable"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u64>()
.unwrap_or(total_bytes)
})
.unwrap_or(total_bytes);
let mut used_bytes: u64 = 0;
// mach_host_self, and HOST_VM_INFO64 are macos c functions (ffi), so we HAVE to use unsafe
// i needa learn a better way to do this tbh
// learned smth new lol
unsafe {
let mut count = HOST_VM_INFO64_COUNT;
let mut vmstat: vm_statistics64_data_t = mem::zeroed(); // this this is unsafe we have to manually zero it
if host_statistics64(
mach_host_self(),
HOST_VM_INFO64,
&mut vmstat as *mut _ as *mut _,
&mut count,
) == 0
{
let page_size = Command::new("pagesize")
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u64>()
.unwrap_or(4096)
})
.unwrap_or(4096);
let app_memory = (vmstat.internal_page_count as u64) * page_size;
let wired_memory = (vmstat.wire_count as u64) * page_size;
let compressed_memory = (vmstat.compressor_page_count as u64) * page_size;
let reserved_memory = total_bytes.saturating_sub(usable_bytes);
used_bytes = app_memory + wired_memory + compressed_memory + reserved_memory;
}
}
let total_gib = total_bytes as f64 / 1073741824.0;
let used_gib = used_bytes as f64 / 1073741824.0;
let percentage = if total_bytes > 0 {
(used_bytes as f64 / total_bytes as f64) * 100.0
} else {
0.0
};
format!(
"{:.2} GiB / {:.2} GiB ({:.0}%)",
used_gib, total_gib, percentage
)
}

View File

@@ -1,9 +1,17 @@
pub mod battery;
pub mod cpu;
pub mod cursor;
pub mod desktop_env;
pub mod display;
pub mod font;
pub mod gpu;
pub mod ip;
pub mod locale;
pub mod memory;
pub mod packages;
pub mod shell;
pub mod storage;
pub mod swap;
pub mod terminal;
pub mod terminal_font;
pub mod uptime;

100
src/helpers/storage.rs Normal file
View File

@@ -0,0 +1,100 @@
// https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/disk/disk.c
use libc::{c_int, c_char};
use std::ffi::CStr;
#[repr(C)]
#[derive(Clone, Copy)]
struct Statfs {
f_bsize: u32,
f_iosize: c_int,
f_blocks: u64,
f_bfree: u64,
f_bavail: u64,
f_files: u64,
f_ffree: u64,
f_fsid: [u32; 2],
f_owner: u32,
f_type: u32,
f_flags: u32,
f_fssubtype: u32,
f_fstypename: [c_char; 16],
f_mntonname: [c_char; 1024],
f_mntfromname: [c_char; 1024],
f_reserved: [u32; 8],
}
unsafe extern "C" {
fn getfsstat(buf: *mut Statfs, bufsize: c_int, flags: c_int) -> c_int;
}
const MNT_WAIT: c_int = 1;
const MNT_NOWAIT: c_int = 2;
const MNT_RDONLY: u32 = 0x00000001;
pub fn get_storage_info() -> String {
unsafe {
let size = getfsstat(std::ptr::null_mut(), 0, MNT_WAIT);
if size <= 0 {
return "<unknown>".to_string();
}
let statfs_size = std::mem::size_of::<Statfs>();
if statfs_size != 2168 {
return "<unknown>".to_string();
}
let mut buf = vec![std::mem::zeroed::<Statfs>(); size as usize];
let bufsize = (statfs_size * size as usize) as c_int;
let result = getfsstat(buf.as_mut_ptr(), bufsize, MNT_NOWAIT);
if result <= 0 {
return "<unknown>".to_string();
}
for fs in &buf {
let mountpoint = CStr::from_ptr(fs.f_mntonname.as_ptr() as *const c_char)
.to_string_lossy()
.to_string();
if mountpoint == "/" {
let filesystem = CStr::from_ptr(fs.f_fstypename.as_ptr() as *const c_char)
.to_string_lossy()
.to_string();
let block_size = fs.f_bsize as u64;
let total_bytes = fs.f_blocks * block_size;
let available_bytes = fs.f_bavail * block_size;
let used_bytes = total_bytes.saturating_sub(available_bytes);
let total_gib = total_bytes as f64 / 1073741824.0;
let used_gib = used_bytes as f64 / 1073741824.0;
let percentage = if total_bytes > 0 {
(used_bytes as f64 / total_bytes as f64) * 100.0
} else {
0.0
};
let read_only = (fs.f_flags & MNT_RDONLY) != 0;
let mut result = format!(
"{:.2} GiB / {:.2} GiB ({:.0}%)",
used_gib, total_gib, percentage
);
if !filesystem.is_empty() {
result.push_str(&format!(" - {}", filesystem));
}
if read_only {
result.push_str(" [Read-only]");
}
return result;
}
}
}
"<unknown>".to_string()
}

41
src/helpers/swap.rs Normal file
View File

@@ -0,0 +1,41 @@
// https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/swap/swap_apple.c
use std::process::Command;
pub fn get_swap_info() -> String {
let output = Command::new("sysctl").args(["-n", "vm.swapusage"]).output();
if let Ok(out) = output {
let stdout = String::from_utf8_lossy(&out.stdout);
let mut total_mb = 0.0;
let mut used_mb = 0.0;
for part in stdout.split_whitespace() {
if let Some(val) = part.strip_suffix('M') {
if let Ok(num) = val.parse::<f64>() {
if stdout.contains(&format!("total = {}", part)) {
total_mb = num;
} else if stdout.contains(&format!("used = {}", part)) {
used_mb = num;
}
}
}
}
let total_gib = total_mb / 1024.0;
let used_gib = used_mb / 1024.0;
let percentage = if total_mb > 0.0 {
(used_mb / total_mb) * 100.0
} else {
0.0
};
return format!(
"{:.2} GiB / {:.2} GiB ({:.0}%)",
used_gib, total_gib, percentage
);
}
"0.00 GiB / 0.00 GiB (0%)".to_string()
}

View File

@@ -25,6 +25,14 @@ struct Stats {
cursor: String,
terminal: String,
terminal_font: String,
cpu: String,
gpu: String,
memory: String,
swap: String,
storage: String,
ip: String,
battery: String,
locale: String,
// Extra fields
architecture: String,
@@ -54,6 +62,14 @@ fn main() {
cursor: helpers::cursor::get_cursor_info(),
terminal: helpers::terminal::get_terminal_info(),
terminal_font: helpers::terminal_font::get_terminal_font_info(),
cpu: helpers::cpu::get_cpu_info(),
gpu: helpers::gpu::get_gpu_info(),
memory: helpers::memory::get_memory_info(),
swap: helpers::swap::get_swap_info(),
storage: helpers::storage::get_storage_info(),
ip: helpers::ip::get_ip_info(),
battery: helpers::battery::get_battery_info(),
locale: helpers::locale::get_locale_info(),
};
// TODO: Add ascii art support later
@@ -76,5 +92,13 @@ fn main() {
println!("{}", stats.cursor);
println!("{}", stats.terminal);
println!("{}", stats.terminal_font);
println!("{}", stats.cpu);
println!("{}", stats.gpu);
println!("{}", stats.memory);
println!("{}", stats.swap);
println!("{}", stats.storage);
println!("{}", stats.ip);
println!("{}", stats.battery);
println!("{}", stats.locale);
}
}