1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use anyhow::{Context, Result};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Platform {
9 Darwin,
10 Linux,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[allow(dead_code)]
16pub enum DesktopEnvironment {
17 Gnome,
18 Kde,
19 Cosmic,
20 Other,
21 None,
22}
23
24impl std::fmt::Display for Platform {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Platform::Darwin => write!(f, "macOS"),
28 Platform::Linux => write!(f, "Linux"),
29 }
30 }
31}
32
33impl std::fmt::Display for DesktopEnvironment {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 DesktopEnvironment::Gnome => write!(f, "GNOME"),
37 DesktopEnvironment::Kde => write!(f, "KDE Plasma"),
38 DesktopEnvironment::Cosmic => write!(f, "COSMIC"),
39 DesktopEnvironment::Other => write!(f, "other"),
40 DesktopEnvironment::None => write!(f, "none"),
41 }
42 }
43}
44
45pub fn detect_platform() -> Platform {
47 if runtime_os() == "darwin" {
48 Platform::Darwin
49 } else {
50 Platform::Linux
51 }
52}
53
54#[allow(dead_code)]
56pub fn detect_desktop_environment() -> DesktopEnvironment {
57 if detect_platform() == Platform::Darwin {
58 return DesktopEnvironment::None;
59 }
60
61 if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
63 let lower = desktop.to_lowercase();
64 if lower.contains("gnome") {
65 return DesktopEnvironment::Gnome;
66 }
67 if lower.contains("kde") || lower.contains("plasma") {
68 return DesktopEnvironment::Kde;
69 }
70 if lower.contains("cosmic") {
71 return DesktopEnvironment::Cosmic;
72 }
73 return DesktopEnvironment::Other;
74 }
75
76 if let Ok(session) = std::env::var("DESKTOP_SESSION") {
78 let lower = session.to_lowercase();
79 if lower.contains("gnome") {
80 return DesktopEnvironment::Gnome;
81 }
82 if lower.contains("plasma") || lower.contains("kde") {
83 return DesktopEnvironment::Kde;
84 }
85 if lower.contains("cosmic") {
86 return DesktopEnvironment::Cosmic;
87 }
88 return DesktopEnvironment::Other;
89 }
90
91 if Command::new("pgrep")
93 .arg("gnome-shell")
94 .output()
95 .map(|o| o.status.success())
96 .unwrap_or(false)
97 {
98 return DesktopEnvironment::Gnome;
99 }
100 if Command::new("pgrep")
101 .arg("plasmashell")
102 .output()
103 .map(|o| o.status.success())
104 .unwrap_or(false)
105 {
106 return DesktopEnvironment::Kde;
107 }
108 if Command::new("pgrep")
109 .arg("cosmic-comp")
110 .output()
111 .map(|o| o.status.success())
112 .unwrap_or(false)
113 {
114 return DesktopEnvironment::Cosmic;
115 }
116
117 DesktopEnvironment::None
118}
119
120#[allow(dead_code)]
122pub fn is_nixos() -> bool {
123 Path::new("/etc/NIXOS").exists()
124}
125
126pub fn find_repo() -> Result<PathBuf> {
129 if let Ok(cwd) = std::env::current_dir() {
131 let mut dir = cwd.as_path();
132 loop {
133 let flake = dir.join("flake.nix");
134 if flake.exists() && is_nex_flake(&flake) {
135 return Ok(dir.to_path_buf());
136 }
137 match dir.parent() {
138 Some(parent) => dir = parent,
139 None => break,
140 }
141 }
142 }
143
144 if let Some(home) = dirs::home_dir() {
146 let candidates = [
147 home.join("workspace/black-meridian/styrene-lab/macos-nix"),
148 home.join("macos-nix"),
149 home.join("nix-config"),
150 home.join(".config/nix-darwin"),
151 home.join(".config/nixos"),
152 PathBuf::from("/etc/nixos"),
156 ];
157 for path in &candidates {
158 let flake = path.join("flake.nix");
159 if flake.exists() && is_nex_flake(&flake) {
160 return Ok(path.clone());
161 }
162 }
163 }
164
165 anyhow::bail!("could not find nix config repo (nix-darwin or NixOS)")
166}
167
168fn is_nex_flake(path: &Path) -> bool {
170 std::fs::read_to_string(path)
171 .map(|content| {
172 content.contains("darwinConfigurations") || content.contains("nixosConfigurations")
173 })
174 .unwrap_or(false)
175}
176
177pub fn hostname() -> Result<String> {
179 match detect_platform() {
180 Platform::Darwin => {
181 let output = Command::new("scutil")
182 .args(["--get", "LocalHostName"])
183 .output()
184 .context("failed to run scutil --get LocalHostName")?;
185
186 if !output.status.success() {
187 anyhow::bail!("scutil --get LocalHostName failed");
188 }
189
190 let name = String::from_utf8(output.stdout)
191 .context("hostname is not valid UTF-8")?
192 .trim()
193 .to_string();
194
195 Ok(name)
196 }
197 Platform::Linux => {
198 if let Ok(name) = std::fs::read_to_string("/etc/hostname") {
200 let trimmed = name.trim().to_string();
201 if !trimmed.is_empty() {
202 return Ok(trimmed);
203 }
204 }
205
206 let output = Command::new("hostname")
208 .output()
209 .context("failed to run hostname")?;
210
211 if !output.status.success() {
212 anyhow::bail!("hostname command failed");
213 }
214
215 let name = String::from_utf8(output.stdout)
216 .context("hostname is not valid UTF-8")?
217 .trim()
218 .to_string();
219
220 Ok(name)
221 }
222 }
223}
224
225pub fn detect_system() -> &'static str {
228 let os = runtime_os();
229 let arch = runtime_arch();
230 match (arch, os) {
231 ("x86_64", "darwin") => "x86_64-darwin",
232 ("aarch64", "darwin") => "aarch64-darwin",
233 ("x86_64", "linux") => "x86_64-linux",
234 ("aarch64", "linux") => "aarch64-linux",
235 _ => {
236 if cfg!(target_os = "macos") {
238 if cfg!(target_arch = "x86_64") {
239 "x86_64-darwin"
240 } else {
241 "aarch64-darwin"
242 }
243 } else {
244 if cfg!(target_arch = "x86_64") {
245 "x86_64-linux"
246 } else {
247 "aarch64-linux"
248 }
249 }
250 }
251 }
252}
253
254fn runtime_os() -> &'static str {
256 if Path::new("/proc/version").exists() {
258 return "linux";
259 }
260 if Path::new("/System/Library").exists() {
262 return "darwin";
263 }
264 if cfg!(target_os = "macos") {
266 "darwin"
267 } else {
268 "linux"
269 }
270}
271
272fn runtime_arch() -> &'static str {
274 if let Ok(output) = Command::new("uname").arg("-m").output() {
275 if output.status.success() {
276 let arch = String::from_utf8_lossy(&output.stdout);
277 let arch = arch.trim();
278 return match arch {
279 "x86_64" => "x86_64",
280 "aarch64" | "arm64" => "aarch64",
281 _ => {
282 if cfg!(target_arch = "x86_64") {
283 "x86_64"
284 } else {
285 "aarch64"
286 }
287 }
288 };
289 }
290 }
291 if cfg!(target_arch = "x86_64") {
292 "x86_64"
293 } else {
294 "aarch64"
295 }
296}
297
298pub fn default_repo_name() -> &'static str {
300 match detect_platform() {
301 Platform::Darwin => "macos-nix",
302 Platform::Linux => "nix-config",
303 }
304}
305
306#[cfg(test)]
307#[allow(clippy::unwrap_used)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_detect_system_returns_valid_string() {
313 let sys = detect_system();
314 assert!(
315 [
316 "x86_64-darwin",
317 "aarch64-darwin",
318 "x86_64-linux",
319 "aarch64-linux"
320 ]
321 .contains(&sys),
322 "detect_system returned unexpected: {sys}"
323 );
324 }
325
326 #[test]
327 fn test_detect_platform_consistent_with_system() {
328 let platform = detect_platform();
329 let system = detect_system();
330 match platform {
331 Platform::Darwin => assert!(system.ends_with("-darwin")),
332 Platform::Linux => assert!(system.ends_with("-linux")),
333 }
334 }
335
336 #[test]
337 fn test_runtime_arch_returns_known() {
338 let arch = runtime_arch();
339 assert!(
340 ["x86_64", "aarch64"].contains(&arch),
341 "runtime_arch returned unexpected: {arch}"
342 );
343 }
344
345 #[test]
346 fn test_platform_display() {
347 assert_eq!(format!("{}", Platform::Darwin), "macOS");
348 assert_eq!(format!("{}", Platform::Linux), "Linux");
349 }
350
351 #[test]
352 fn test_default_repo_name() {
353 let name = default_repo_name();
354 assert!(name == "macos-nix" || name == "nix-config");
355 }
356}