aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-08 02:30:43 -0500
committerCraig Jennings <c@cjennings.net>2026-06-08 02:30:43 -0500
commite622f65fd61e260f36a62250b8894f555b8680dc (patch)
treef64f7f0c9f1187f862d726d87a3e4b9e100c949f
parenteb5abe0a15bf0fd904bed2b27289bfb55089ee2a (diff)
downloaddotemacs-e622f65fd61e260f36a62250b8894f555b8680dc.tar.gz
dotemacs-e622f65fd61e260f36a62250b8894f555b8680dc.zip
feat(theme-selector): magit and elfeed bespoke previews (tier-3 phase 5)
I added renderMagitPreview and renderElfeedPreview. The magit one is a status buffer: head and branch lines, an untracked section, a diff hunk with context, removed, and added lines, and recent commits with hashes, authors, and a keyword and tag. The elfeed one is a search list: the filter line, dated entries with feed, unread and read titles, and tags, plus log lines colored by level. The preview pane dispatches to each app's bespoke renderer, the generic fallback covers everything else, and the pane label now names the app and says whether the preview is bespoke or generic.
-rw-r--r--scripts/theme-selector/generate.py36
-rw-r--r--scripts/theme-selector/theme-selector.html36
-rw-r--r--todo.org4
3 files changed, 70 insertions, 6 deletions
diff --git a/scripts/theme-selector/generate.py b/scripts/theme-selector/generate.py
index 7fc00145..80e9ad54 100644
--- a/scripts/theme-selector/generate.py
+++ b/scripts/theme-selector/generate.py
@@ -304,7 +304,7 @@ HTML = """<!doctype html><meta charset=utf-8><title>theme-selector</title>
<table class="leg" id="pkgtable"><thead><tr><th>face</th><th>fg</th><th>bg</th><th>weight</th><th>inherit</th><th>size</th><th>contrast</th><th></th></tr></thead><tbody id="pkgbody"></tbody></table>
</section>
<section class="pane grow" style="display:flex;flex-direction:column">
- <div class="langbar"><label style="color:#b4b1a2">preview (generic — face names in their own colors)</label></div>
+ <div class="langbar"><label id="pkgprevlabel" style="color:#b4b1a2">preview</label></div>
<div id="pkgpreview" class="mock" style="overflow:auto"></div>
</section>
</div>
@@ -571,8 +571,39 @@ function renderOrgPreview(){const a='org-mode',L=[];
L.push(' '+os(a,'org-table','| blue | #67809c |'));
return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\\n')}</div>`;
}
+function renderMagitPreview(){const a='magit',L=[];
+ L.push(os(a,'magit-head','Head:')+' '+os(a,'magit-branch-local','main')+' '+os(a,'magit-diff-revision-summary','Ship the tool'));
+ L.push(os(a,'magit-head','Merge:')+' '+os(a,'magit-branch-remote','origin/main'));
+ L.push(os(a,'magit-head','Push:')+' '+os(a,'magit-branch-remote','origin/main'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Untracked files (1)'));
+ L.push(' '+os(a,'magit-filename','notes.txt'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Unstaged changes (1)'));
+ L.push(os(a,'magit-diff-file-heading','modified generate.py'));
+ L.push(os(a,'magit-diff-hunk-heading','@@ -1,4 +1,5 @@ def main'));
+ L.push(os(a,'magit-diff-context',' unchanged context'));
+ L.push(os(a,'magit-diff-removed','- old line'));
+ L.push(os(a,'magit-diff-added','+ new line'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Recent commits'));
+ L.push(os(a,'magit-hash','b5b1869f')+' '+os(a,'magit-log-author','Craig')+' enlarge the picker');
+ L.push(os(a,'magit-hash','4fa5e995')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-keyword','feat')+' color picker');
+ L.push(os(a,'magit-hash','de07e01a')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-tag','v0.3')+' relative height');
+ return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\\n')}</div>`;}
+function renderElfeedPreview(){const a='elfeed',L=[];
+ L.push(os(a,'elfeed-search-filter-face','@6-months-ago +unread')+' '+os(a,'elfeed-search-unread-count-face','3/120'));
+ L.push('');
+ L.push(os(a,'elfeed-search-date-face','2026-06-08')+' '+os(a,'elfeed-search-feed-face','Planet Emacs')+' '+os(a,'elfeed-search-unread-title-face','New release of Magit')+' '+os(a,'elfeed-search-tag-face',':emacs:'));
+ L.push(os(a,'elfeed-search-date-face','2026-06-07')+' '+os(a,'elfeed-search-feed-face','LWN')+' '+os(a,'elfeed-search-unread-title-face','Kernel 6.18 lands')+' '+os(a,'elfeed-search-tag-face',':linux:'));
+ L.push(os(a,'elfeed-search-date-face','2026-06-05')+' '+os(a,'elfeed-search-feed-face','Hacker News')+' '+os(a,'elfeed-search-title-face','Show HN: a theme editor')+' '+os(a,'elfeed-search-tag-face',':show:'));
+ L.push('');
+ L.push(os(a,'elfeed-log-date-face','02:24:01')+' '+os(a,'elfeed-log-info-level-face','INFO ')+' updated 12 feeds');
+ L.push(os(a,'elfeed-log-date-face','02:24:02')+' '+os(a,'elfeed-log-warn-level-face','WARN ')+' slow feed: example.com');
+ L.push(os(a,'elfeed-log-date-face','02:24:03')+' '+os(a,'elfeed-log-error-level-face','ERROR')+' failed: bad.example');
+ return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\\n')}</div>`;}
function genericPreview(app){let h='<div style="padding:10px 14px;font:15px/1.8 monospace">';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+=`<div style="color:${efg};${ebg?'background:'+ebg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};font-size:${(f.height||1)}em">${esc(label)}</div>`;}return h+'</div>';}
-function buildPkgPreview(){const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return;p.innerHTML=(APPS[app].preview==='org')?renderOrgPreview():genericPreview(app);p.style.background=MAP['bg'];}
+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'];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'];}
@@ -611,6 +642,7 @@ function pkgSelftest(){
}
if(location.hash==='#selftest')pkgSelftest();
if(location.hash.startsWith('#pick')){openPicker();const m=location.hash.slice(5);if(m){const b=document.querySelector('.pmode button[data-m="'+m+'"]');if(b)b.click();}}
+if(location.hash.startsWith('#app')){const ap=location.hash.slice(4),s=document.getElementById('appsel');if(s&&ap){s.value=ap;pkgChanged();}}
</script>"""
HTML=(HTML.replace("SAMPLES_J",json.dumps(SAMPLES))
.replace("PALETTE_J",json.dumps(PALETTE)).replace("CATS_J",json.dumps(CATS))
diff --git a/scripts/theme-selector/theme-selector.html b/scripts/theme-selector/theme-selector.html
index 92190bc5..bc6308ff 100644
--- a/scripts/theme-selector/theme-selector.html
+++ b/scripts/theme-selector/theme-selector.html
@@ -128,7 +128,7 @@
<table class="leg" id="pkgtable"><thead><tr><th>face</th><th>fg</th><th>bg</th><th>weight</th><th>inherit</th><th>size</th><th>contrast</th><th></th></tr></thead><tbody id="pkgbody"></tbody></table>
</section>
<section class="pane grow" style="display:flex;flex-direction:column">
- <div class="langbar"><label style="color:#b4b1a2">preview (generic — face names in their own colors)</label></div>
+ <div class="langbar"><label id="pkgprevlabel" style="color:#b4b1a2">preview</label></div>
<div id="pkgpreview" class="mock" style="overflow:auto"></div>
</section>
</div>
@@ -395,8 +395,39 @@ function renderOrgPreview(){const a='org-mode',L=[];
L.push(' '+os(a,'org-table','| blue | #67809c |'));
return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\n')}</div>`;
}
+function renderMagitPreview(){const a='magit',L=[];
+ L.push(os(a,'magit-head','Head:')+' '+os(a,'magit-branch-local','main')+' '+os(a,'magit-diff-revision-summary','Ship the tool'));
+ L.push(os(a,'magit-head','Merge:')+' '+os(a,'magit-branch-remote','origin/main'));
+ L.push(os(a,'magit-head','Push:')+' '+os(a,'magit-branch-remote','origin/main'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Untracked files (1)'));
+ L.push(' '+os(a,'magit-filename','notes.txt'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Unstaged changes (1)'));
+ L.push(os(a,'magit-diff-file-heading','modified generate.py'));
+ L.push(os(a,'magit-diff-hunk-heading','@@ -1,4 +1,5 @@ def main'));
+ L.push(os(a,'magit-diff-context',' unchanged context'));
+ L.push(os(a,'magit-diff-removed','- old line'));
+ L.push(os(a,'magit-diff-added','+ new line'));
+ L.push('');
+ L.push(os(a,'magit-section-heading','Recent commits'));
+ L.push(os(a,'magit-hash','b5b1869f')+' '+os(a,'magit-log-author','Craig')+' enlarge the picker');
+ L.push(os(a,'magit-hash','4fa5e995')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-keyword','feat')+' color picker');
+ L.push(os(a,'magit-hash','de07e01a')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-tag','v0.3')+' relative height');
+ return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\n')}</div>`;}
+function renderElfeedPreview(){const a='elfeed',L=[];
+ L.push(os(a,'elfeed-search-filter-face','@6-months-ago +unread')+' '+os(a,'elfeed-search-unread-count-face','3/120'));
+ L.push('');
+ L.push(os(a,'elfeed-search-date-face','2026-06-08')+' '+os(a,'elfeed-search-feed-face','Planet Emacs')+' '+os(a,'elfeed-search-unread-title-face','New release of Magit')+' '+os(a,'elfeed-search-tag-face',':emacs:'));
+ L.push(os(a,'elfeed-search-date-face','2026-06-07')+' '+os(a,'elfeed-search-feed-face','LWN')+' '+os(a,'elfeed-search-unread-title-face','Kernel 6.18 lands')+' '+os(a,'elfeed-search-tag-face',':linux:'));
+ L.push(os(a,'elfeed-search-date-face','2026-06-05')+' '+os(a,'elfeed-search-feed-face','Hacker News')+' '+os(a,'elfeed-search-title-face','Show HN: a theme editor')+' '+os(a,'elfeed-search-tag-face',':show:'));
+ L.push('');
+ L.push(os(a,'elfeed-log-date-face','02:24:01')+' '+os(a,'elfeed-log-info-level-face','INFO ')+' updated 12 feeds');
+ L.push(os(a,'elfeed-log-date-face','02:24:02')+' '+os(a,'elfeed-log-warn-level-face','WARN ')+' slow feed: example.com');
+ L.push(os(a,'elfeed-log-date-face','02:24:03')+' '+os(a,'elfeed-log-error-level-face','ERROR')+' failed: bad.example');
+ return `<div style="padding:12px 16px;font:15px/1.7 monospace;white-space:pre">${L.join('\n')}</div>`;}
function genericPreview(app){let h='<div style="padding:10px 14px;font:15px/1.8 monospace">';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+=`<div style="color:${efg};${ebg?'background:'+ebg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};font-size:${(f.height||1)}em">${esc(label)}</div>`;}return h+'</div>';}
-function buildPkgPreview(){const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return;p.innerHTML=(APPS[app].preview==='org')?renderOrgPreview():genericPreview(app);p.style.background=MAP['bg'];}
+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'];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'];}
@@ -435,4 +466,5 @@ function pkgSelftest(){
}
if(location.hash==='#selftest')pkgSelftest();
if(location.hash.startsWith('#pick')){openPicker();const m=location.hash.slice(5);if(m){const b=document.querySelector('.pmode button[data-m="'+m+'"]');if(b)b.click();}}
+if(location.hash.startsWith('#app')){const ap=location.hash.slice(4),s=document.getElementById('appsel');if(s&&ap){s.value=ap;pkgChanged();}}
</script> \ No newline at end of file
diff --git a/todo.org b/todo.org
index d1566800..e75a456a 100644
--- a/todo.org
+++ b/todo.org
@@ -78,8 +78,8 @@ Added the "package faces" section: app selector (org/magit/elfeed), per-app face
*** 2026-06-08 Mon @ 02:27:51 -0500 Phase 4 — org preview landed
Added =renderOrgPreview()=: a mock org document painted live from the org package faces (title, headings with heights, TODO/DONE, tag, scheduled date, property drawer, inline code/verbatim, link, checkbox, quote, src block, header-row table). The preview pane dispatches on the app's preview key; org-mode gets this, others keep the generic list. Verified: node, headless screenshot, self-test PASS.
-*** TODO [#B] Phase 5 — magit + elfeed previews :solo:
-Bespoke mocks: a magit status buffer and an elfeed search list, live from their faces. Verify: screenshot. Fidelity to Manual testing. Spec phase 5.
+*** 2026-06-08 Mon @ 02:30:42 -0500 Phase 5 — magit + elfeed previews landed
+Bespoke =renderMagitPreview()= (status buffer: head/branches, untracked, a diff hunk with context/added/removed, recent commits with hashes/authors/keyword/tag) and =renderElfeedPreview()= (search list: filter, dated entries with feed/unread-title/read-title/tags, log lines by level). The preview label now names the app and notes generic vs bespoke. Verified: node, headless screenshots, self-test PASS.
*** TODO [#B] Phase 6 — generated all-package inventory :solo:
Build step queries Emacs for installed packages' faces grouped by package, writes a data file =generate.py= embeds; the dropdown then lists every package with an editable table + a labeled generic fallback preview. Verify: data file generates, dropdown lists packages, a fallback package edits and round-trips. Spec phase 6.