#!/usr/bin/env python3 """ PPT Master - Icon Sync Copy chosen library icons into a project's own `icons/` folder at the moment they are selected. Run it with the icon names you are picking; each is copied from the global library into `/icons//`. Any name the library does not have is reported on the spot and the command exits non-zero — so you re-pick a valid icon then, not at export time. Over-copying candidates is fine: finalize only embeds the icons actually referenced by ``, the rest sit unused. Custom icons you place in `/icons//` yourself are honored too — a name already present in the project is treated as satisfied, not missing. Usage: python3 scripts/icon_sync.py [ ...] Examples: python3 scripts/icon_sync.py projects/deck chunk-filled/home tabler-outline/chart python3 scripts/icon_sync.py projects/deck simple-icons/github Dependencies: None (standard library only). See references/executor-base.md §4 and templates/icons/README.md. """ from __future__ import annotations import argparse import shutil import sys from pathlib import Path from typing import Optional from console_encoding import configure_utf8_stdio configure_utf8_stdio() _LIB_ALIASES = {"chunk": "chunk-filled"} _GLOBAL_ICONS_DIR = Path(__file__).resolve().parent.parent / "templates" / "icons" def _split_name(icon_name: str) -> tuple[str, str]: """`lib/name` -> (lib, name), applying the chunk→chunk-filled alias.""" if "/" not in icon_name: # legacy un-prefixed names live in chunk-filled/ return "chunk-filled", icon_name lib, name = icon_name.split("/", 1) return _LIB_ALIASES.get(lib, lib), name def sync_icons(project_path: Path, icon_names: list[str], global_dir: Path = _GLOBAL_ICONS_DIR) -> tuple[list[str], list[str]]: """Copy each `lib/name` from the global library into `/icons/`. Returns (copied, missing). A name already present in the project (e.g. a custom icon) counts as satisfied, not missing. """ project_icons = project_path / "icons" copied: list[str] = [] missing: list[str] = [] for raw in icon_names: lib, name = _split_name(raw) src = global_dir / lib / f"{name}.svg" dst = project_icons / lib / f"{name}.svg" if src.is_file(): dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) copied.append(f"{lib}/{name}") elif dst.is_file(): copied.append(f"{lib}/{name} (already in project)") else: missing.append(f"{lib}/{name}") return copied, missing def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Copy chosen library icons into a project's icons/ folder; report missing ones.", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("project_path", help="Project directory") parser.add_argument("icons", nargs="+", help="Icon names to copy, e.g. chunk-filled/home") return parser def main(argv: Optional[list[str]] = None) -> int: args = build_parser().parse_args(argv) project = Path(args.project_path) if not project.is_dir(): print(f"[ERROR] project not found: {project}", file=sys.stderr) return 1 copied, missing = sync_icons(project, args.icons) if copied: print(f"[OK] {len(copied)} icon(s) in {project / 'icons'}:", file=sys.stderr) for c in copied: print(f" + {c}", file=sys.stderr) if missing: print(f"\n[MISSING] {len(missing)} icon(s) not in the library — re-pick before continuing:", file=sys.stderr) for m in missing: lib = m.split("/", 1)[0] print(f" ✗ {m} (search: ls {_GLOBAL_ICONS_DIR}/{lib}/ | grep )", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())