aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scripts/theme-studio/capture-default-faces.py22
-rw-r--r--scripts/theme-studio/default_faces.py79
-rw-r--r--scripts/theme-studio/face_specs.py54
3 files changed, 79 insertions, 76 deletions
diff --git a/scripts/theme-studio/capture-default-faces.py b/scripts/theme-studio/capture-default-faces.py
index 3de09941b..8c8fd6679 100644
--- a/scripts/theme-studio/capture-default-faces.py
+++ b/scripts/theme-studio/capture-default-faces.py
@@ -17,6 +17,8 @@ import re
import subprocess
import tempfile
+from face_specs import FACE_ATTRS
+
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[1]
OUT = HERE / "emacs-default-faces.json"
@@ -85,21 +87,11 @@ BUILTIN_FEATURES = [
"shr",
]
-ATTRS = {
- ":foreground": "foreground",
- ":background": "background",
- ":weight": "weight",
- ":slant": "slant",
- ":underline": "underline",
- ":strike-through": "strike",
- ":overline": "overline",
- ":box": "box",
- ":height": "height",
- ":inherit": "inherit",
- ":inverse-video": "inverseVideo",
- ":extend": "extend",
- ":distant-foreground": "distantForeground",
-}
+# Emacs face :attribute keyword -> snapshot field name, derived from the shared
+# face-attribute spec so the capture, the seed extraction, and STYLE_DEFAULTS all
+# stay in step. Attributes the snapshot doesn't carry (e.g. family) have no
+# capture keyword and are skipped.
+ATTRS = {a["capture"]: a["snapshot"] for a in FACE_ATTRS if a["capture"]}
def x11_colors() -> dict[str, str]:
diff --git a/scripts/theme-studio/default_faces.py b/scripts/theme-studio/default_faces.py
index 9a0dba5e3..70ceb39e6 100644
--- a/scripts/theme-studio/default_faces.py
+++ b/scripts/theme-studio/default_faces.py
@@ -6,6 +6,8 @@ import json
import pathlib
from typing import Any
+from face_specs import FACE_ATTRS
+
class DefaultFaces:
def __init__(self, data: dict[str, Any] | None):
@@ -35,49 +37,46 @@ class DefaultFaces:
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] = {}
- 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
- # Representation-only cutover: the snapshot's bold/italic become the new
- # weight/slant shape, and underline/strike become objects. The same
- # narrowing as before (only "bold"/"italic" survive; richer weights and
- # underline colors wait for the snapshot refresh), so the emitted theme
- # is byte-identical.
- if data.get("weight") == "bold":
- out["weight"] = "bold"
- if data.get("slant") == "italic":
- out["slant"] = "italic"
- if data.get("underline"):
- out["underline"] = {"style": "line", "color": None}
- if data.get("strike"):
- out["strike"] = {"color": None}
- # Additive attrs the snapshot already carries for the faces that set them:
- # distant-foreground (e.g. lazy-highlight), inverse-video, and extend.
- # overline is captured too once the snapshot is refreshed; stock faces
- # almost never set it, so it is usually absent. These seed faces with the
- # attrs Emacs gives them by default, so the studio opens closer to reality.
- df = data.get("distantForegroundHex") or data.get("distantForeground")
- if df:
- out["distant-fg"] = df
- if data.get("overline"):
- out["overline"] = {"color": None}
- if data.get("inverseVideo") in (True, "t"):
- out["inverse"] = True
- if data.get("extend") in (True, "t"):
- out["extend"] = 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
+ 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:
diff --git a/scripts/theme-studio/face_specs.py b/scripts/theme-studio/face_specs.py
index 5fa038068..1b3e150d6 100644
--- a/scripts/theme-studio/face_specs.py
+++ b/scripts/theme-studio/face_specs.py
@@ -5,27 +5,39 @@ from __future__ import annotations
from typing import Any
-# The full per-face attribute model, in its final shape. weight and slant replace
-# the legacy bold/italic booleans (weight is one of light/normal/medium/semibold/
-# bold/heavy; slant is normal/italic/oblique). underline and strike are objects:
-# underline is {style: line|wave, color} and strike is {color}; null means unset.
-# inherit and height are no longer package-only — every tier can set them.
-STYLE_DEFAULTS: dict[str, Any] = {
- "fg": None,
- "bg": None,
- "distant-fg": None,
- "family": None,
- "weight": None,
- "slant": None,
- "underline": None,
- "strike": None,
- "overline": None,
- "box": None,
- "inverse": False,
- "extend": False,
- "inherit": None,
- "height": None,
-}
+# The full per-face attribute model, in its final shape, as one spec list that is
+# the single source for: STYLE_DEFAULTS (model key -> default), the capture probe
+# map in capture-default-faces.py (emacs :attr -> snapshot field), and the
+# snapshot extraction in default_faces.seed (snapshot field + kind -> model value).
+# Per row:
+# model : theme-model key
+# default : value when unset
+# capture : the Emacs face :attribute keyword the capture probes (None = not
+# captured, e.g. family, which the model carries but the snapshot omits)
+# snapshot : the field name the capture writes (and seed reads)
+# kind : how seed turns the snapshot field into a model value (None = not seeded)
+# weight/slant replaced the legacy bold/italic booleans; underline/strike/overline
+# are objects ({style: line|wave, color} / {color}); inherit and height apply to
+# every tier. Keep this in step with app-core.js FACE_ATTRS (separate runtime).
+FACE_ATTRS: list[dict[str, Any]] = [
+ {"model": "fg", "default": None, "capture": ":foreground", "snapshot": "foreground", "kind": "color"},
+ {"model": "bg", "default": None, "capture": ":background", "snapshot": "background", "kind": "color"},
+ {"model": "distant-fg", "default": None, "capture": ":distant-foreground", "snapshot": "distantForeground", "kind": "color"},
+ {"model": "family", "default": None, "capture": None, "snapshot": None, "kind": None},
+ {"model": "weight", "default": None, "capture": ":weight", "snapshot": "weight", "kind": "weight-bold"},
+ {"model": "slant", "default": None, "capture": ":slant", "snapshot": "slant", "kind": "slant-italic"},
+ {"model": "underline", "default": None, "capture": ":underline", "snapshot": "underline", "kind": "underline-obj"},
+ {"model": "strike", "default": None, "capture": ":strike-through", "snapshot": "strike", "kind": "color-obj"},
+ {"model": "overline", "default": None, "capture": ":overline", "snapshot": "overline", "kind": "color-obj"},
+ {"model": "box", "default": None, "capture": ":box", "snapshot": "box", "kind": "box"},
+ {"model": "inverse", "default": False, "capture": ":inverse-video", "snapshot": "inverseVideo", "kind": "bool"},
+ {"model": "extend", "default": False, "capture": ":extend", "snapshot": "extend", "kind": "bool"},
+ {"model": "inherit", "default": None, "capture": ":inherit", "snapshot": "inherit", "kind": "scalar"},
+ {"model": "height", "default": None, "capture": ":height", "snapshot": "height", "kind": "height"},
+]
+
+# model key -> default, derived from the spec above (order preserved).
+STYLE_DEFAULTS: dict[str, Any] = {a["model"]: a["default"] for a in FACE_ATTRS}
# Kept as a distinct name for callers, but inherit/height are no longer
# package-only, so the package defaults are now the same full set.