import json, os, re, base64 from app_inventory import add_inventory_apps, add_nerd_icons_app, apply_default_face_seeds, apply_package_overrides, face_rows from default_faces import DefaultFaces from face_data import * from face_specs import face_spec, ui_face_spec, migrate_legacy HERE=os.path.dirname(os.path.abspath(__file__)) def read_text(name): with open(os.path.join(HERE,name)) as src: return src.read() def read_json(name): return json.loads(read_text(name)) NERD_ICONS_LEGEND_FIELDS = ("key", "label", "face", "category", "glyph") NERD_ICONS_GALLERY_GLYPH_FIELDS = ("glyph", "name") _NO_ARTIFACT = object() # distinguishes absent/malformed from a file that parsed to null def _load_nerd_icons_artifact(path, kind, tail): """Open and JSON-parse the nerd-icons artifact at PATH. Return the parsed value, or _NO_ARTIFACT (with a KIND/TAIL-labeled warning) when absent or malformed. Shared skeleton for the legend and gallery loaders.""" if not os.path.exists(path): print(f"WARNING: nerd-icons {kind} absent ({path}); {tail}") return _NO_ARTIFACT try: with open(path) as src: return json.load(src) except (json.JSONDecodeError, OSError) as exc: print(f"WARNING: nerd-icons {kind} malformed ({path}: {exc}); {tail}") return _NO_ARTIFACT def load_nerd_icons_legend(path=None): """Return the nerd-icons legend rows, or None when the artifact is unusable. The legend is captured by build-nerd-icons-legend.el into nerd-icons-legend.json. The artifact is a JSON object {legend, gallery}; a legacy bare array is read as the legend directly (back-compat). Absent, malformed, empty, or carrying a row without all five string fields (key/label/face/category/glyph) -> None, with a warning, so the caller falls back to the generic nerd-icons app instead of erroring. nerd-icons not being installed at capture time yields an empty/absent file, which lands here as None. """ path = path or os.path.join(HERE, "nerd-icons-legend.json") data = _load_nerd_icons_artifact(path, "legend", "generic nerd-icons app") if data is _NO_ARTIFACT: return None rows = data.get("legend") if isinstance(data, dict) else data if not isinstance(rows, list) or not rows: print(f"WARNING: nerd-icons legend empty ({path}); generic nerd-icons app") return None for row in rows: if not (isinstance(row, dict) and all(isinstance(row.get(f), str) and row.get(f) for f in NERD_ICONS_LEGEND_FIELDS)): print(f"WARNING: nerd-icons legend row invalid ({row!r}); generic nerd-icons app") return None return rows def load_nerd_icons_gallery(path=None): """Return the nerd-icons gallery groups, or None when absent/unusable. The gallery (the full colored catalog) rides nerd-icons-legend.json under the "gallery" key: a list of {face, hue, glyphs:[{glyph,name}]} groups captured by build-nerd-icons-legend.el, one group per color face, ordered by hue. A legacy array-only artifact (legend, no gallery), an absent/malformed file, or a structurally invalid group -> None, so the caller simply omits the gallery while the legend data still loads. Never raises. """ path = path or os.path.join(HERE, "nerd-icons-legend.json") data = _load_nerd_icons_artifact(path, "gallery", "legend without gallery") if data is _NO_ARTIFACT: return None groups = data.get("gallery") if isinstance(data, dict) else None if not isinstance(groups, list) or not groups: return None # legacy/array-only artifact: legend present, no gallery — not an error for group in groups: if not (isinstance(group, dict) and isinstance(group.get("face"), str) and group["face"].startswith("nerd-icons-") and isinstance(group.get("hue"), (int, float)) and isinstance(group.get("glyphs"), list) and group["glyphs"]): print(f"WARNING: nerd-icons gallery group invalid ({group!r}); legend without gallery") return None for entry in group["glyphs"]: if not (isinstance(entry, dict) and all(isinstance(entry.get(f), str) and entry.get(f) for f in NERD_ICONS_GALLERY_GLYPH_FIELDS)): print(f"WARNING: nerd-icons gallery glyph invalid ({entry!r}); legend without gallery") return None return groups def strip_exports(src): """Drop ES-module `export`/`import` lines so the body loads as a classic