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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
"""Manage the single global Adom symbol library (Adom.kicad_sym).
All symbols installed by Adom Desktop live in one KiCad symbol
library file. This module handles:
- Creating the library file if it doesn't exist
- Registering it in sym-lib-table if not already there
- Adding/updating individual symbols inside the library
- Listing symbols currently in the library
"""
import re
from pathlib import Path
from lib_table import LibTable, LibEntry
# The one library name used everywhere
ADOM_LIB_NAME = "Adom"
ADOM_LIB_FILE = "Adom.kicad_sym"
ADOM_LIB_DESCRIPTION = "Adom Desktop - custom symbols"
# Regex to extract top-level (symbol "NAME" ...) blocks.
# Top-level symbols are indented with exactly one leading whitespace group
# (2 spaces typically) whereas sub-unit symbols have deeper indentation.
# We use a balanced-paren counter to find the closing paren.
_SYMBOL_NAME_RE = re.compile(r'^\s+\(symbol\s+"([^"]+)"', re.MULTILINE)
# Template for an empty Adom.kicad_sym
_EMPTY_LIB = (
"(kicad_symbol_lib\n"
' (version 20231120)\n'
' (generator "adom-desktop")\n'
")\n"
)
def _get_paths(kicad_info: dict) -> tuple[Path, Path] | None:
"""Return (sym_lib_table_path, adom_kicad_sym_path) or None."""
config_dir = kicad_info.get("config_dir")
user_dir = kicad_info.get("user_dir")
if not config_dir or not user_dir:
return None
table_path = Path(config_dir) / "sym-lib-table"
sym_dir = Path(user_dir) / "symbols"
sym_dir.mkdir(parents=True, exist_ok=True)
lib_path = sym_dir / ADOM_LIB_FILE
return table_path, lib_path
def ensure_adom_library(kicad_info: dict) -> dict:
"""Make sure Adom.kicad_sym exists and is registered.
Returns:
{
"exists": bool, # whether the file existed before this call
"registered": bool, # whether it was already in sym-lib-table
"created": bool, # whether we had to create the file
"symbols": [str], # symbol names currently in the library
}
"""
if not kicad_info.get("installed"):
return {"exists": False, "registered": False, "created": False, "symbols": [],
"error": "KiCad not installed"}
paths = _get_paths(kicad_info)
if not paths:
return {"exists": False, "registered": False, "created": False, "symbols": [],
"error": "KiCad config/user directory not found"}
table_path, lib_path = paths
# --- Ensure the .kicad_sym file exists ---
existed = lib_path.exists()
if not existed:
lib_path.write_text(_EMPTY_LIB, encoding="utf-8")
# --- Ensure it's registered in sym-lib-table ---
table = LibTable.parse_file(str(table_path))
was_registered = table.has_library(ADOM_LIB_NAME)
if not was_registered:
uri = str(lib_path).replace("\\", "/")
entry = LibEntry(
name=ADOM_LIB_NAME,
lib_type="KiCad",
uri=uri,
options="",
descr=ADOM_LIB_DESCRIPTION,
)
table.add_library(entry)
table.write_file(str(table_path))
# --- List current symbols ---
symbols = list_symbols(lib_path)
return {
"exists": existed,
"registered": was_registered,
"created": not existed,
"symbols": symbols,
}
def list_symbols(lib_path: Path) -> list[str]:
"""Return the names of all top-level symbols in a .kicad_sym file."""
if not lib_path.exists():
return []
text = lib_path.read_text(encoding="utf-8")
# Top-level symbols: (symbol "NAME" at indentation level 1 (2 spaces)
# Sub-units like "NAME_0_0" are children — we skip those.
names = []
for m in _SYMBOL_NAME_RE.finditer(text):
name = m.group(1)
# Skip sub-unit symbols (contain _N_N suffix pattern)
if not re.search(r"_\d+_\d+$", name):
names.append(name)
return names
def _extract_symbol_block(text: str, start_pos: int) -> str:
"""Extract a complete (symbol ...) block starting from start_pos.
Uses balanced parenthesis counting to find the matching close paren.
"""
depth = 0
i = start_pos
while i < len(text):
if text[i] == "(":
depth += 1
elif text[i] == ")":
depth -= 1
if depth == 0:
return text[start_pos : i + 1]
i += 1
# Shouldn't happen with valid files, but return what we have
return text[start_pos:]
def _extract_top_level_symbols(text: str) -> list[tuple[str, str]]:
"""Extract all top-level (symbol "NAME" ...) blocks from a .kicad_sym.
Returns list of (name, full_block_text) tuples.
Each block includes the symbol and all its sub-units.
"""
results = []
# Find all top-level symbol openings (indented with exactly 2 spaces)
for m in re.finditer(r'^ \(symbol "([^"]+)"', text, re.MULTILINE):
name = m.group(1)
# Skip sub-unit symbols
if re.search(r"_\d+_\d+$", name):
continue
block = _extract_symbol_block(text, m.start())
results.append((name, block))
return results
def add_symbol(kicad_info: dict, symbol_text: str) -> dict:
"""Add or update a symbol in Adom.kicad_sym.
Args:
kicad_info: KiCad detection info dict.
symbol_text: Full content of a .kicad_sym file containing the symbol(s).
Returns:
{
"success": bool,
"added": [str], # names of symbols added/updated
"lib_path": str, # path to Adom.kicad_sym
}
"""
# Ensure library exists first
ensure_result = ensure_adom_library(kicad_info)
if ensure_result.get("error"):
return {"success": False, "error": ensure_result["error"]}
paths = _get_paths(kicad_info)
if not paths:
return {"success": False, "error": "KiCad paths not found"}
_, lib_path = paths
# Extract symbols from incoming text
incoming_symbols = _extract_top_level_symbols(symbol_text)
if not incoming_symbols:
return {"success": False, "error": "No symbols found in the provided file content"}
# Read the existing library
existing_text = lib_path.read_text(encoding="utf-8")
# Extract existing symbols
existing_symbols = _extract_top_level_symbols(existing_text)
existing_by_name = {name: block for name, block in existing_symbols}
# Merge: update existing, add new
added_names = []
for name, block in incoming_symbols:
existing_by_name[name] = block
added_names.append(name)
# Rebuild the library file
# Parse header (version + generator lines)
header_match = re.match(
r'(\(kicad_symbol_lib\s*\n(?:\s+\(version[^\)]+\)\s*\n)?(?:\s+\(generator[^\)]+\)\s*\n)?)',
existing_text,
)
if header_match:
header = header_match.group(1)
else:
header = "(kicad_symbol_lib\n (version 20231120)\n (generator \"adom-desktop\")\n"
# Build new file content
parts = [header]
for _name, block in existing_by_name.items():
parts.append(f" {block.strip()}\n")
parts.append(")\n")
new_text = "".join(parts)
lib_path.write_text(new_text, encoding="utf-8")
return {
"success": True,
"added": added_names,
"lib_path": str(lib_path),
}