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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
"""S-expression parser/writer for KiCad library table files (sym-lib-table, fp-lib-table)."""
import re
from pathlib import Path
class LibEntry:
"""Represents a single library entry in a lib table."""
def __init__(self, name: str, lib_type: str, uri: str,
options: str = "", descr: str = ""):
self.name = name
self.lib_type = lib_type
self.uri = uri
self.options = options
self.descr = descr
def to_sexpr(self) -> str:
return (
f' (lib (name "{self.name}")'
f'(type "{self.lib_type}")'
f'(uri "{self.uri}")'
f'(options "{self.options}")'
f'(descr "{self.descr}"))'
)
class LibTable:
"""Represents a KiCad sym-lib-table or fp-lib-table."""
def __init__(self, table_type: str):
"""table_type is 'sym_lib_table' or 'fp_lib_table'."""
self.table_type = table_type
self.version: int | None = None # KiCad 9+ uses (version 7)
self.entries: list = []
@classmethod
def parse_file(cls, filepath: str) -> "LibTable":
"""Parse a lib table file. Creates empty table if file doesn't exist."""
path = Path(filepath)
if not path.exists():
if "sym" in path.name:
t = cls("sym_lib_table")
else:
t = cls("fp_lib_table")
t.version = 7 # KiCad 9+ expects (version 7)
return t
text = path.read_text(encoding="utf-8")
return cls.parse_string(text)
@classmethod
def parse_string(cls, text: str) -> "LibTable":
"""Parse S-expression text into a LibTable."""
m = re.match(r'\s*\((\w+)', text)
if not m:
raise ValueError("Cannot parse lib table: no root expression found")
table = cls(m.group(1))
# Parse (version N) if present (KiCad 9+)
ver_match = re.search(r'\(version\s+(\d+)\)', text)
if ver_match:
table.version = int(ver_match.group(1))
# Match each (lib ...) entry
lib_pattern = re.compile(
r'\(lib\s+'
r'\(name\s+"([^"]*)"\)'
r'\s*\(type\s+"([^"]*)"\)'
r'\s*\(uri\s+"([^"]*)"\)'
r'\s*\(options\s+"([^"]*)"\)'
r'\s*\(descr\s+"([^"]*)"\)'
r'\s*\)',
re.DOTALL
)
for m in lib_pattern.finditer(text):
entry = LibEntry(
name=m.group(1),
lib_type=m.group(2),
uri=m.group(3),
options=m.group(4),
descr=m.group(5),
)
table.entries.append(entry)
return table
def has_library(self, name: str) -> bool:
"""Check if a library with the given name exists."""
return any(e.name == name for e in self.entries)
def add_library(self, entry: LibEntry) -> bool:
"""Add a library entry. Returns False if name already exists."""
if self.has_library(entry.name):
return False
self.entries.append(entry)
return True
def remove_library(self, name: str) -> bool:
"""Remove a library by name. Returns True if found and removed."""
before = len(self.entries)
self.entries = [e for e in self.entries if e.name != name]
return len(self.entries) < before
def to_string(self) -> str:
"""Serialize back to S-expression format."""
lines = [f"({self.table_type}"]
if self.version is not None:
lines.append(f" (version {self.version})")
for entry in self.entries:
lines.append(entry.to_sexpr())
lines.append(")")
return "\n".join(lines) + "\n"
def write_file(self, filepath: str):
"""Write the table to a file, creating parent dirs if needed."""
path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(self.to_string(), encoding="utf-8")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
table = LibTable.parse_file(sys.argv[1])
print(f"Table type: {table.table_type}")
print(f"Entries: {len(table.entries)}")
for e in table.entries[:5]:
print(f" - {e.name}: {e.uri}")
if len(table.entries) > 5:
print(f" ... and {len(table.entries) - 5} more")
else:
print("Usage: python lib_table.py <path-to-lib-table-file>")