From c96c1e9f94f52876b3a8c6ab8e35a00e5f556f3d Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 16 Jun 2026 08:27:53 -0500 Subject: feat(theme-studio): add gnus as a view package mu4e renders the open message with gnus, so the article-view headers, quote levels, signature, and inline emphasis are all gnus faces, not mu4e ones. gnus ships them as bright greens on a dark background, and the theme had no way to reach them. I added gnus as a bespoke view package: the article-view face set in face_data.py with palette seeds that mirror the mu4e header treatment, a realistic preview (header block, emphasized body, an 11-level quoted reply chain, signature), and a #gnustest gate that asserts every emitted face is a real gnus face. Theming and exporting gnus in the studio retires the green in the live view. --- scripts/theme-studio/app.js | 31 +++++++++++++++++++++- scripts/theme-studio/app_inventory.py | 1 + scripts/theme-studio/browser-gates.js | 15 +++++++++++ scripts/theme-studio/face_data.py | 12 +++++++++ scripts/theme-studio/generate.py | 1 + scripts/theme-studio/theme-studio.html | 48 ++++++++++++++++++++++++++++++++-- 6 files changed, 105 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index b8369f9b8..126926fec 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -753,6 +753,35 @@ function renderMu4ePreview(){const a='mu4e',L=[]; L.push(''); L.push(os(a,'mu4e-compose-separator-face','--text follows this line--')); return previewLines(L);} +function renderGnusPreview(){const a='gnus',L=[]; + // mu4e renders the open message with gnus, so this is the article view: + // a header block, a body with inline emphasis and a button, then a quoted + // reply chain (one cite face per nesting level) and the signature. + L.push(os(a,'gnus-header-name','From: ')+os(a,'gnus-header-from','Christine Park <christine@example.com>')); + L.push(os(a,'gnus-header-name','To: ')+os(a,'gnus-header-content','craig@cjennings.net')); + L.push(os(a,'gnus-header-name','Newsgroups: ')+os(a,'gnus-header-newsgroups','gnu.emacs.help')); + L.push(os(a,'gnus-header-name','Subject: ')+os(a,'gnus-header-subject','Re: quarterly numbers')); + L.push(os(a,'gnus-header-name','Date: ')+os(a,'gnus-header-content','Sat, 14 Jun 2026 09:12:04 -0500')); + L.push(''); + L.push('Thanks for the draft. The '+os(a,'gnus-emphasis-bold','revenue line')+' is '+os(a,'gnus-emphasis-italic','close')+', but the '+os(a,'gnus-emphasis-underline','footnote')+' is '+os(a,'gnus-emphasis-strikethru','wrong')+' '+os(a,'gnus-emphasis-highlight-words','FIXME')+'.'); + L.push('See the worksheet: '+os(a,'gnus-button','[https://example.com/q2]')); + L.push(''); + L.push(os(a,'gnus-cite-attribution','On Fri, Bob Lin wrote:')); + L.push(os(a,'gnus-cite-1','> The Q2 totals are ready for review.')); + L.push(os(a,'gnus-cite-2','>> Did the Segpay refund post yet?')); + L.push(os(a,'gnus-cite-3','>>> Yes, it cleared on the 5th.')); + L.push(os(a,'gnus-cite-4','>>>> Good, then we are square.')); + L.push(os(a,'gnus-cite-5','>>>>> earlier reply, level 5')); + L.push(os(a,'gnus-cite-6','>>>>>> level 6')); + L.push(os(a,'gnus-cite-7','>>>>>>> level 7')); + L.push(os(a,'gnus-cite-8','>>>>>>>> level 8')); + L.push(os(a,'gnus-cite-9','>>>>>>>>> level 9')); + L.push(os(a,'gnus-cite-10','>>>>>>>>>> level 10')); + L.push(os(a,'gnus-cite-11','>>>>>>>>>>> level 11')); + L.push(''); + L.push(os(a,'gnus-signature','-- ')); + L.push(os(a,'gnus-signature','Christine Park, Finance')); + return previewLines(L);} function renderOrgFacesPreview(){const a='org-faces',L=[]; L.push('Agenda header row -- one face per keyword and priority (this config, not built-in org):'); L.push(''); @@ -1007,7 +1036,7 @@ function renderMarkdownPreview(){const a='markdown-mode',L=[]; const PACKAGE_PREVIEWS={ autodim:renderAutodimPreview,markdown:renderMarkdownPreview, org:renderOrgPreview,magit:renderMagitPreview,elfeed:renderElfeedPreview,ghostel:renderGhostelPreview, - dashboard:renderDashboardPreview,mu4e:renderMu4ePreview,orgfaces:renderOrgFacesPreview,lsp:renderLspPreview,gitgutter:renderGitGutterPreview, + 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 diff --git a/scripts/theme-studio/app_inventory.py b/scripts/theme-studio/app_inventory.py index 9add0f658..ed904b119 100644 --- a/scripts/theme-studio/app_inventory.py +++ b/scripts/theme-studio/app_inventory.py @@ -14,6 +14,7 @@ BESPOKE_APPS = { "org", "org-mode", "mu4e", + "gnus", "org-faces", "ghostel", "auto-dim-other-buffers", diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 550177c34..2748f326e 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -778,6 +778,21 @@ if(location.hash==='#mupreviewtest'){let ok=true;const notes=[];const A=(c,n)=>{ A(used.includes(f),'preview includes '+f); document.title='MUPREVIEWTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='mupreviewtest';d.textContent='MUPREVIEWTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} +// gnus-preview gate (open with #gnustest): gnus is its own view package (it drives +// the mu4e article view), and every data-face its preview emits is a real gnus face. +if(location.hash==='#gnustest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + A(!!APPS['gnus'],'gnus is a registered view package'); + A(APPS['gnus']&&APPS['gnus'].preview==='gnus','gnus uses the gnus preview renderer'); + const box=document.createElement('div');box.innerHTML=renderGnusPreview(); + const valid=new Set((APPS['gnus']&&APPS['gnus'].faces||[]).map(r=>r[0])); + const used=[...box.querySelectorAll('[data-face]')].map(e=>e.dataset.face); + A(used.length>=20,'preview exercises many faces ('+used.length+')'); + const bad=used.filter(f=>!valid.has(f)); + A(bad.length===0,'every data-face is a real gnus face; bad='+bad.join(',')); + for(const f of ['gnus-header-name','gnus-header-from','gnus-header-subject','gnus-cite-1','gnus-cite-attribution','gnus-signature','gnus-button','gnus-emphasis-highlight-words']) + A(used.includes(f),'preview includes '+f); + document.title='GNUSTEST '+(ok?'PASS':'FAIL'); + const d=document.createElement('div');d.id='gnustest';d.textContent='GNUSTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} // Box-cluster gate (open with #boxtest): the box control is a 2x2 cluster of // four radio buttons (none / line / pressed / raised); the color swatch shows // only while a box style is active. diff --git a/scripts/theme-studio/face_data.py b/scripts/theme-studio/face_data.py index bfce31cdd..a73db7137 100644 --- a/scripts/theme-studio/face_data.py +++ b/scripts/theme-studio/face_data.py @@ -316,3 +316,15 @@ SHR_FACES=("shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6 shr-text shr-link shr-sele SHR_SEED={ "shr-h1":{"fg":"gold","bold":True,"height":1.4},"shr-h2":{"fg":"blue","bold":True,"height":1.2},"shr-h3":{"fg":"blue","bold":True},"shr-h4":{"fg":"silver","bold":True},"shr-h5":{"fg":"steel","bold":True},"shr-h6":{"fg":"pewter","bold":True}, "shr-text":{"fg":"#cdced1"},"shr-link":{"fg":"blue","underline":True},"shr-selected-link":{"fg":"gold","bold":True,"underline":True},"shr-code":{"fg":"terracotta","bg":"bg-dim"},"shr-mark":{"fg":"#000000","bg":"gold"},"shr-strike-through":{"fg":"pewter","strike":True},"shr-sup":{"fg":"steel","height":0.8},"shr-abbreviation":{"fg":"steel","italic":True,"underline":True}} +# gnus drives the mu4e article (message) view: headers, quote levels, signature, +# buttons, and inline emphasis. gnus's own defaults are bright greens on a dark +# background, so these seeds restate the set in the theme palette. +GNUS_FACES=("gnus-header-name gnus-header-from gnus-header-subject gnus-header-content gnus-header-newsgroups " + "gnus-cite-1 gnus-cite-2 gnus-cite-3 gnus-cite-4 gnus-cite-5 gnus-cite-6 gnus-cite-7 gnus-cite-8 gnus-cite-9 gnus-cite-10 gnus-cite-11 gnus-cite-attribution " + "gnus-signature gnus-button " + "gnus-emphasis-bold gnus-emphasis-italic gnus-emphasis-underline gnus-emphasis-strikethru gnus-emphasis-highlight-words").split() +GNUS_SEED={ + "gnus-header-name":{"fg":"blue","bold":True},"gnus-header-from":{"fg":"gold"},"gnus-header-subject":{"fg":"white","bold":True},"gnus-header-content":{"fg":"silver"},"gnus-header-newsgroups":{"fg":"silver"}, + "gnus-cite-1":{"fg":"sage"},"gnus-cite-2":{"fg":"steel"},"gnus-cite-3":{"fg":"gold"},"gnus-cite-4":{"fg":"blue"},"gnus-cite-5":{"fg":"sage"},"gnus-cite-6":{"fg":"steel"},"gnus-cite-7":{"fg":"gold"},"gnus-cite-8":{"fg":"blue"},"gnus-cite-9":{"fg":"sage"},"gnus-cite-10":{"fg":"steel"},"gnus-cite-11":{"fg":"gold"},"gnus-cite-attribution":{"fg":"silver","italic":True}, + "gnus-signature":{"fg":"pewter","italic":True},"gnus-button":{"fg":"blue","underline":True}, + "gnus-emphasis-bold":{"bold":True},"gnus-emphasis-italic":{"italic":True},"gnus-emphasis-underline":{"underline":True},"gnus-emphasis-strikethru":{"fg":"pewter","strike":True},"gnus-emphasis-highlight-words":{"fg":"gold","bold":True}} diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index 632bbc23a..c489b79cc 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -228,6 +228,7 @@ _BESPOKE_APPS=[ ("magit","magit","magit",MAGIT_FACES,"magit-",MAGIT_SEED), ("elfeed","elfeed","elfeed",ELFEED_FACES,"elfeed-",ELFEED_SEED), ("mu4e","mu4e","mu4e",MU4E_FACES,"mu4e-",MU4E_SEED), + ("gnus","gnus (mu4e article view)","gnus",GNUS_FACES,"gnus-",GNUS_SEED), ("org-faces","org-faces","orgfaces",ORGFACES_FACES,"org-faces-",ORGFACES_SEED), ("ghostel","ghostel","ghostel",GHOSTEL_FACES,"ghostel-",GHOSTEL_SEED), ("auto-dim-other-buffers","auto-dim","autodim",AUTODIM_FACES,"auto-dim-other-buffers-",AUTODIM_SEED), diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 8bf0a5c8c..d64316c11 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -270,7 +270,7 @@