app
Adom Desktop - KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
"""Handler for install_library command."""
import re
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from lib_table import LibTable, LibEntry
# Map library type to configuration
LIB_TYPE_CONFIG = {
"symbol": {
"table_file": "sym-lib-table",
"table_type": "sym_lib_table",
"user_subdir": "symbols",
"kicad_type": "KiCad",
},
"footprint": {
"table_file": "fp-lib-table",
"table_type": "fp_lib_table",
"user_subdir": "footprints",
"kicad_type": "KiCad",
},
}
def _install_footprint(kicad_info: dict, args: dict) -> dict:
"""Install a footprint library (.kicad_mod → .pretty directory)."""
lib_path = args.get("libraryPath", "")
lib_name = args.get("libraryName", "")
description = args.get("description", "Installed by Adom Desktop")
model_3d_path = args.get("model3dPath", "")
source = Path(lib_path)
if not source.exists():
return {
"success": False,
"error": f"Footprint source not found: {lib_path}",
"_hint": "The path may exist on Docker but not on the Windows host. Use push_file to copy from Docker first, or verify the host-side path.",
}
config_dir = kicad_info.get("config_dir")
user_dir = kicad_info.get("user_dir")
if not config_dir or not user_dir:
return {"success": False, "error": "KiCad config/user directory not found"}
# Footprint libraries are .pretty directories containing .kicad_mod files
footprints_dir = Path(user_dir) / "footprints"
footprints_dir.mkdir(parents=True, exist_ok=True)
if source.is_dir() and source.suffix == ".pretty":
# Source is already a .pretty directory — copy as-is
if not lib_name:
lib_name = source.stem
pretty_dir = footprints_dir / source.name
if pretty_dir.exists():
shutil.rmtree(pretty_dir)
shutil.copytree(str(source), str(pretty_dir))
elif source.suffix == ".kicad_mod":
# Single .kicad_mod file — wrap in a .pretty directory
if not lib_name:
lib_name = source.stem
pretty_name = f"{lib_name}.pretty"
pretty_dir = footprints_dir / pretty_name
pretty_dir.mkdir(parents=True, exist_ok=True)
# KiCad requires the .kicad_mod filename to match the footprint name
# declared inside the file. Extract it and rename if needed.
fp_text = source.read_text(encoding="utf-8")
fp_name_match = re.search(r'\(footprint "([^"]+)"', fp_text)
if fp_name_match:
correct_name = f"{fp_name_match.group(1)}.kicad_mod"
else:
correct_name = source.name
dest_file = pretty_dir / correct_name
shutil.copy2(str(source), str(dest_file))
else:
return {
"success": False,
"error": f"Invalid footprint source: expected .kicad_mod file or .pretty directory, got {source.name}",
}
# Install 3D model if provided
model_installed_path = ""
if model_3d_path:
model_source = Path(model_3d_path)
if model_source.exists():
models_dir = Path(user_dir) / "3dmodels" / f"{lib_name}.3dshapes"
models_dir.mkdir(parents=True, exist_ok=True)
model_dest = models_dir / model_source.name
shutil.copy2(str(model_source), str(model_dest))
model_installed_path = str(model_dest).replace("\\", "/")
# Update the footprint file to add the 3D model reference
if source.suffix == ".kicad_mod":
_add_model_to_footprint(pretty_dir / source.name, model_installed_path)
# Register in fp-lib-table
table_path = Path(config_dir) / "fp-lib-table"
table = LibTable.parse_file(str(table_path))
uri = str(pretty_dir).replace("\\", "/")
if not table.has_library(lib_name):
entry = LibEntry(
name=lib_name,
lib_type="KiCad",
uri=uri,
options="",
descr=description,
)
table.add_library(entry)
table.write_file(str(table_path))
registered = True
else:
registered = False
result = {
"success": True,
"output": f"Footprint library '{lib_name}' installed to {pretty_dir}"
+ (f" (already registered)" if not registered else f" and registered in fp-lib-table"),
"data": {
"name": lib_name,
"path": str(pretty_dir),
"table": "fp-lib-table",
},
"_hint": (
f"✅ Footprint library '{lib_name}' is now in fp-lib-table. "
f"For next-time confidence, run `kicad_lint_library '{{\"libraryPath\":\"<path-to-.pretty-dir>\"}}'` "
f"BEFORE install to validate each .kicad_mod inside in ~100ms (catches malformed files, "
f"outdated formats). Any open KiCad windows need to be closed + reopened to pick up the new "
f"fp-lib-table entry."
),
}
if model_installed_path:
result["data"]["model3dPath"] = model_installed_path
result["output"] += f". 3D model installed to {model_installed_path}"
return result
def _add_model_to_footprint(footprint_path: Path, model_path: str):
"""Add a (model ...) reference to a .kicad_mod file."""
text = footprint_path.read_text(encoding="utf-8")
# Remove any existing model reference
text = re.sub(r'\s*\(model\s+"[^"]*"[^)]*\(offset[^)]*\)[^)]*\(scale[^)]*\)[^)]*\(rotate[^)]*\)\s*\)', '', text)
# Insert model reference before the final closing paren
model_block = (
f'\n (model "{model_path}"\n'
f' (offset (xyz 0 0 0))\n'
f' (scale (xyz 1 1 1))\n'
f' (rotate (xyz 0 0 0))\n'
f' )'
)
# Find last ) and insert before it
idx = text.rfind(")")
if idx >= 0:
text = text[:idx] + model_block + "\n" + text[idx:]
footprint_path.write_text(text, encoding="utf-8")
def handle_install_library(kicad_info: dict, args: dict) -> dict:
"""Install a library into KiCad's global lib table."""
if not kicad_info.get("installed"):
return {
"success": False,
"error": "KiCad not installed",
"_hint": "Report to the user and ask them to install KiCad from https://www.kicad.org/download/, then retry.",
}
lib_path = args.get("libraryPath", "")
lib_type = args.get("libraryType", "symbol")
if not lib_path:
return {"success": False, "error": "No libraryPath specified"}
# Footprints need special handling (.pretty directories)
if lib_type == "footprint":
return _install_footprint(kicad_info, args)
# --- Symbol library install (original logic) ---
lib_name = args.get("libraryName", "")
description = args.get("description", "Installed by Adom Desktop")
source = Path(lib_path)
if not source.exists():
return {
"success": False,
"error": f"Library source not found: {lib_path}",
"_hint": "The path may exist on Docker but not on the Windows host. Use push_file to copy from Docker first, or verify the host-side path.",
}
if not lib_name:
lib_name = source.stem
type_config = LIB_TYPE_CONFIG.get(lib_type)
if not type_config:
return {
"success": False,
"error": f"Unknown library type: {lib_type}. Use 'symbol' or 'footprint'",
}
config_dir = kicad_info.get("config_dir")
if not config_dir:
return {"success": False, "error": "KiCad config directory not found"}
user_dir = kicad_info.get("user_dir")
if not user_dir:
return {"success": False, "error": "KiCad user directory not found"}
# Copy library file to user's library directory
dest_dir = Path(user_dir) / type_config["user_subdir"]
dest_dir.mkdir(parents=True, exist_ok=True)
if source.is_dir():
dest = dest_dir / source.name
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(str(source), str(dest))
else:
dest = dest_dir / source.name
shutil.copy2(str(source), str(dest))
# Parse the lib table
table_path = Path(config_dir) / type_config["table_file"]
table = LibTable.parse_file(str(table_path))
# Use forward slashes for KiCad compatibility
uri = str(dest).replace("\\", "/")
entry = LibEntry(
name=lib_name,
lib_type=type_config["kicad_type"],
uri=uri,
options="",
descr=description,
)
if table.has_library(lib_name):
return {
"success": False,
"error": f"Library '{lib_name}' already exists in {type_config['table_file']}",
"_hint": "Choose a different libraryName, or call list_symbol_libraries / list_footprint_libraries to see existing entries.",
}
table.add_library(entry)
table.write_file(str(table_path))
return {
"success": True,
"output": f"Library '{lib_name}' installed to {dest} and registered in {type_config['table_file']}",
"data": {
"name": lib_name,
"path": str(dest),
"table": type_config["table_file"],
},
"_hint": (
f"✅ Library '{lib_name}' is now registered. For next-time confidence, run "
f"`kicad_lint_library '{{\"libraryPath\":\"<path>\"}}'` BEFORE install to "
f"catch malformed files, outdated formats, or wrong-filetype-renamed-with-"
f".kicad_sym surprises in ~100ms — much faster than discovering a broken "
f"library inside the GUI later. Any KiCad windows already open need to be "
f"closed + reopened to pick up the new library entry (sym-lib-table is read at startup)."
),
}