master
John Lauer 0.9.340: Live routing verbs through KiCad's IPC API (routing_state, route_net, remove_route, routing_validate), offline… 66ec283 2mo ago
"""kicad_ipc_api: read or flip KiCad's IPC API server switch (Preferences > Plugins).

The live routing verbs need `api.enable_server` in kicad_common.json. KiCad reads
that file at start, so a change needs KiCad closed and relaunched; this verb
never restarts KiCad itself and says so in needsRestart.
"""
from __future__ import annotations

import json
import os
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path


def _settings_roots() -> list[Path]:
    if sys.platform == "win32":
        base = os.environ.get("APPDATA")
        return [Path(base) / "kicad"] if base else []
    if sys.platform == "darwin":
        return [Path.home() / "Library" / "Preferences" / "kicad"]
    return [Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "kicad"]


def _common_files() -> list[Path]:
    found = []
    for root in _settings_roots():
        if root.is_dir():
            found += sorted(root.glob("*/kicad_common.json"))
    def key(p):
        try:
            return tuple(int(x) for x in p.parent.name.split("."))
        except ValueError:
            return (0,)
    return sorted(found, key=key, reverse=True)


def handle_ipc_api(info: dict, args: dict) -> dict:
    files = _common_files()
    if not files:
        return {"success": False, "errorCode": "settings_not_found",
                "error": "No kicad_common.json found; start KiCad once so it writes its settings",
                "_hint": "KiCad creates <settings>/<version>/kicad_common.json on first launch."}
    target = files[0]
    try:
        settings = json.loads(target.read_text(encoding="utf-8"))
    except Exception as exc:
        return {"success": False, "errorCode": "settings_unreadable", "error": f"{target}: {exc}"}
    api = settings.get("api") if isinstance(settings.get("api"), dict) else {}
    before = bool(api.get("enable_server"))
    enable = args.get("enable")
    result = {"success": True, "file": str(target), "kicadSettingsVersion": target.parent.name,
              "enabled": before, "changed": False, "needsRestart": False}
    if enable is not None and not isinstance(enable, bool):
        return {"success": False, "errorCode": "invalid_arg", "error": "enable must be true or false"}
    if enable is None or enable == before:
        result["_hint"] = ("IPC API server is " + ("enabled" if before else "disabled") +
                           ". Pass enable:true|false to change it (KiCad must then be restarted).")
        return result
    backup = target.with_name(target.name + ".adom-bak")
    shutil.copy2(target, backup)
    settings.setdefault("api", {})["enable_server"] = enable
    tmp = target.with_name(target.name + ".adom-tmp")
    tmp.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
    os.replace(tmp, target)
    result.update(enabled=enable, changed=True, needsRestart=True, backup=str(backup),
                  changedAt=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
                  _hint="Written to kicad_common.json. KiCad reads it at start: kicad_close, then launch "
                        "again, then kicad_routing_state will connect. A running KiCad that saves its "
                        "preferences on exit overwrites this change, so close KiCad first when in doubt.")
    return result