返回 ppt-master
icon_sync.py
根目录 / skills / ppt-master / scripts / icon_sync.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Icon Sync
4
5 Copy chosen library icons into `<project>/icons/<lib>/` when selected. Missing
6 names exit non-zero before export. Known basenames need no separate existence
7 check; search the chosen library only for unresolved concepts.
8
9 Project-local custom icons count as satisfied. In one resource-selection batch,
10 `simple-icons` may accompany one of the four stylistic libraries for
11 real brand marks.
12
13 Usage:
14 python3 scripts/icon_sync.py <project_path> <lib/name> [<lib/name> ...]
15
16 Examples:
17 python3 scripts/icon_sync.py projects/deck tabler-outline/home tabler-outline/chart
18 python3 scripts/icon_sync.py projects/deck tabler-outline/home simple-icons/github
19
20 Dependencies:
21 None (standard library only).
22
23 See references/executor-base.md §4 and templates/icons/README.md.
24 """
25
26 from __future__ import annotations
27
28 import argparse
29 import shutil
30 import sys
31 from pathlib import Path
32 from typing import Optional
33
34 from console_encoding import configure_utf8_stdio
35
36 configure_utf8_stdio()
37
38 _LIB_ALIASES = {"chunk": "chunk-filled"}
39 _STYLISTIC_LIBRARIES = {
40 "chunk-filled",
41 "phosphor-duotone",
42 "tabler-filled",
43 "tabler-outline",
44 }
45 _GLOBAL_ICONS_DIR = Path(__file__).resolve().parent.parent / "templates" / "icons"
46
47
48 def _split_name(icon_name: str) -> tuple[str, str]:
49 """`lib/name` -> (lib, name), applying the chunk→chunk-filled alias."""
50 if "/" not in icon_name:
51 # legacy un-prefixed names live in chunk-filled/
52 return "chunk-filled", icon_name
53 lib, name = icon_name.split("/", 1)
54 return _LIB_ALIASES.get(lib, lib), name
55
56
57 def sync_icons(project_path: Path, icon_names: list[str], global_dir: Path = _GLOBAL_ICONS_DIR) -> tuple[list[str], list[str]]:
58 """Copy each `lib/name` from the global library into `<project>/icons/`.
59
60 Returns (copied, missing). A name already present in the project (e.g. a
61 custom icon) counts as satisfied, not missing.
62 """
63 project_icons = project_path / "icons"
64 copied: list[str] = []
65 missing: list[str] = []
66
67 for raw in icon_names:
68 lib, name = _split_name(raw)
69 src = global_dir / lib / f"{name}.svg"
70 dst = project_icons / lib / f"{name}.svg"
71 if src.is_file():
72 dst.parent.mkdir(parents=True, exist_ok=True)
73 shutil.copy2(src, dst)
74 copied.append(f"{lib}/{name}")
75 elif dst.is_file():
76 copied.append(f"{lib}/{name} (already in project)")
77 else:
78 missing.append(f"{lib}/{name}")
79
80 return copied, missing
81
82
83 def build_parser() -> argparse.ArgumentParser:
84 parser = argparse.ArgumentParser(
85 description="Copy chosen library icons into a project's icons/ folder; report missing ones.",
86 formatter_class=argparse.RawDescriptionHelpFormatter,
87 )
88 parser.add_argument("project_path", help="Project directory")
89 parser.add_argument("icons", nargs="+", help="Icon names to copy, e.g. chunk-filled/home")
90 return parser
91
92
93 def main(argv: Optional[list[str]] = None) -> int:
94 args = build_parser().parse_args(argv)
95 project = Path(args.project_path)
96 if not project.is_dir():
97 print(f"[ERROR] project not found: {project}", file=sys.stderr)
98 return 1
99
100 requested_libraries = {_split_name(raw)[0] for raw in args.icons}
101 stylistic_libraries = sorted(requested_libraries & _STYLISTIC_LIBRARIES)
102 if len(stylistic_libraries) > 1:
103 print(
104 f"[ERROR] mixed stylistic icon libraries: {', '.join(stylistic_libraries)}",
105 file=sys.stderr,
106 )
107 print(
108 "Choose one of the four stylistic libraries per selection batch; "
109 "simple-icons may coexist for real brand marks.",
110 file=sys.stderr,
111 )
112 return 1
113
114 copied, missing = sync_icons(project, args.icons)
115
116 if copied:
117 print(f"[OK] {len(copied)} icon(s) in {project / 'icons'}:", file=sys.stderr)
118 for c in copied:
119 print(f" + {c}", file=sys.stderr)
120
121 if missing:
122 print(f"\n[MISSING] {len(missing)} icon(s) not in the library — re-pick before continuing:", file=sys.stderr)
123 for m in missing:
124 lib = m.split("/", 1)[0]
125 print(
126 f' ✗ {m} (search: rg --files "{_GLOBAL_ICONS_DIR / lib}" -g \'*<keyword>*.svg\')',
127 file=sys.stderr,
128 )
129 return 1
130
131 return 0
132
133
134 if __name__ == "__main__":
135 raise SystemExit(main())
136
136 lines PYTHON