From 5e72000497732e7d20c179d2562e4bfacc1e3fbe Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 24 Jun 2026 05:46:43 -0400 Subject: feat(theme-studio): bespoke nerd-icons filetype-legend preview (phase 2) Register nerd-icons as a bespoke app whenever its captured legend is valid: the 34 color faces stay editable rows, and the legend rides APPS['nerd-icons'].legend. A new renderNerdIconsPreview draws each curated filetype's glyph in its mapped face's effective color, read through the same registry the other previews use, so recoloring a face repaints every row mapped to it. When the legend is absent the generic inventory app stands in. The #nerdiconstest browser gate covers the wiring, the dir-row owner, and the recolor-repaint. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ --- scripts/theme-studio/app.js | 3 ++- scripts/theme-studio/app_inventory.py | 25 +++++++++++++++++++ scripts/theme-studio/browser-gates.js | 27 ++++++++++++++++++++ scripts/theme-studio/generate.py | 6 ++++- scripts/theme-studio/previews.js | 11 +++++++++ scripts/theme-studio/test_generate.py | 15 ++++++++++++ scripts/theme-studio/theme-studio.html | 45 +++++++++++++++++++++++++++++++--- 7 files changed, 127 insertions(+), 5 deletions(-) (limited to 'scripts/theme-studio') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 28b8e3cdb..0faf9923f 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -765,7 +765,8 @@ const PACKAGE_PREVIEWS={ dashboard:renderDashboardPreview,mu4e:renderMu4ePreview,gnus:renderGnusPreview,orgfaces:renderOrgFacesPreview,lsp:renderLspPreview,gitgutter:renderGitGutterPreview, flycheck:renderFlycheckPreview,dired:renderDiredPreview,dirvish:renderDirvishPreview,calibredb:renderCalibredbPreview, erc:renderErcPreview,orgdrill:renderOrgdrillPreview,orgnoter:renderOrgnoterPreview,signel:renderSignelPreview, - pearl:renderPearlPreview,slack:renderSlackPreview,telega:renderTelegaPreview,shr:renderShrPreview + pearl:renderPearlPreview,slack:renderSlackPreview,telega:renderTelegaPreview,shr:renderShrPreview, + nerdicons:renderNerdIconsPreview }; function buildPkgPreview(){ const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return; diff --git a/scripts/theme-studio/app_inventory.py b/scripts/theme-studio/app_inventory.py index 11ca605d1..09e2ed0a0 100644 --- a/scripts/theme-studio/app_inventory.py +++ b/scripts/theme-studio/app_inventory.py @@ -50,6 +50,31 @@ def add_inventory_apps(apps: dict[str, Any], inventory_path: str) -> dict[str, A return apps +def add_nerd_icons_app(apps: dict[str, Any], inventory_path: str, legend: Any) -> dict[str, Any]: + """Register nerd-icons as a bespoke legend app from its inventory faces. + + The 34 nerd-icons color faces stay editable rows; LEGEND (the validated rows + from generate.load_nerd_icons_legend) rides the app so the bespoke previews.js + renderer can draw each filetype glyph in its mapped face color. A no-op when + LEGEND is falsy or the inventory lacks nerd-icons -- the caller guards on a + valid legend, and add_inventory_apps then creates the generic fallback app. + Must run before add_inventory_apps so the generic path skips nerd-icons. + """ + if not legend or not os.path.exists(inventory_path): + return apps + with open(inventory_path) as src: + faces = json.load(src).get("nerd-icons") + if not faces: + return apps + apps["nerd-icons"] = { + "label": "nerd-icons", + "preview": "nerdicons", + "faces": [[face, face_label(face, "nerd-icons-"), {}] for face in faces], + "legend": legend, + } + return apps + + def apply_default_face_seeds(apps: dict[str, Any], defaults: Any) -> None: if not defaults.available: return diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 3b909c424..e428ae58c 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -810,6 +810,33 @@ if(location.hash==='#gnustest')gate('gnustest',A=>{ assertPreviewFaces(A, renderGnusPreview(), APPS['gnus']&&APPS['gnus'].faces, 20, 'gnus', ['gnus-header-name','gnus-header-from','gnus-header-subject','gnus-cite-1','gnus-cite-attribution','gnus-signature','gnus-button','gnus-emphasis-highlight-words']); }); +// nerd-icons legend gate (open with #nerdiconstest): nerd-icons is a bespoke +// filetype-legend app; every glyph span is a real nerd-icons face, the dir row +// models nerd-icons-yellow, and recoloring a face repaints every row mapped to it. +if(location.hash==='#nerdiconstest')gate('nerdiconstest',A=>{ + A(!!APPS['nerd-icons'],'nerd-icons is a registered app'); + A(APPS['nerd-icons']&&APPS['nerd-icons'].preview==='nerdicons','nerd-icons uses the nerdicons preview renderer'); + A(!!PACKAGE_PREVIEWS['nerdicons'],'nerdicons renderer registered'); + const legend=(APPS['nerd-icons']&&APPS['nerd-icons'].legend)||[]; + A(Array.isArray(legend)&&legend.length>=10,'legend has the curated rows ('+legend.length+')'); + const dir=legend.find(r=>r.key==='dir'); + A(dir&&dir.face==='nerd-icons-yellow','dir row models nerd-icons-yellow'); + if(PACKAGE_PREVIEWS['nerdicons']&&APPS['nerd-icons']){ + assertPreviewFaces(A, renderNerdIconsPreview(), APPS['nerd-icons'].faces, 10, 'nerd-icons', + ['nerd-icons-purple','nerd-icons-yellow','nerd-icons-blue','nerd-icons-dblue']); + // Recoloring a face repaints every legend row mapped to it (os reads the live registry). + withSavedState(['PKGMAP'],()=>{ + const target='nerd-icons-purple',mapped=legend.filter(r=>r.face===target); + A(mapped.length>=1,'at least one row maps to '+target); + PKGMAP['nerd-icons']=PKGMAP['nerd-icons']||{}; + PKGMAP['nerd-icons'][target]={fg:'#abcdef',bg:null,weight:null,slant:null,inherit:null,height:1,source:'user'}; + const box=document.createElement('div');box.innerHTML=renderNerdIconsPreview(); + const els=[...box.querySelectorAll('[data-face="'+target+'"]')]; + A(els.length===mapped.length,'every '+target+' row rendered ('+els.length+'/'+mapped.length+')'); + A(els.length>0&&els.every(e=>/#abcdef/i.test(e.getAttribute('style')||'')),'recolor repaints the mapped rows'); + }); + } + }); // picker-distinct gate (open with #pickertest): the color picker panel must stand // out from the page background. It carries a highlighted gold accent border, and its // background is meaningfully lighter than the body so the two are easy to tell apart. diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index 4f390a850..edfb7e52b 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -1,5 +1,5 @@ import json, os, re -from app_inventory import add_inventory_apps, apply_default_face_seeds, apply_package_overrides, face_rows +from app_inventory import add_inventory_apps, add_nerd_icons_app, apply_default_face_seeds, apply_package_overrides, face_rows from default_faces import DefaultFaces from face_data import * from face_specs import face_spec, ui_face_spec, migrate_legacy @@ -297,6 +297,10 @@ def _build(): # Phase 6: merge the generated all-package inventory (refresh with build-inventory.el). # Bespoke apps stay; every other installed package becomes an editable generic app. _inv_path=os.path.join(HERE,"package-inventory.json") + # nerd-icons becomes a bespoke filetype-legend app when its captured legend is + # valid; otherwise add_inventory_apps below makes it a plain generic app (the + # fallback). Must precede add_inventory_apps so the generic path skips it. + add_nerd_icons_app(APPS, _inv_path, load_nerd_icons_legend()) add_inventory_apps(APPS, _inv_path) apply_default_face_seeds(APPS, DEFAULTS) # Apply seed theme package overrides when THEME_STUDIO_SEED is set: each full diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js index cb9d5babe..fef616c40 100644 --- a/scripts/theme-studio/previews.js +++ b/scripts/theme-studio/previews.js @@ -481,3 +481,14 @@ function renderMarkdownPreview(){const a='markdown-mode',L=[]; L.push(os(a,'markdown-html-tag-delimiter-face','<')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','>')+'Ctrl-C'+os(a,'markdown-html-tag-delimiter-face','</')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','>')); L.push(os(a,'markdown-footnote-marker-face','[^1]:')+' '+os(a,'markdown-footnote-text-face','the footnote text.')); return previewLines(L);} +// nerd-icons legend preview: each curated filetype's real nerd-font glyph drawn +// in its mapped color face, then the sample name. The legend rides +// APPS['nerd-icons'].legend (captured by build-nerd-icons-legend.el); recoloring +// a face repaints every row mapped to it because os() reads the live registry. +// Falls back to the generic preview if the legend is missing (the bespoke app +// only registers with a valid one, so that path is defensive). +function renderNerdIconsPreview(){ + const a='nerd-icons',rows=(APPS[a]&&APPS[a].legend)||[],L=[]; + if(!rows.length)return genericPreview(a); + for(const r of rows) L.push(os(a,r.face,r.glyph)+' '+r.label); + return previewLines(L);} diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 7ff207e43..58541cbae 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -641,6 +641,21 @@ class NerdIconsLegend(unittest.TestCase): self.assertIsNone(generate.load_nerd_icons_legend(path)) self.assertIn("invalid", out.getvalue()) + def test_nerd_icons_registered_as_bespoke_legend_app(self): + app = generate.APPS.get("nerd-icons") + self.assertIsNotNone(app, "nerd-icons should be a bespoke app with the legend present") + self.assertEqual(app["preview"], "nerdicons") + self.assertTrue(app.get("legend")) + self.assertGreaterEqual(len(app["faces"]), 30) + # The dir-completion face is a different package and keeps its own app. + self.assertIn("nerd-icons-completion", generate.APPS) + + def test_nerd_icons_app_faces_are_seeded_with_native_colors(self): + # apply_default_face_seeds fills the editable rows from emacs-default-faces.json. + rows = {r[0]: r[2] for r in generate.APPS["nerd-icons"]["faces"]} + self.assertIn("nerd-icons-blue", rows) + self.assertTrue(rows["nerd-icons-blue"], "nerd-icons-blue should carry a native seed") + if __name__ == "__main__": unittest.main() diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 4a3ec4fe1..8724ef559 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -290,10 +290,10 @@