master
Colby Knox Restore 25 files to their state before PR #61: the wiki's pr revert left them truncated to the diff's context 4f3a0f4 8d ago
//! macOS implementation (owner: Kyle). Headless parts are complete: the app bundle's
//! kicad-cli, ~/Library/Preferences/kicad/<ver>, ~/Documents/KiCad/<ver>. Window control
//! on macOS is `open -a`, osascript and the Accessibility API; add them here in phase 3
//! and flip the capability flags.

use std::path::{Path, PathBuf};
use std::process::Command;

use serde_json::{json, Value};

use crate::{install_from_cli, major_minor, Capabilities, KicadInstall, Platform};

pub struct Native;

fn home() -> Option<PathBuf> {
    std::env::var("HOME").ok().map(PathBuf::from)
}

impl Platform for Native {
    fn capabilities(&self) -> Capabilities {
        Capabilities {
            os: "macos",
            owner: "Kyle (macOS Hydrogen and Adom Bridge)",
            kicad_cli: true,
            window_control: false,
            ui_automation: false,
            dialog_sweep: false,
            focus_etiquette: false,
            ipc_api: false,
            silent_install: false,
        }
    }
    fn cli_file_name(&self) -> &'static str {
        "kicad-cli"
    }
    fn find_installs(&self) -> Vec<KicadInstall> {
        let mut out = Vec::new();
        let mut candidates = vec![PathBuf::from("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli")];
        if let Some(h) = home() {
            candidates.push(h.join("Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli"));
        }
        for (i, c) in candidates.iter().enumerate() {
            if let Some(inst) = install_from_cli(c, "", "app bundle", i == 1) {
                out.push(inst);
            }
        }
        out
    }
    fn config_dir(&self, version: &str) -> Option<PathBuf> {
        Some(home()?.join("Library/Preferences/kicad").join(major_minor(version)))
    }
    fn user_dir(&self, version: &str) -> Option<PathBuf> {
        Some(home()?.join("Documents/KiCad").join(major_minor(version)))
    }
    fn quiet_command(&self, cmd: &mut Command) {
        cmd.stdin(std::process::Stdio::null());
    }
    fn config_root(&self) -> Option<PathBuf> {
        Some(home()?.join("Library/Preferences/kicad"))
    }
    fn user_root(&self) -> Option<PathBuf> {
        Some(home()?.join("Documents/KiCad"))
    }
    fn documents_dir(&self) -> Option<PathBuf> {
        Some(home()?.join("Documents"))
    }
    fn process_alive(&self, pid: u32) -> Option<bool> {
        let mut cmd = Command::new("kill");
        cmd.args(["-0", &pid.to_string()]);
        self.quiet_command(&mut cmd);
        Some(cmd.output().ok()?.status.success())
    }

    // ---- Phase 3b: kicad_upgrade. No silent installer path here: KiCad on macOS comes
    // from the .dmg on downloads.kicad.org or Homebrew. The verb explains that; these four keep the facts honest.

    fn elevation_info(&self) -> Result<Value, String> {
        let mut cmd = Command::new("id");
        cmd.arg("-u");
        self.quiet_command(&mut cmd);
        let euid: Option<u32> = cmd.output().ok().filter(|o| o.status.success()).and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok());
        let root = euid.map(|u| u == 0);
        Ok(json!({
            "platform": "darwin",
            "user": std::env::var("USER").or_else(|_| std::env::var("LOGNAME")).unwrap_or_default(),
            "euid": euid,
            "isAdmin": root,
            "uacEnabled": Value::Null,
            "consentPrompt": Value::Null,
            "canInstallAllUsersSilently": root,
            "note": "no UAC on macOS; isAdmin is euid == 0 (a root shell), which a normal Hydrogen session never is",
        }))
    }
    fn installer_kind(&self) -> &'static str {
        "dmg"
    }
    fn installer_download_url(&self, _version: &str) -> Option<String> {
        None
    }

    // ---- Freerouting: the 2.4.1 macOS release is a dmg, which the bridge does not mount
    // or drive. Kyle: a zip or a plain app bundle asset would slot in here like linux.rs.
    // Until then the user installs Freerouting themselves and sets ADOM_FREEROUTING_EXE.

    fn freerouting_bundle(&self) -> Option<(String, u64, &'static str)> {
        None
    }
    fn freerouting_extract(&self, _archive: &Path, _dest: &Path, _unzip: &dyn Fn(&Path, &Path) -> Result<(), String>) -> Result<PathBuf, String> {
        Err("Freerouting on macOS ships as a dmg; the bridge does not drive dmg installs (owner: Kyle). Install it yourself and set ADOM_FREEROUTING_EXE.".into())
    }
}