#[derive(Debug, Default, Clone)]
pub struct SystemInfo {
pub os: String,
pub version: String,
pub display_server: String,
pub desktop: String,
}
impl SystemInfo {
pub fn one_liner(&self) -> String {
let mut parts: Vec<&str> = Vec::with_capacity(4);
let header = if self.version.is_empty() {
self.os.as_str()
} else {
self.version.as_str()
};
if !header.is_empty() {
parts.push(header);
}
if !self.display_server.is_empty() {
parts.push(self.display_server.as_str());
}
if !self.desktop.is_empty() {
parts.push(self.desktop.as_str());
}
if parts.is_empty() {
"unknown".into()
} else {
parts.join(" · ")
}
}
}
pub fn detect() -> SystemInfo {
use std::sync::OnceLock;
static CACHE: OnceLock<SystemInfo> = OnceLock::new();
CACHE.get_or_init(detect_uncached).clone()
}
fn detect_uncached() -> SystemInfo {
#[cfg(target_os = "linux")]
{
detect_linux()
}
#[cfg(target_os = "macos")]
{
detect_macos()
}
#[cfg(target_os = "windows")]
{
detect_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
SystemInfo {
os: std::env::consts::OS.to_string(),
..SystemInfo::default()
}
}
}
#[cfg(target_os = "linux")]
fn detect_linux() -> SystemInfo {
SystemInfo {
os: "Linux".into(),
version: read_os_release_pretty().unwrap_or_else(|| "Linux".into()),
display_server: linux_display_server(),
desktop: linux_desktop(),
}
}
#[cfg(target_os = "linux")]
fn read_os_release_pretty() -> Option<String> {
let content = std::fs::read_to_string("/etc/os-release").ok()?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("PRETTY_NAME=") {
return Some(rest.trim().trim_matches('"').to_string());
}
}
None
}
#[cfg(target_os = "linux")]
fn linux_display_server() -> String {
if let Ok(t) = std::env::var("XDG_SESSION_TYPE") {
match t.as_str() {
"wayland" => return "Wayland".into(),
"x11" => return "X11".into(),
"tty" => return "TTY".into(),
_ => {}
}
}
if std::env::var("WAYLAND_DISPLAY").is_ok() {
"Wayland".into()
} else if std::env::var("DISPLAY").is_ok() {
"X11".into()
} else {
String::new()
}
}
#[cfg(target_os = "linux")]
fn linux_desktop() -> String {
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
.or_else(|_| std::env::var("DESKTOP_SESSION"))
.ok()
.and_then(|s| s.split(':').next().map(str::to_string))
.unwrap_or_default();
if desktop.is_empty() {
return String::new();
}
let upper = desktop.to_ascii_uppercase();
match upper.as_str() {
"GNOME" | "GNOME-CLASSIC" | "UBUNTU" | "UBUNTU:GNOME" => "GNOME (Mutter)".into(),
"KDE" | "PLASMA" => "KDE Plasma (KWin)".into(),
"XFCE" => "XFCE (Xfwm4)".into(),
"X-CINNAMON" | "CINNAMON" => "Cinnamon (Muffin)".into(),
"MATE" => "MATE (Marco)".into(),
"LXQT" => "LXQt (Openbox)".into(),
"LXDE" => "LXDE (Openbox)".into(),
"PANTHEON" => "Pantheon (Gala)".into(),
"BUDGIE" | "BUDGIE:GNOME" => "Budgie".into(),
"DEEPIN" => "Deepin".into(),
"SWAY" => "Sway".into(),
"HYPRLAND" => "Hyprland".into(),
"WAYFIRE" => "Wayfire".into(),
"I3" => "i3".into(),
"BSPWM" => "bspwm".into(),
"AWESOME" => "awesome".into(),
_ => desktop, }
}
#[cfg(target_os = "macos")]
fn detect_macos() -> SystemInfo {
let version = read_macos_version().unwrap_or_else(|| "macOS".into());
SystemInfo {
os: "macOS".into(),
version,
display_server: "Quartz".into(),
desktop: "Aqua".into(),
}
}
#[cfg(target_os = "macos")]
fn read_macos_version() -> Option<String> {
let path = "/System/Library/CoreServices/SystemVersion.plist";
let xml = std::fs::read_to_string(path).ok()?;
let value_after = |key: &str| -> Option<String> {
let needle = format!("<key>{key}</key>");
let idx = xml.find(&needle)? + needle.len();
let rest = &xml[idx..];
let open = rest.find("<string>")? + "<string>".len();
let close = rest[open..].find("</string>")?;
Some(rest[open..open + close].trim().to_string())
};
let product = value_after("ProductName").unwrap_or_else(|| "macOS".into());
let version = value_after("ProductVersion").unwrap_or_default();
let build = value_after("ProductBuildVersion").unwrap_or_default();
let mut s = product;
if !version.is_empty() {
s.push(' ');
s.push_str(&version);
}
if !build.is_empty() {
s.push_str(" (");
s.push_str(&build);
s.push(')');
}
Some(s)
}
#[cfg(target_os = "windows")]
fn detect_windows() -> SystemInfo {
SystemInfo {
os: "Windows".into(),
version: read_windows_version().unwrap_or_else(|| "Windows".into()),
display_server: "DWM".into(),
desktop: "Explorer".into(),
}
}
#[cfg(target_os = "windows")]
fn read_windows_version() -> Option<String> {
use windows::Win32::Foundation::ERROR_SUCCESS;
use windows::Win32::System::Registry::{
HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, RegGetValueW,
};
use windows::core::PCWSTR;
let wide = |s: &str| {
s.encode_utf16()
.chain(std::iter::once(0))
.collect::<Vec<u16>>()
};
let key = wide(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
let string = |name: &str| -> Option<String> {
let name = wide(name);
let mut buf = [0u16; 256];
let mut len = std::mem::size_of_val(&buf) as u32;
let read = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
PCWSTR(key.as_ptr()),
PCWSTR(name.as_ptr()),
RRF_RT_REG_SZ,
None,
Some(buf.as_mut_ptr().cast()),
Some(&mut len),
)
};
if read != ERROR_SUCCESS {
return None;
}
let chars = (len as usize / 2).min(buf.len());
let s = String::from_utf16_lossy(&buf[..chars]);
let s = s.trim_end_matches('\0').trim();
(!s.is_empty()).then(|| s.to_string())
};
let dword = |name: &str| -> Option<u32> {
let name = wide(name);
let mut value = 0u32;
let mut len = 4u32;
let read = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
PCWSTR(key.as_ptr()),
PCWSTR(name.as_ptr()),
RRF_RT_REG_DWORD,
None,
Some((&mut value as *mut u32).cast()),
Some(&mut len),
)
};
(read == ERROR_SUCCESS).then_some(value)
};
let mut name = string("ProductName")?;
let build = string("CurrentBuild").unwrap_or_default();
if build.parse::<u32>().is_ok_and(|b| b >= 22000) {
name = name.replacen("Windows 10", "Windows 11", 1);
}
let release = string("DisplayVersion").or_else(|| string("ReleaseId"));
let build = match dword("UBR") {
Some(ubr) => format!("{build}.{ubr}"),
None => build,
};
Some(match release {
Some(r) => format!("{name} {r} (build {build})"),
None => format!("{name} (build {build})"),
})
}