From 6f2138c63572099af217077acb43674dbae083d9 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 8 Jun 2026 03:39:48 -0500 Subject: feat(theme-selector): bespoke previews for ghostel, mu4e, and dashboard These three no longer fall to the generic face-name list. ghostel renders a mock terminal covering all 19 faces (the 16 ANSI colors, default, and the fake cursor). mu4e renders a headers list, message view, and compose stub covering all 37 of the faces the dupre theme themes; mu4e is absent from the generated inventory, so its face list is curated. dashboard covers all 8. Every face has a real preview element, so the click-to-flash linking works both directions with no fallback. --- scripts/theme-selector/README.md | 10 ++-- scripts/theme-selector/generate.py | 85 ++++++++++++++++++++++++++++-- scripts/theme-selector/theme-selector.html | 47 ++++++++++++++++- 3 files changed, 134 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/scripts/theme-selector/README.md b/scripts/theme-selector/README.md index ca708c51..afeb751f 100644 --- a/scripts/theme-selector/README.md +++ b/scripts/theme-selector/README.md @@ -64,10 +64,14 @@ foreground and background dropdown, bold/italic toggles, an `inherit` dropdown height stepper, a contrast readout, and a per-face reset. There's a per-app reset and a text filter for the large sets. -org-mode, magit, and elfeed have bespoke previews (a mock org document, a magit -status buffer, an elfeed search list). Every other installed package is reachable +Six applications have bespoke previews that exercise nearly all of their faces: +org-mode (a document plus an agenda view), magit (a status buffer plus blame, +reflog, sequence, bisect, and signature rows), elfeed (a search list and log), +ghostel (a mock terminal with the 16 ANSI colors), mu4e (a headers list, message +view, and compose stub), and dashboard. Every other installed package is reachable too, with an editable table and a generic preview (each face name in its own -colors), so any package can be themed. +colors), so any package can be themed. Clicking a face row flashes that face in +the preview, and clicking a preview element flashes its row. **Inheritance** is modeled, not flattened: a face's effective color is resolved through its `inherit` chain and shown in the table and preview; setting an diff --git a/scripts/theme-selector/generate.py b/scripts/theme-selector/generate.py index 46f3be09..fb236bc7 100644 --- a/scripts/theme-selector/generate.py +++ b/scripts/theme-selector/generate.py @@ -165,6 +165,39 @@ ELFEED_SEED={ "elfeed-log-date-face":{"fg":"steel"},"elfeed-log-error-level-face":{"fg":"terracotta","bold":True}, "elfeed-log-warn-level-face":{"fg":"gold"},"elfeed-log-info-level-face":{"fg":"sage"}, "elfeed-log-debug-level-face":{"fg":"pewter"}} +# ghostel (terminal): the 16 ANSI colors plus default and the fake cursor. +GHOSTEL_FACES=("ghostel-default ghostel-fake-cursor ghostel-fake-cursor-box " + "ghostel-color-black ghostel-color-red ghostel-color-green ghostel-color-yellow " + "ghostel-color-blue ghostel-color-magenta ghostel-color-cyan ghostel-color-white " + "ghostel-color-bright-black ghostel-color-bright-red ghostel-color-bright-green ghostel-color-bright-yellow " + "ghostel-color-bright-blue ghostel-color-bright-magenta ghostel-color-bright-cyan ghostel-color-bright-white").split() +GHOSTEL_SEED={ + "ghostel-default":{"fg":"#cdced1"},"ghostel-fake-cursor":{"fg":"#000000","bg":"silver"},"ghostel-fake-cursor-box":{"fg":"silver"}, + "ghostel-color-black":{"fg":"pewter"},"ghostel-color-red":{"fg":"terracotta"},"ghostel-color-green":{"fg":"emerald"},"ghostel-color-yellow":{"fg":"gold"}, + "ghostel-color-blue":{"fg":"blue"},"ghostel-color-magenta":{"fg":"regal"},"ghostel-color-cyan":{"fg":"sage"},"ghostel-color-white":{"fg":"silver"}, + "ghostel-color-bright-black":{"fg":"steel"},"ghostel-color-bright-red":{"fg":"#de4949"},"ghostel-color-bright-green":{"fg":"#84b068"},"ghostel-color-bright-yellow":{"fg":"#eed376"}, + "ghostel-color-bright-blue":{"fg":"#7a9abe"},"ghostel-color-bright-magenta":{"fg":"#b07fd0"},"ghostel-color-bright-cyan":{"fg":"#7fc0a8"},"ghostel-color-bright-white":{"fg":"white"}} +DASHBOARD_FACES=("dashboard-banner-logo-title dashboard-text-banner dashboard-heading " + "dashboard-items-face dashboard-navigator dashboard-no-items-face dashboard-footer-face dashboard-footer-icon-face").split() +DASHBOARD_SEED={ + "dashboard-banner-logo-title":{"fg":"gold","bold":True},"dashboard-text-banner":{"fg":"steel"},"dashboard-heading":{"fg":"blue","bold":True}, + "dashboard-items-face":{"fg":"#cdced1"},"dashboard-navigator":{"fg":"blue"},"dashboard-no-items-face":{"fg":"pewter"}, + "dashboard-footer-face":{"fg":"tan"},"dashboard-footer-icon-face":{"fg":"gold"}} +# mu4e is not in the generated inventory (not loaded when it was built), so its +# face list is curated from the set the dupre theme already themes. +MU4E_FACES=("mu4e-title-face mu4e-context-face mu4e-modeline-face mu4e-ok-face mu4e-warning-face " + "mu4e-header-title-face mu4e-header-key-face mu4e-header-value-face mu4e-header-face mu4e-header-highlight-face mu4e-header-marks-face " + "mu4e-unread-face mu4e-flagged-face mu4e-replied-face mu4e-forwarded-face mu4e-draft-face mu4e-trashed-face mu4e-moved-face mu4e-related-face " + "mu4e-contact-face mu4e-special-header-value-face mu4e-attach-number-face mu4e-url-number-face mu4e-link-face " + "mu4e-cited-1-face mu4e-cited-2-face mu4e-cited-3-face mu4e-cited-4-face mu4e-cited-5-face mu4e-cited-6-face mu4e-cited-7-face " + "mu4e-footer-face mu4e-region-code mu4e-system-face mu4e-highlight-face mu4e-compose-header-face mu4e-compose-separator-face").split() +MU4E_SEED={ + "mu4e-title-face":{"fg":"blue","bold":True},"mu4e-context-face":{"fg":"blue","bold":True},"mu4e-modeline-face":{"fg":"silver"},"mu4e-ok-face":{"fg":"sage","bold":True},"mu4e-warning-face":{"fg":"gold","bold":True}, + "mu4e-header-title-face":{"fg":"blue","bold":True},"mu4e-header-key-face":{"fg":"blue","bold":True},"mu4e-header-value-face":{"fg":"silver"},"mu4e-header-face":{"fg":"#cdced1"},"mu4e-header-highlight-face":{"bg":"gunmetal"},"mu4e-header-marks-face":{"fg":"gold"}, + "mu4e-unread-face":{"fg":"white","bold":True},"mu4e-flagged-face":{"fg":"gold"},"mu4e-replied-face":{"fg":"silver"},"mu4e-forwarded-face":{"fg":"silver"},"mu4e-draft-face":{"fg":"steel","italic":True},"mu4e-trashed-face":{"fg":"pewter"},"mu4e-moved-face":{"fg":"steel","italic":True},"mu4e-related-face":{"fg":"steel","italic":True}, + "mu4e-contact-face":{"fg":"#cdced1"},"mu4e-special-header-value-face":{"fg":"silver"},"mu4e-attach-number-face":{"fg":"blue","bold":True},"mu4e-url-number-face":{"fg":"blue","bold":True},"mu4e-link-face":{"fg":"blue"}, + "mu4e-cited-1-face":{"fg":"silver"},"mu4e-cited-2-face":{"fg":"steel"},"mu4e-cited-3-face":{"fg":"sage"},"mu4e-cited-4-face":{"fg":"pewter"},"mu4e-cited-5-face":{"fg":"tan"},"mu4e-cited-6-face":{"fg":"terracotta"},"mu4e-cited-7-face":{"fg":"regal"}, + "mu4e-footer-face":{"fg":"pewter"},"mu4e-region-code":{"bg":"bg-dim"},"mu4e-system-face":{"fg":"pewter","italic":True},"mu4e-highlight-face":{"fg":"gold","bold":True},"mu4e-compose-header-face":{"fg":"blue","bold":True},"mu4e-compose-separator-face":{"fg":"pewter"}} def _faces(names,prefix,seed): out=[] for f in names: @@ -173,13 +206,16 @@ def _faces(names,prefix,seed): return out APPS={"org-mode":{"label":"org-mode","preview":"org","faces":_faces(ORG_FACES,"org-",ORG_SEED)}, "magit":{"label":"magit","preview":"magit","faces":_faces(MAGIT_FACES,"magit-",MAGIT_SEED)}, - "elfeed":{"label":"elfeed","preview":"elfeed","faces":_faces(ELFEED_FACES,"elfeed-",ELFEED_SEED)}} + "elfeed":{"label":"elfeed","preview":"elfeed","faces":_faces(ELFEED_FACES,"elfeed-",ELFEED_SEED)}, + "mu4e":{"label":"mu4e","preview":"mu4e","faces":_faces(MU4E_FACES,"mu4e-",MU4E_SEED)}, + "ghostel":{"label":"ghostel","preview":"ghostel","faces":_faces(GHOSTEL_FACES,"ghostel-",GHOSTEL_SEED)}, + "dashboard":{"label":"dashboard","preview":"dashboard","faces":_faces(DASHBOARD_FACES,"dashboard-",DASHBOARD_SEED)}} # 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") if os.path.exists(_inv_path): _INV=json.load(open(_inv_path)) - _BESPOKE={"magit","elfeed","org","org-mode"} + _BESPOKE={"magit","elfeed","org","org-mode","mu4e","ghostel","dashboard"} for _pkg in sorted(_INV): if _pkg in _BESPOKE or _pkg in APPS: continue APPS[_pkg]={"label":_pkg,"preview":"generic","faces":[ @@ -666,8 +702,51 @@ function renderElfeedPreview(){const a='elfeed',L=[]; L.push(os(a,'elfeed-log-date-face','02:24:03')+' '+os(a,'elfeed-log-error-level-face','ERROR')+' failed: bad.example'); L.push(os(a,'elfeed-log-date-face','02:24:04')+' '+os(a,'elfeed-log-debug-level-face','DEBUG')+' parsed 340 entries'); return `
${L.join('\\n')}
`;} +function renderGhostelPreview(){const a='ghostel',L=[]; + L.push(os(a,'ghostel-default','craig@host')+' '+os(a,'ghostel-color-green','~/code')+' $ ls'+os(a,'ghostel-fake-cursor',' ')+os(a,'ghostel-fake-cursor-box','[ ]')); + L.push(''); + L.push(os(a,'ghostel-default','normal:')+' '+os(a,'ghostel-color-black','black')+' '+os(a,'ghostel-color-red','red')+' '+os(a,'ghostel-color-green','green')+' '+os(a,'ghostel-color-yellow','yellow')+' '+os(a,'ghostel-color-blue','blue')+' '+os(a,'ghostel-color-magenta','magenta')+' '+os(a,'ghostel-color-cyan','cyan')+' '+os(a,'ghostel-color-white','white')); + L.push(os(a,'ghostel-default','bright:')+' '+os(a,'ghostel-color-bright-black','black')+' '+os(a,'ghostel-color-bright-red','red')+' '+os(a,'ghostel-color-bright-green','green')+' '+os(a,'ghostel-color-bright-yellow','yellow')+' '+os(a,'ghostel-color-bright-blue','blue')+' '+os(a,'ghostel-color-bright-magenta','magenta')+' '+os(a,'ghostel-color-bright-cyan','cyan')+' '+os(a,'ghostel-color-bright-white','white')); + L.push(''); + L.push(os(a,'ghostel-default','default terminal output, 256-color text and a blinking ')+os(a,'ghostel-fake-cursor','cursor')+'.'); + return `
${L.join('\\n')}
`;} +function renderDashboardPreview(){const a='dashboard',L=[]; + L.push(os(a,'dashboard-text-banner',' ___ _ __ ___ __ _ ___ ___')); + L.push(os(a,'dashboard-banner-logo-title',' Welcome back, Craig')); + L.push(''); + L.push(os(a,'dashboard-heading','Recent Files')); + L.push(' '+os(a,'dashboard-items-face','init.el')); + L.push(' '+os(a,'dashboard-items-face','notes.org')); + L.push(os(a,'dashboard-heading','Bookmarks')); + L.push(' '+os(a,'dashboard-no-items-face','-- no items --')); + L.push(''); + L.push(os(a,'dashboard-navigator','[ Projects ] [ Recent ] [ Agenda ]')); + L.push(os(a,'dashboard-footer-icon-face','*')+' '+os(a,'dashboard-footer-face','Happy hacking, Craig!')); + return `
${L.join('\\n')}
`;} +function renderMu4ePreview(){const a='mu4e',L=[]; + L.push(os(a,'mu4e-title-face','mu4e')+' '+os(a,'mu4e-context-face','[Personal]')+' '+os(a,'mu4e-ok-face','online')+' '+os(a,'mu4e-warning-face','2 retry')+' '+os(a,'mu4e-modeline-face','12/340')); + L.push(''); + L.push(os(a,'mu4e-header-title-face','Date Flags From Subject')); + L.push(os(a,'mu4e-header-value-face','2026-06-08')+' '+os(a,'mu4e-header-marks-face','N')+' '+os(a,'mu4e-unread-face','Alice')+' '+os(a,'mu4e-unread-face','Unread message')); + L.push(os(a,'mu4e-header-value-face','2026-06-07')+' '+os(a,'mu4e-header-marks-face','R')+' '+os(a,'mu4e-header-face','Bob')+' '+os(a,'mu4e-replied-face','Replied thread')); + L.push(os(a,'mu4e-header-value-face','2026-06-06')+' '+os(a,'mu4e-header-marks-face','F')+' '+os(a,'mu4e-header-face','Carol')+' '+os(a,'mu4e-forwarded-face','Forwarded note')); + L.push(os(a,'mu4e-header-value-face','2026-06-05')+' '+os(a,'mu4e-header-marks-face','D')+' '+os(a,'mu4e-draft-face','(draft)')+' '+os(a,'mu4e-draft-face','Draft in progress')); + L.push(os(a,'mu4e-header-value-face','2026-06-04')+' '+os(a,'mu4e-header-marks-face','T')+' '+os(a,'mu4e-trashed-face','Dan')+' '+os(a,'mu4e-moved-face','Trashed and moved')); + L.push(os(a,'mu4e-header-highlight-face','2026-06-03 ! Eve Flagged ')+os(a,'mu4e-flagged-face','important')+os(a,'mu4e-related-face',' (related)')); + L.push(''); + L.push(os(a,'mu4e-header-key-face','From:')+' '+os(a,'mu4e-contact-face','Alice <alice@example.com>')); + L.push(os(a,'mu4e-header-key-face','To:')+' '+os(a,'mu4e-special-header-value-face','craig, list@gnu.org')); + L.push(os(a,'mu4e-header-key-face','Attach:')+' '+os(a,'mu4e-attach-number-face','[1]')+' report.pdf link '+os(a,'mu4e-url-number-face','[2]')+' '+os(a,'mu4e-link-face','https://gnu.org')); + L.push(''); + L.push(' body with a '+os(a,'mu4e-highlight-face','search hit')+' and '+os(a,'mu4e-region-code','code region')+'.'); + L.push(' '+os(a,'mu4e-cited-1-face','> level 1')+' '+os(a,'mu4e-cited-2-face','>> 2')+' '+os(a,'mu4e-cited-3-face','>>> 3')+' '+os(a,'mu4e-cited-4-face','> 4')+' '+os(a,'mu4e-cited-5-face','> 5')+' '+os(a,'mu4e-cited-6-face','> 6')+' '+os(a,'mu4e-cited-7-face','> 7')); + L.push(' '+os(a,'mu4e-system-face','*** system message ***')+' '+os(a,'mu4e-footer-face','-- sent with mu4e')); + L.push(''); + L.push(os(a,'mu4e-compose-header-face','Subject:')+' new mail'); + L.push(os(a,'mu4e-compose-separator-face','--text follows this line--')); + return `
${L.join('\\n')}
`;} function genericPreview(app){let h='
';for(const [face,label,def] of APPS[app].faces){const f=PKGMAP[app][face],efg=pkgEffFg(app,face)||MAP['p'],ebg=pkgEffBg(app,face);h+=`
${esc(label)}
`;}return h+'
';} -function buildPkgPreview(){const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return;const pv=APPS[app].preview;const bespoke=pv==='org'||pv==='magit'||pv==='elfeed';p.innerHTML=pv==='org'?renderOrgPreview():pv==='magit'?renderMagitPreview():pv==='elfeed'?renderElfeedPreview():genericPreview(app);p.style.background=MAP['bg'];p.onclick=(e)=>{const u=e.target.closest('[data-face]');if(u)flashPkg(u.dataset.face);};const lbl=document.getElementById('pkgprevlabel');if(lbl)lbl.textContent=bespoke?(APPS[app].label+' preview'):'preview (generic — face names in their own colors)';} +function buildPkgPreview(){const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return;const pv=APPS[app].preview;const bespoke=['org','magit','elfeed','ghostel','dashboard','mu4e'].includes(pv);p.innerHTML=pv==='org'?renderOrgPreview():pv==='magit'?renderMagitPreview():pv==='elfeed'?renderElfeedPreview():pv==='ghostel'?renderGhostelPreview():pv==='dashboard'?renderDashboardPreview():pv==='mu4e'?renderMu4ePreview():genericPreview(app);p.style.background=MAP['bg'];p.onclick=(e)=>{const u=e.target.closest('[data-face]');if(u)flashPkg(u.dataset.face);};const lbl=document.getElementById('pkgprevlabel');if(lbl)lbl.textContent=bespoke?(APPS[app].label+' preview'):'preview (generic — face names in their own colors)';} function resetApp(){const app=curApp();PKGMAP[app]={};for(const [face,label,d] of APPS[app].faces)PKGMAP[app][face]=seedFace(d);pkgChanged();} function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;pv.style.color=UIMAP[face].fg||MAP['p'];pv.style.background=UIMAP[face].bg||MAP['bg'];} diff --git a/scripts/theme-selector/theme-selector.html b/scripts/theme-selector/theme-selector.html index 02250963..a1b3613b 100644 --- a/scripts/theme-selector/theme-selector.html +++ b/scripts/theme-selector/theme-selector.html @@ -133,7 +133,7 @@