use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
Darwin,
Linux,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum DesktopEnvironment {
Gnome,
Kde,
Cosmic,
Other,
None,
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Platform::Darwin => write!(f, "macOS"),
Platform::Linux => write!(f, "Linux"),
}
}
}
impl std::fmt::Display for DesktopEnvironment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopEnvironment::Gnome => write!(f, "GNOME"),
DesktopEnvironment::Kde => write!(f, "KDE Plasma"),
DesktopEnvironment::Cosmic => write!(f, "COSMIC"),
DesktopEnvironment::Other => write!(f, "other"),
DesktopEnvironment::None => write!(f, "none"),
}
}
}
pub fn detect_platform() -> Platform {
if runtime_os() == "darwin" {
Platform::Darwin
} else {
Platform::Linux
}
}
#[allow(dead_code)]
pub fn detect_desktop_environment() -> DesktopEnvironment {
if detect_platform() == Platform::Darwin {
return DesktopEnvironment::None;
}
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
let lower = desktop.to_lowercase();
if lower.contains("gnome") {
return DesktopEnvironment::Gnome;
}
if lower.contains("kde") || lower.contains("plasma") {
return DesktopEnvironment::Kde;
}
if lower.contains("cosmic") {
return DesktopEnvironment::Cosmic;
}
return DesktopEnvironment::Other;
}
if let Ok(session) = std::env::var("DESKTOP_SESSION") {
let lower = session.to_lowercase();
if lower.contains("gnome") {
return DesktopEnvironment::Gnome;
}
if lower.contains("plasma") || lower.contains("kde") {
return DesktopEnvironment::Kde;
}
if lower.contains("cosmic") {
return DesktopEnvironment::Cosmic;
}
return DesktopEnvironment::Other;
}
if Command::new("pgrep")
.arg("gnome-shell")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return DesktopEnvironment::Gnome;
}
if Command::new("pgrep")
.arg("plasmashell")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return DesktopEnvironment::Kde;
}
if Command::new("pgrep")
.arg("cosmic-comp")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return DesktopEnvironment::Cosmic;
}
DesktopEnvironment::None
}
#[allow(dead_code)]
pub fn is_nixos() -> bool {
Path::new("/etc/NIXOS").exists()
}
pub fn find_repo() -> Result<PathBuf> {
if let Ok(cwd) = std::env::current_dir() {
let mut dir = cwd.as_path();
loop {
let flake = dir.join("flake.nix");
if flake.exists() && is_nex_flake(&flake) {
return Ok(dir.to_path_buf());
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
}
if let Some(home) = dirs::home_dir() {
let candidates = [
home.join("workspace/black-meridian/styrene-lab/macos-nix"),
home.join("macos-nix"),
home.join("nix-config"),
home.join(".config/nix-darwin"),
home.join(".config/nixos"),
PathBuf::from("/etc/nixos"),
];
for path in &candidates {
let flake = path.join("flake.nix");
if flake.exists() && is_nex_flake(&flake) {
return Ok(path.clone());
}
}
}
anyhow::bail!("could not find nix config repo (nix-darwin or NixOS)")
}
fn is_nex_flake(path: &Path) -> bool {
std::fs::read_to_string(path)
.map(|content| {
content.contains("darwinConfigurations") || content.contains("nixosConfigurations")
})
.unwrap_or(false)
}
pub fn hostname() -> Result<String> {
match detect_platform() {
Platform::Darwin => {
let output = Command::new("scutil")
.args(["--get", "LocalHostName"])
.output()
.context("failed to run scutil --get LocalHostName")?;
if !output.status.success() {
anyhow::bail!("scutil --get LocalHostName failed");
}
let name = String::from_utf8(output.stdout)
.context("hostname is not valid UTF-8")?
.trim()
.to_string();
Ok(name)
}
Platform::Linux => {
if let Ok(name) = std::fs::read_to_string("/etc/hostname") {
let trimmed = name.trim().to_string();
if !trimmed.is_empty() {
return Ok(trimmed);
}
}
let output = Command::new("hostname")
.output()
.context("failed to run hostname")?;
if !output.status.success() {
anyhow::bail!("hostname command failed");
}
let name = String::from_utf8(output.stdout)
.context("hostname is not valid UTF-8")?
.trim()
.to_string();
Ok(name)
}
}
}
pub fn detect_system() -> &'static str {
let os = runtime_os();
let arch = runtime_arch();
match (arch, os) {
("x86_64", "darwin") => "x86_64-darwin",
("aarch64", "darwin") => "aarch64-darwin",
("x86_64", "linux") => "x86_64-linux",
("aarch64", "linux") => "aarch64-linux",
_ => {
if cfg!(target_os = "macos") {
if cfg!(target_arch = "x86_64") {
"x86_64-darwin"
} else {
"aarch64-darwin"
}
} else {
if cfg!(target_arch = "x86_64") {
"x86_64-linux"
} else {
"aarch64-linux"
}
}
}
}
}
fn runtime_os() -> &'static str {
if Path::new("/proc/version").exists() {
return "linux";
}
if Path::new("/System/Library").exists() {
return "darwin";
}
if cfg!(target_os = "macos") {
"darwin"
} else {
"linux"
}
}
fn runtime_arch() -> &'static str {
if let Ok(output) = Command::new("uname").arg("-m").output() {
if output.status.success() {
let arch = String::from_utf8_lossy(&output.stdout);
let arch = arch.trim();
return match arch {
"x86_64" => "x86_64",
"aarch64" | "arm64" => "aarch64",
_ => {
if cfg!(target_arch = "x86_64") {
"x86_64"
} else {
"aarch64"
}
}
};
}
}
if cfg!(target_arch = "x86_64") {
"x86_64"
} else {
"aarch64"
}
}
pub fn default_repo_name() -> &'static str {
match detect_platform() {
Platform::Darwin => "macos-nix",
Platform::Linux => "nix-config",
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_detect_system_returns_valid_string() {
let sys = detect_system();
assert!(
[
"x86_64-darwin",
"aarch64-darwin",
"x86_64-linux",
"aarch64-linux"
]
.contains(&sys),
"detect_system returned unexpected: {sys}"
);
}
#[test]
fn test_detect_platform_consistent_with_system() {
let platform = detect_platform();
let system = detect_system();
match platform {
Platform::Darwin => assert!(system.ends_with("-darwin")),
Platform::Linux => assert!(system.ends_with("-linux")),
}
}
#[test]
fn test_runtime_arch_returns_known() {
let arch = runtime_arch();
assert!(
["x86_64", "aarch64"].contains(&arch),
"runtime_arch returned unexpected: {arch}"
);
}
#[test]
fn test_platform_display() {
assert_eq!(format!("{}", Platform::Darwin), "macOS");
assert_eq!(format!("{}", Platform::Linux), "Linux");
}
#[test]
fn test_default_repo_name() {
let name = default_repo_name();
assert!(name == "macos-nix" || name == "nix-config");
}
}