app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
John Lauer
v0.1.9: full source tree (handlers/agent/extension/tools) + #2 ADDevelop detection + #3 busy-notice reaper
343fc40
1mo ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
"""rcs_catalog.py — parse Altium's *.rcs process-launcher tables into a command catalog.
Every menu/toolbar action in Altium is a "process launcher" line like
PL PLPCB:Undo Command='PCB:Undo' Caption='&Undo' Image='UNDO.PNG' Shortcut1='Ctrl+Z'
Shortcut2='Alt+Backspace' Params='' Description='Undo' End
The Command is what RunProcess / RunProcessWithParameters takes; Params is the
Name=Value|Name=Value string. AD 26.9.1 ships 47 .rcs files -> 3,205 launchers ->
1,109 distinct commands (PCB 211, Sch 190, WorkspaceManager 87, Client 50 …).
"""
from __future__ import annotations
import json
import re
import time
from pathlib import Path
_LINE = re.compile(r"^\s*PL\w*\s+(?P<id>\S+)\s+(?P<rest>.*)End\s*$")
_ATTR = re.compile(r"(\w+)='((?:[^']|'')*)'")
_cache: dict = {}
def parse_dir(system_dir: str) -> dict:
key = system_dir.lower()
c = _cache.get(key)
if c and time.time() - c["at"] < 3600:
return c["data"]
commands: dict[str, dict] = {}
files = 0
launchers = 0
for rcs in sorted(Path(system_dir).glob("*.rcs")) + sorted(Path(system_dir).glob("*.RCS")):
files += 1
try:
text = rcs.read_text(encoding="latin-1", errors="replace")
except Exception:
continue
for line in text.splitlines():
if line.lstrip().startswith("//"):
continue
m = _LINE.match(line)
if not m:
continue
launchers += 1
attrs = {k: v.replace("''", "'") for k, v in _ATTR.findall(m.group("rest"))}
cmd = attrs.get("Command")
if not cmd:
continue
e = commands.setdefault(cmd, {"command": cmd, "server": cmd.split(":", 1)[0],
"captions": [], "shortcuts": [], "params": [], "descriptions": [],
"sources": []})
for k, lst in (("Caption", "captions"), ("Description", "descriptions"), ("Params", "params")):
v = attrs.get(k, "")
v = v.replace("&", "") if k == "Caption" else v
if v and v not in e[lst]:
e[lst].append(v)
for k in ("Shortcut1", "Shortcut2"):
v = attrs.get(k, "")
if v and v not in e["shortcuts"]:
e["shortcuts"].append(v)
if rcs.name not in e["sources"]:
e["sources"].append(rcs.name)
servers: dict[str, int] = {}
for c in commands.values():
servers[c["server"]] = servers.get(c["server"], 0) + 1
data = {"systemDir": system_dir, "files": files, "launchers": launchers,
"distinctCommands": len(commands), "servers": dict(sorted(servers.items(), key=lambda kv: -kv[1])),
"commands": commands}
_cache[key] = {"at": time.time(), "data": data}
return data
def search(system_dir: str, query: str = "", server: str | None = None, limit: int = 50) -> dict:
data = parse_dir(system_dir)
q = (query or "").lower()
hits = []
for cmd, e in data["commands"].items():
if server and e["server"].lower() != server.lower():
continue
hay = " ".join([cmd] + e["captions"] + e["descriptions"] + e["params"]).lower()
if q and q not in hay:
continue
hits.append({k: e[k] for k in ("command", "server", "captions", "shortcuts", "params", "descriptions")})
if len(hits) >= limit:
break
return {"query": query, "server": server, "count": len(hits), "total": data["distinctCommands"],
"servers": data["servers"], "hits": hits}