diff options
Diffstat (limited to 'scripts/theme-studio/default_faces.py')
| -rw-r--r-- | scripts/theme-studio/default_faces.py | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/scripts/theme-studio/default_faces.py b/scripts/theme-studio/default_faces.py new file mode 100644 index 00000000..ce2bf319 --- /dev/null +++ b/scripts/theme-studio/default_faces.py @@ -0,0 +1,160 @@ +"""Helpers for theme-studio's captured Emacs default face snapshot.""" + +from __future__ import annotations + +import json +import pathlib +from typing import Any + + +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(self, face: str, effective: bool = False) -> dict[str, Any]: + data = self.face(face, effective) + out: dict[str, Any] = {} + fg = data.get("foregroundHex") or data.get("foreground") + bg = data.get("backgroundHex") or data.get("background") + if fg: + out["fg"] = fg + if bg: + out["bg"] = bg + if data.get("weight") == "bold": + out["bold"] = True + if data.get("slant") == "italic": + out["italic"] = True + if data.get("underline"): + out["underline"] = True + if data.get("strike"): + out["strike"] = True + if data.get("inherit"): + out["inherit"] = data.get("inherit") + if data.get("height") and data.get("height") != 1: + out["height"] = data.get("height") + box = self.box_to_theme(data.get("box")) + if box: + out["box"] = box + 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 _build_color_hex(self) -> dict[str, str]: + out: dict[str, str] = {} + if not self.data: + return out + 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"): + name = face_data.get(attr) + hex_value = face_data.get(attr + "Hex") + 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] = {} + if not self.data: + return out + 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"): + hex_value = face_data.get(attr + "Hex") + name = face_data.get(attr) + 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 |
