From 8a93f68c10770c592468862dc9ec08386b291447 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 2 Jul 2026 13:20:24 -0400 Subject: feat(theme-studio): pin retired packages so themes keep covering them PINNED_PACKAGE_FACES is the curated record of packages retired from the config (ghostel, all-the-icons). A pinned package survives inventory regeneration, shows a 'not loaded' label + hover in the app dropdown, and keeps refreshing its face list from the live inventory while that still carries it. An attempted live regen made the fragility concrete: today's daemon session was missing 142 faces from lazily-loaded packages, so regen-from-live is inherently lossy and the pin is the only durable record. --- scripts/theme-studio/app_inventory.py | 33 ++++++++++++++++++---- scripts/theme-studio/face_data.py | 28 +++++++++++++++++++ scripts/theme-studio/test_generate.py | 50 ++++++++++++++++++++++++++++++++++ scripts/theme-studio/theme-studio.html | 2 +- 4 files changed, 106 insertions(+), 7 deletions(-) (limited to 'scripts/theme-studio') diff --git a/scripts/theme-studio/app_inventory.py b/scripts/theme-studio/app_inventory.py index 0f1def0d..b5b33a56 100644 --- a/scripts/theme-studio/app_inventory.py +++ b/scripts/theme-studio/app_inventory.py @@ -7,7 +7,7 @@ import os from collections.abc import Sequence from typing import Any -from face_data import BESPOKE_APP_SPECS +from face_data import BESPOKE_APP_SPECS, PINNED_PACKAGE_FACES # Keys of the bespoke apps (single-sourced in face_data), excluded from the @@ -41,11 +41,19 @@ def face_rows(names: Sequence[str], prefix: str, seed: dict[str, dict[str, Any]] def add_inventory_apps(apps: dict[str, Any], inventory_path: str) -> dict[str, Any]: - """Add generic editable apps for installed packages not covered by bespoke previews.""" - if not os.path.exists(inventory_path): - return apps - with open(inventory_path) as src: - inventory = json.load(src) + """Add generic editable apps for installed packages not covered by bespoke previews. + + PINNED_PACKAGE_FACES (the ecosystem coverage policy) is the curated record + of packages retired from this config: a pinned package is always marked + not loaded, and it survives inventory regeneration -- an uninstall must + never drop an app from the studio. Its face list stays fresh from the live + inventory when present (a still-installed dependency can grow faces); the + pin is the fallback when the inventory no longer carries it. + """ + inventory: dict[str, Any] = {} + if os.path.exists(inventory_path): + with open(inventory_path) as src: + inventory = json.load(src) for pkg in sorted(inventory): if pkg in BESPOKE_APPS or pkg in apps: continue @@ -54,6 +62,19 @@ def add_inventory_apps(apps: dict[str, Any], inventory_path: str) -> dict[str, A "preview": PREVIEW_KEYS.get(pkg, "generic"), "faces": [[face, face_label(face, pkg + "-"), {}] for face in inventory[pkg]], } + for pkg in sorted(PINNED_PACKAGE_FACES): + if pkg in BESPOKE_APPS: + continue + faces = inventory.get(pkg, PINNED_PACKAGE_FACES[pkg]) + apps[pkg] = { + "label": PACKAGE_LABEL_OVERRIDES.get(pkg, pkg) + " ยท not loaded", + "preview": PREVIEW_KEYS.get(pkg, "generic"), + "unloaded": True, + "hover": ("Retired from this config; its faces are pinned so ecosystem " + "themes still cover it. The live preview is the only place its " + "theming can be seen."), + "faces": [[face, face_label(face, pkg + "-"), {}] for face in faces], + } return apps diff --git a/scripts/theme-studio/face_data.py b/scripts/theme-studio/face_data.py index e2fa7c1d..e86d7e75 100644 --- a/scripts/theme-studio/face_data.py +++ b/scripts/theme-studio/face_data.py @@ -415,6 +415,34 @@ BESPOKE_APP_SPECS=[ ("shr","simple html renderer (shr)","shr",SHR_FACES,"shr-",SHR_SEED), ] +# Ecosystem coverage pins (see README.md "Coverage policy"). The studio themes +# popular packages even when this config no longer installs them, so a theme +# built here still covers the wider ecosystem. package-inventory.json is +# regenerated from the running Emacs and silently drops anything uninstalled; +# every package listed here survives that. When a pinned package IS in the live +# inventory, the live face list wins (it may have grown); when absent, the app +# is built from this pin and marked "not loaded" in the UI. When retiring a +# package from the config, add its inventory face list here first. +PINNED_PACKAGE_FACES={ + "ghostel":[ + "ghostel-color-black","ghostel-color-blue","ghostel-color-bright-black", + "ghostel-color-bright-blue","ghostel-color-bright-cyan","ghostel-color-bright-green", + "ghostel-color-bright-magenta","ghostel-color-bright-red","ghostel-color-bright-white", + "ghostel-color-bright-yellow","ghostel-color-cyan","ghostel-color-green", + "ghostel-color-magenta","ghostel-color-red","ghostel-color-white", + "ghostel-color-yellow","ghostel-default","ghostel-fake-cursor","ghostel-fake-cursor-box"], + "all-the-icons":[ + "all-the-icons-blue","all-the-icons-blue-alt","all-the-icons-cyan","all-the-icons-cyan-alt", + "all-the-icons-dblue","all-the-icons-dcyan","all-the-icons-dgreen","all-the-icons-dmaroon", + "all-the-icons-dorange","all-the-icons-dpink","all-the-icons-dpurple","all-the-icons-dred", + "all-the-icons-dsilver","all-the-icons-dyellow","all-the-icons-green","all-the-icons-lblue", + "all-the-icons-lcyan","all-the-icons-lgreen","all-the-icons-lmaroon","all-the-icons-lorange", + "all-the-icons-lpink","all-the-icons-lpurple","all-the-icons-lred","all-the-icons-lsilver", + "all-the-icons-lyellow","all-the-icons-maroon","all-the-icons-orange","all-the-icons-pink", + "all-the-icons-purple","all-the-icons-purple-alt","all-the-icons-red","all-the-icons-red-alt", + "all-the-icons-silver","all-the-icons-yellow"], +} + # Hover text for foundational/reused apps: names what consumes these faces, so # the app label can stay clean and the "who reuses this" context rides the app # dropdown's tooltip instead. Apps not listed here get no hover. diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index d4744cde..e53bdd62 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -798,5 +798,55 @@ class NerdIconsGallery(unittest.TestCase): self.assertIn("nerd-icons-blue", faces) +class PinnedPackages(unittest.TestCase): + """The ecosystem coverage policy's mechanism: PINNED_PACKAGE_FACES is the + curated record of packages retired from this config. A pinned package is + always marked not loaded (pinning happens exactly at retirement), and it + survives inventory regeneration even when uninstalled. Its face list stays + fresh from the live inventory when present (a still-installed dependency + may grow faces); the pin is the fallback when the inventory drops it.""" + + def _apps_with_inventory(self, inventory): + import app_inventory + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f: + json.dump(inventory, f) + path = f.name + try: + return app_inventory.add_inventory_apps({}, path) + finally: + os.unlink(path) + + def test_pinned_package_absent_from_inventory_is_added_unloaded(self): + apps = self._apps_with_inventory({"someother": ["someother-face"]}) + self.assertIn("ghostel", apps) + self.assertTrue(apps["ghostel"]["unloaded"]) + self.assertIn("not loaded", apps["ghostel"]["label"]) + faces = {row[0] for row in apps["ghostel"]["faces"]} + self.assertIn("ghostel-default", faces) + + def test_pinned_package_in_inventory_keeps_live_faces_still_flagged (self): + apps = self._apps_with_inventory({"ghostel": ["ghostel-default", "ghostel-brand-new"]}) + self.assertTrue(apps["ghostel"]["unloaded"]) + self.assertIn("not loaded", apps["ghostel"]["label"]) + faces = {row[0] for row in apps["ghostel"]["faces"]} + self.assertIn("ghostel-brand-new", faces) + + def test_unpinned_inventory_package_is_not_flagged(self): + apps = self._apps_with_inventory({"someother": ["someother-face"]}) + self.assertFalse(apps["someother"].get("unloaded")) + self.assertNotIn("not loaded", apps["someother"]["label"]) + + def test_pinned_apps_carry_an_explanatory_hover(self): + apps = self._apps_with_inventory({}) + self.assertIn("pinned", apps["ghostel"].get("hover", "")) + + def test_all_the_icons_is_pinned(self): + apps = self._apps_with_inventory({}) + self.assertIn("all-the-icons", apps) + self.assertTrue(apps["all-the-icons"]["unloaded"]) + faces = {row[0] for row in apps["all-the-icons"]["faces"]} + self.assertIn("all-the-icons-blue", faces) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 8e219fcd..e70a91e0 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -297,7 +297,7 @@