app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
"""launch.py — cold-start KiCad (the project manager) from nothing.
Fills the gap found in issue #19: `kicad_open_editors` only INVENTORIES editors
inside already-running KiCad instances, and every other open_* verb launches a
specific editor. This is the verb whose one job is "KiCad is closed; open it."
Windows-first (Popen on kicad.exe with a no-activate startupinfo so the window
opens in the BACKGROUND per the bridge's focus rules); macOS falls back to
`open -a KiCad`.
"""
import os
import subprocess
import sys
import time
from pathlib import Path
IS_WINDOWS = sys.platform == "win32"
def _kicad_windows() -> list:
try:
from handlers.kicad_ui import _find_all_kicad_windows
return _find_all_kicad_windows()
except Exception:
return []
def handle_launch(kicad_info: dict, args: dict) -> dict:
"""Cold-start KiCad's project manager (idempotent).
Args (all optional):
project: path to a .kicad_pro to open with it
waitSeconds: how long to wait for a KiCad window (default 15)
"""
existing = _kicad_windows()
if existing:
return {
"success": True,
"alreadyRunning": True,
"windows": [w.get("title") for w in existing][:10],
"output": f"KiCad is already running ({len(existing)} window(s)) — nothing launched.",
"_hint": ("Idempotent no-op: KiCad was already up. Use kicad_open_editors to inventory "
"editors, or the open_* verbs to open a specific one."),
}
project = (args.get("project") or "").strip()
if project and not Path(project).exists():
return {"success": False, "error": f"Project not found: {project}",
"_hint": "Absolute Windows path required; %VAR% expansion is not performed."}
if not IS_WINDOWS:
try:
cmd = ["open", "-a", "KiCad"] + ([project] if project else [])
_p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
from handlers.win_focus import register_owned_pid
register_owned_pid(_p.pid)
except Exception:
pass
return {"success": True, "launched": True,
"output": "KiCad launch requested via `open -a KiCad`."}
except Exception as e:
return {"success": False, "error": f"Failed to launch KiCad: {e}"}
exe = kicad_info.get("kicad_exe")
if not exe or not Path(exe).exists():
return {
"success": False,
"error": "kicad.exe not found",
"_hint": ("No KiCad installation detected. kicad_readiness reports install state; "
"kicad_upgrade installs KiCad silently (per-user, no UAC)."),
}
_si = None
try:
from handlers import win_focus
_si = win_focus.background_startupinfo()
except Exception:
pass
# CAPTURE KICAD'S OWN DIAGNOSTICS INSTEAD OF DISCARDING THEM.
#
# This used to be stdout=DEVNULL, stderr=DEVNULL. KiCad writes no log file:
# everything it has to say about itself goes to stderr via wxLogError /
# wxLogTrace, and a Windows GUI process has no console, so DEVNULL meant the
# ONLY channel KiCad has for reporting its own failures was being thrown away
# by the very thing driving it. A failed board load could then only be found
# by screenshotting a modal dialog and reading the pixels.
#
# KICAD_ENABLE_WXTRACE=1 is required in a RELEASE build or wx logging is off
# entirely (common/single_top.cpp:202, kicad/kicad.cpp:579). Errors and
# warnings need no WXTRACE mask; masks only add the trace firehose.
_log_path = None
_log_fh = None
try:
from handlers.kicad_log import new_log_path, trace_env
_log_path = new_log_path("launch")
_log_fh = open(_log_path, "ab", buffering=0)
_env = trace_env(args.get("traceMasks"))
except Exception: # never let logging break launching
_log_fh, _env = None, None
try:
_p = subprocess.Popen([exe] + ([project] if project else []),
stdout=(_log_fh or subprocess.DEVNULL),
stderr=(_log_fh or subprocess.DEVNULL),
env=_env,
startupinfo=_si)
try:
from handlers.win_focus import register_owned_pid
register_owned_pid(_p.pid)
except Exception:
pass
except Exception as e:
return {"success": False, "error": f"Failed to launch KiCad: {e}"}
# The parent's copy of the handle is not needed once the child owns it; the
# child keeps writing after we close ours.
try:
if _log_fh:
_log_fh.close()
except Exception:
pass
wait_s = float(args.get("waitSeconds") or 15)
deadline = time.monotonic() + max(3.0, wait_s)
windows: list = []
while time.monotonic() < deadline:
windows = _kicad_windows()
if windows:
break
time.sleep(0.5)
if windows:
return {
"success": True,
"launched": True,
"windows": [w.get("title") for w in windows][:10],
"output": f"KiCad launched — {len(windows)} window(s) up.",
"logPath": str(_log_path) if _log_path else None,
"_hint": ("Launched in the BACKGROUND (no focus steal). First run on a fresh install "
"may show a setup wizard — kicad_window_info self-heals benign dialogs; "
"kicad_screenshot_all shows every window if something looks stuck."),
"_next": ["kicad_window_info", "kicad_open_editors", "kicad_open_board"],
}
return {
"success": False,
"logPath": str(_log_path) if _log_path else None,
"error": f"kicad.exe started but no KiCad window appeared within {int(wait_s)}s",
"_hint": ("Cold start on a slow box (or software-OpenGL) can exceed the wait — retry "
"kicad_window_info in a few seconds before treating this as a failure. "
"A modal (crash-recovery / first-run) can also hold the window back: "
"kicad_screenshot_all will show it."),
"statusVerb": "kicad_window_info",
}