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 let Ok(platform) = std::env::var("NEX_TEST_PLATFORM") {
48 if std::env::var_os("NEX_TESTING").is_some() {
49 return match platform.as_str() {
50 "darwin" | "macos" => Platform::Darwin,
51 "linux" | "nixos" => Platform::Linux,
52 _ => Platform::Linux,
53 };
54 }
55 }
56
57 if runtime_os() == "darwin" {
58 Platform::Darwin
59 } else {
60 Platform::Linux
61 }
62}
63
64#[allow(dead_code)]
66pub fn detect_desktop_environment() -> DesktopEnvironment {
67 if detect_platform() == Platform::Darwin {
68 return DesktopEnvironment::None;
69 }
70
71 if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
73 let lower = desktop.to_lowercase();
74 if lower.contains("gnome") {
75 return DesktopEnvironment::Gnome;
76 }
77 if lower.contains("kde") || lower.contains("plasma") {
78 return DesktopEnvironment::Kde;
79 }
80 if lower.contains("cosmic") {
81 return DesktopEnvironment::Cosmic;
82 }
83 return DesktopEnvironment::Other;
84 }
85
86 if let Ok(session) = std::env::var("DESKTOP_SESSION") {
88 let lower = session.to_lowercase();
89 if lower.contains("gnome") {
90 return DesktopEnvironment::Gnome;
91 }
92 if lower.contains("plasma") || lower.contains("kde") {
93 return DesktopEnvironment::Kde;
94 }
95 if lower.contains("cosmic") {
96 return DesktopEnvironment::Cosmic;
97 }
98 return DesktopEnvironment::Other;
99 }
100
101 if Command::new("pgrep")
103 .arg("gnome-shell")
104 .output()
105 .map(|o| o.status.success())
106 .unwrap_or(false)
107 {
108 return DesktopEnvironment::Gnome;
109 }
110 if Command::new("pgrep")
111 .arg("plasmashell")
112 .output()
113 .map(|o| o.status.success())
114 .unwrap_or(false)
115 {
116 return DesktopEnvironment::Kde;
117 }
118 if Command::new("pgrep")
119 .arg("cosmic-comp")
120 .output()
121 .map(|o| o.status.success())
122 .unwrap_or(false)
123 {
124 return DesktopEnvironment::Cosmic;
125 }
126
127 DesktopEnvironment::None
128}
129
130#[allow(dead_code)]
132pub fn is_nixos() -> bool {
133 Path::new("/etc/NIXOS").exists()
134}
135
136pub fn find_repo() -> Result<PathBuf> {
139 if let Ok(cwd) = std::env::current_dir() {
141 let mut dir = cwd.as_path();
142 loop {
143 let flake = dir.join("flake.nix");
144 if flake.exists() && is_nex_flake(&flake) {
145 return Ok(dir.to_path_buf());
146 }
147 match dir.parent() {
148 Some(parent) => dir = parent,
149 None => break,
150 }
151 }
152 }
153
154 if let Some(home) = dirs::home_dir() {
156 let candidates = [
157 home.join("workspace/black-meridian/styrene-lab/macos-nix"),
158 home.join("macos-nix"),
159 home.join("nix-config"),
160 home.join(".config/nix-darwin"),
161 home.join(".config/nixos"),
162 PathBuf::from("/etc/nixos"),
166 ];
167 for path in &candidates {
168 let flake = path.join("flake.nix");
169 if flake.exists() && is_nex_flake(&flake) {
170 return Ok(path.clone());
171 }
172 }
173 }
174
175 anyhow::bail!("could not find nix config repo (nix-darwin or NixOS)")
176}
177
178fn is_nex_flake(path: &Path) -> bool {
180 std::fs::read_to_string(path)
181 .map(|content| {
182 content.contains("darwinConfigurations") || content.contains("nixosConfigurations")
183 })
184 .unwrap_or(false)
185}
186
187pub fn hostname() -> Result<String> {
189 match detect_platform() {
190 Platform::Darwin => {
191 let output = Command::new("scutil")
192 .args(["--get", "LocalHostName"])
193 .output()
194 .context("failed to run scutil --get LocalHostName")?;
195
196 if !output.status.success() {
197 anyhow::bail!("scutil --get LocalHostName failed");
198 }
199
200 let name = String::from_utf8(output.stdout)
201 .context("hostname is not valid UTF-8")?
202 .trim()
203 .to_string();
204
205 Ok(name)
206 }
207 Platform::Linux => {
208 if let Ok(name) = std::fs::read_to_string("/etc/hostname") {
210 let trimmed = name.trim().to_string();
211 if !trimmed.is_empty() {
212 return Ok(trimmed);
213 }
214 }
215
216 let output = Command::new("hostname")
218 .output()
219 .context("failed to run hostname")?;
220
221 if !output.status.success() {
222 anyhow::bail!("hostname command failed");
223 }
224
225 let name = String::from_utf8(output.stdout)
226 .context("hostname is not valid UTF-8")?
227 .trim()
228 .to_string();
229
230 Ok(name)
231 }
232 }
233}
234
235pub fn detect_system() -> &'static str {
238 let os = runtime_os();
239 let arch = runtime_arch();
240 match (arch, os) {
241 ("x86_64", "darwin") => "x86_64-darwin",
242 ("aarch64", "darwin") => "aarch64-darwin",
243 ("x86_64", "linux") => "x86_64-linux",
244 ("aarch64", "linux") => "aarch64-linux",
245 _ => {
246 if cfg!(target_os = "macos") {
248 if cfg!(target_arch = "x86_64") {
249 "x86_64-darwin"
250 } else {
251 "aarch64-darwin"
252 }
253 } else {
254 if cfg!(target_arch = "x86_64") {
255 "x86_64-linux"
256 } else {
257 "aarch64-linux"
258 }
259 }
260 }
261 }
262}
263
264fn runtime_os() -> &'static str {
266 if Path::new("/proc/version").exists() {
268 return "linux";
269 }
270 if Path::new("/System/Library").exists() {
272 return "darwin";
273 }
274 if cfg!(target_os = "macos") {
276 "darwin"
277 } else {
278 "linux"
279 }
280}
281
282fn runtime_arch() -> &'static str {
284 if let Ok(output) = Command::new("uname").arg("-m").output() {
285 if output.status.success() {
286 let arch = crate::exec::captured_text(&output.stdout);
287 let arch = arch.trim();
288 return match arch {
289 "x86_64" => "x86_64",
290 "aarch64" | "arm64" => "aarch64",
291 _ => {
292 if cfg!(target_arch = "x86_64") {
293 "x86_64"
294 } else {
295 "aarch64"
296 }
297 }
298 };
299 }
300 }
301 if cfg!(target_arch = "x86_64") {
302 "x86_64"
303 } else {
304 "aarch64"
305 }
306}
307
308pub fn default_repo_name() -> &'static str {
310 match detect_platform() {
311 Platform::Darwin => "macos-nix",
312 Platform::Linux => "nix-config",
313 }
314}
315
316#[cfg(test)]
317#[allow(clippy::unwrap_used)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_detect_system_returns_valid_string() {
323 let sys = detect_system();
324 assert!(
325 [
326 "x86_64-darwin",
327 "aarch64-darwin",
328 "x86_64-linux",
329 "aarch64-linux"
330 ]
331 .contains(&sys),
332 "detect_system returned unexpected: {sys}"
333 );
334 }
335
336 #[test]
337 fn test_detect_platform_consistent_with_system() {
338 let platform = detect_platform();
339 let system = detect_system();
340 match platform {
341 Platform::Darwin => assert!(system.ends_with("-darwin")),
342 Platform::Linux => assert!(system.ends_with("-linux")),
343 }
344 }
345
346 #[test]
347 fn test_runtime_arch_returns_known() {
348 let arch = runtime_arch();
349 assert!(
350 ["x86_64", "aarch64"].contains(&arch),
351 "runtime_arch returned unexpected: {arch}"
352 );
353 }
354
355 #[test]
356 fn test_platform_display() {
357 assert_eq!(format!("{}", Platform::Darwin), "macOS");
358 assert_eq!(format!("{}", Platform::Linux), "Linux");
359 }
360
361 #[test]
362 fn test_default_repo_name() {
363 let name = default_repo_name();
364 assert!(name == "macos-nix" || name == "nix-config");
365 }
366}