diff options
Diffstat (limited to 'scripts/theme-studio/default_faces.py')
| -rw-r--r-- | scripts/theme-studio/default_faces.py | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/scripts/theme-studio/default_faces.py b/scripts/theme-studio/default_faces.py new file mode 100644 index 000000000..b9633cfa7 --- /dev/null +++ b/scripts/theme-studio/default_faces.py @@ -0,0 +1,176 @@ +"""Helpers for theme-studio's captured Emacs default face snapshot.""" + +from __future__ import annotations + +import json +import pathlib +from typing import Any + +from face_specs import FACE_ATTRS + + +class DefaultFaces: + def __init__(self, data: dict[str, Any] | None): + self.data = data + self.color_hex = self._build_color_hex() + self.color_names = self._build_color_names() + + @classmethod + def from_path(cls, path: str | pathlib.Path) -> "DefaultFaces": + path = pathlib.Path(path) + if not path.exists(): + return cls(None) + return cls(json.loads(path.read_text())) + + @property + def available(self) -> bool: + return bool(self.data) + + def face(self, face: str, effective: bool = True) -> dict[str, Any]: + if not self.data: + return {} + data = self.data.get("faces", {}).get(face, {}) + block = "effectiveGuiLight" if effective else "chosenGuiLight" + return data.get(block, {}) or {} + + def color(self, face: str, attr: str = "foreground", effective: bool = True) -> Any: + data = self.face(face, effective) + return data.get(attr + "Hex") or data.get(attr) + + def _seed_value(self, attr: dict[str, Any], data: dict[str, Any]) -> Any: + """Turn a snapshot field into a model value per the attribute's kind. + + The snapshot speaks a different dialect than the model: colors carry a + Hex variant with a name fallback; weight/slant are value-narrowed to the + legacy bold/italic until the snapshot refresh; underline/strike/overline + are truthy flags that become objects; inverse/extend coerce Emacs's "t". + Returns None (skip) when the attribute is unset or not seedable. + """ + kind, snap = attr["kind"], attr["snapshot"] + if not kind: + return None + if kind == "color": + return data.get(snap + "Hex") or data.get(snap) + if kind == "weight-bold": + return "bold" if data.get(snap) == "bold" else None + if kind == "slant-italic": + return "italic" if data.get(snap) == "italic" else None + if kind == "underline-obj": + return {"style": "line", "color": None} if data.get(snap) else None + if kind == "color-obj": + return {"color": None} if data.get(snap) else None + if kind == "bool": + return True if data.get(snap) in (True, "t") else None + if kind == "scalar": + return data.get(snap) or None + if kind == "height": + h = data.get(snap) + return h if (h and h != 1) else None + if kind == "box": + return self.box_to_theme(data.get(snap)) + return None + + def seed(self, face: str, effective: bool = False) -> dict[str, Any]: + data = self.face(face, effective) + out: dict[str, Any] = {} + for attr in FACE_ATTRS: + v = self._seed_value(attr, data) + if v: + out[attr["model"]] = v + return out + + def box_to_theme(self, box: Any) -> dict[str, Any] | None: + if not box: + return None + if isinstance(box, dict): + return box + if not isinstance(box, list): + return None + + vals = {} + i = 0 + while i + 1 < len(box): + vals[box[i]] = box[i + 1] + i += 2 + + width = vals.get(":line-width", 1) + if isinstance(width, list) and width and width[0] == "cons": + width = width[1] + if isinstance(width, (int, float)): + width = abs(int(width)) or 1 + else: + width = 1 + + color = vals.get(":color") + if color: + color = self.color_hex.get(str(color).lower().replace(" ", ""), color) + + style = vals.get(":style") + if style == "released-button": + return {"style": "released", "width": width, "color": None} + if style == "pressed-button": + return {"style": "pressed", "width": width, "color": None} + return {"style": "line", "width": width, "color": color} + + def label(self, value: str | None, fallback: str) -> str: + if not value: + 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 _iter_color_pairs(self): + """Yield (name, hexValue) over every face's chosen/effective color attrs. + Both color maps walk this same nested structure; they differ only in which + of the pair is the key and how each filters.""" + if not self.data: + return + for data in self.data.get("faces", {}).values(): + for block in ("chosenGuiLight", "effectiveGuiLight"): + face_data = data.get(block, {}) or {} + for attr in ("foreground", "background", "distantForeground"): + yield face_data.get(attr), face_data.get(attr + "Hex") + + def _build_color_hex(self) -> dict[str, str]: + out: dict[str, str] = {} + for name, hex_value in self._iter_color_pairs(): + if name and hex_value: + out[str(name).lower().replace(" ", "")] = hex_value + return out + + def _build_color_names(self) -> dict[str, str]: + out: dict[str, str] = {} + for name, hex_value in self._iter_color_pairs(): + 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 |
