From 19780fa994a697966984366a54dcdfbdb7e7838c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 13 Jun 2026 15:27:54 -0500 Subject: Add theme studio default face drift summary --- scripts/theme-studio/default-face-summary.py | 30 ++++++++++++++++++++ scripts/theme-studio/default_faces.py | 32 ++++++++++++++++++++++ scripts/theme-studio/test_generate.py | 41 +++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 scripts/theme-studio/default-face-summary.py diff --git a/scripts/theme-studio/default-face-summary.py b/scripts/theme-studio/default-face-summary.py new file mode 100644 index 00000000..4a163eb4 --- /dev/null +++ b/scripts/theme-studio/default-face-summary.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Print a concise summary of theme-studio's captured Emacs default faces.""" + +from __future__ import annotations + +import json +import pathlib +import sys + +from default_faces import DefaultFaces, changed_summary + + +HERE = pathlib.Path(__file__).resolve().parent + + +def main() -> None: + paths = [pathlib.Path(p) for p in sys.argv[1:]] + if not paths: + paths = [HERE / "emacs-default-faces.json"] + summaries = [DefaultFaces.from_path(path).summary() for path in paths] + if len(summaries) == 1: + print(json.dumps(summaries[0], indent=2, sort_keys=True)) + elif len(summaries) == 2: + print(json.dumps(changed_summary(summaries[0], summaries[1]), indent=2, sort_keys=True)) + else: + raise SystemExit("usage: default-face-summary.py [snapshot.json [snapshot-after.json]]") + + +if __name__ == "__main__": + main() diff --git a/scripts/theme-studio/default_faces.py b/scripts/theme-studio/default_faces.py index a2fd2720..ce2bf319 100644 --- a/scripts/theme-studio/default_faces.py +++ b/scripts/theme-studio/default_faces.py @@ -99,6 +99,30 @@ class DefaultFaces: return fallback return self.color_names.get(str(value).lower(), fallback) + def summary(self) -> dict[str, Any]: + if not self.data: + return {} + inventory = self.data.get("package-inventory", {}) + package_faces = sorted({face for faces in inventory.values() for face in faces}) + package_inherits = { + face: self.seed(face).get("inherit") + for face in package_faces + if self.seed(face).get("inherit") + } + ui_faces = self.data.get("ui-faces", []) + return { + "emacsVersion": self.data.get("meta", {}).get("emacs-version"), + "default": { + "foreground": self.color("default", "foreground"), + "background": self.color("default", "background"), + }, + "faceCount": len(self.data.get("faces", {})), + "packageFaceCount": len(package_faces), + "packageUnresolvedFaceCount": self.data.get("meta", {}).get("package-unresolved-face-count", 0), + "uiOwnSeeds": {face: self.seed(face) for face in ui_faces if self.seed(face)}, + "packageInherits": package_inherits, + } + def _build_color_hex(self) -> dict[str, str]: out: dict[str, str] = {} if not self.data: @@ -126,3 +150,11 @@ class DefaultFaces: if hex_value and name and not str(name).startswith("#"): out.setdefault(hex_value.lower(), str(name).lower().replace(" ", "-")) return out + + +def changed_summary(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]: + changed = {} + for key in sorted(set(before) | set(after)): + if before.get(key) != after.get(key): + changed[key] = {"before": before.get(key), "after": after.get(key)} + return changed diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 034df72b..16ed07f1 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -14,7 +14,7 @@ from collections import Counter, defaultdict import generate # importable without side effects: the file write is __main__-guarded from app_inventory import face_rows -from default_faces import DefaultFaces +from default_faces import DefaultFaces, changed_summary from face_specs import package_face_spec, ui_face_spec @@ -224,6 +224,45 @@ class DefaultFaceAdapter(unittest.TestCase): self.assertEqual(defaults.seed("missing"), {}) self.assertEqual(defaults.label("#000000", "fallback"), "fallback") + def test_summary_reports_default_drift_fields(self): + defaults = DefaultFaces({ + "meta": {"emacs-version": "30.2", "package-unresolved-face-count": 2}, + "ui-faces": ["sample"], + "package-inventory": {"pkg": ["pkg-face"]}, + "faces": { + "default": { + "effectiveGuiLight": { + "foregroundHex": "#000000", + "backgroundHex": "#ffffff", + }, + "chosenGuiLight": {}, + }, + "sample": { + "chosenGuiLight": {"backgroundHex": "#ffffff"}, + "effectiveGuiLight": {}, + }, + "pkg-face": { + "chosenGuiLight": {"inherit": "base-face"}, + "effectiveGuiLight": {}, + }, + }, + }) + self.assertEqual(defaults.summary(), { + "emacsVersion": "30.2", + "default": {"foreground": "#000000", "background": "#ffffff"}, + "faceCount": 3, + "packageFaceCount": 1, + "packageUnresolvedFaceCount": 2, + "uiOwnSeeds": {"sample": {"bg": "#ffffff"}}, + "packageInherits": {"pkg-face": "base-face"}, + }) + + def test_changed_summary_reports_only_changed_top_level_keys(self): + self.assertEqual(changed_summary({"a": 1, "b": 2}, {"a": 1, "b": 3, "c": 4}), { + "b": {"before": 2, "after": 3}, + "c": {"before": None, "after": 4}, + }) + class PackageFaceCoverage(unittest.TestCase): ALLOWED_DUPLICATES = { -- cgit v1.2.3