1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
#+TITLE: theme-selector — package faces (tier 3), starting with org-mode
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
* Status
Spec / awaiting review. Proposes a third tier for the theme-selector
(scripts/theme-selector/) that lets a theme colorize package-specific faces,
built one application at a time. org-mode is the first application.
* Background — the three tiers
The theme-selector already models two tiers of faces:
1. *Syntax* — the font-lock / tree-sitter categories (keyword, string, type,
comment, etc.), in the "code/color assignments" table.
2. *UI* — Emacs's built-in interface faces (cursor, region, mode-line, fringe,
line numbers, isearch, and the rest), in the "ui faces" table with the live
mock-frame preview.
Tier 3 is *package faces*: faces a package declares with =defface= so a theme
can color the package as it wishes. The running config has 1,146 such faces
across 186 packages (magit 111, lsp-mode 97, telega 91, web-mode 82, org ~30
core, and a long tail). No theme colors all of them; quality themes hand-pick
the packages the user actually lives in and theme those.
This spec adds a tier-3 section to the tool, structured so applications are
added one at a time. org-mode ships first.
* Goal
A new "package faces" section with:
1. An *application dropdown* — pick which package's faces to edit (org-mode
first; magit, elfeed, dirvish, telega, marginalia, consult to follow).
2. A *face table* for the selected app — one row per curated face, each with a
foreground dropdown, a background dropdown, and bold / italic toggles, all
drawing from the same palette as the other tables.
3. A *preview pane* for the selected app — a realistic mock of that package
rendered with the live theme, the way the ui-faces mock-frame shows the UI
faces in a buffer. org-mode gets a mock org document.
The export (=theme.json=) gains a =packages= object so the build step can set
these faces too.
* UI placement
A new top-level section under the ui-faces row:
#+begin_example
<h1>package faces</h1>
[ application: (org-mode v) ]
<div class="cols stretch">
left = the selected app's face table (fg / bg / B / I per face)
right = the selected app's preview pane (e.g. the org document mock)
</div>
#+end_example
Same two-column stretch layout as the ui-faces row, so the preview matches the
table's height.
* Data model
A single data structure drives everything, keyed by application:
#+begin_src js
APPS = {
"org-mode": {
label: "org-mode",
faces: [
// face, human label, default {fg, bg, bold, italic}
["org-document-title", "document title", {fg:"gold", bold:true}],
["org-level-1", "heading 1", {fg:"blue", bold:true}],
["org-level-2", "heading 2", {fg:"gold"}],
["org-level-3", "heading 3", {fg:"regal"}],
["org-todo", "TODO keyword", {fg:"terracotta", bold:true}],
["org-done", "DONE keyword", {fg:"sage", bold:true}],
["org-link", "link", {fg:"blue"}], // base `link`
["org-code", "inline code", {fg:"terracotta"}],
["org-verbatim", "verbatim", {fg:"steel"}],
["org-block", "src block body", {fg:"white", bg:"bg-dim"}],
["org-block-begin-line","block delim", {fg:"pewter", bg:"bg-dim"}],
["org-table", "table", {fg:"steel"}],
["org-date", "timestamp", {fg:"steel"}],
["org-tag", "tag", {fg:"tan"}],
["org-special-keyword","keyword/drawer", {fg:"pewter"}],
["org-meta-line", "#+meta line", {fg:"pewter"}],
["org-checkbox", "checkbox", {fg:"gold"}],
["org-headline-done", "done headline", {fg:"pewter"}],
],
preview: "org" // names the preview renderer
},
// magit, elfeed, ... added later with the same shape
}
#+end_src
Defaults reference palette *names* (blue, gold, ...) resolved to hexes at load,
so a curated app seeds sensibly from the current palette. The user reassigns
any face from the palette dropdowns exactly like the other tables.
State mirrors the other tiers: a =PKGMAP= of
={app: {face: {fg, bg, bold, italic}}}=, edited live, rendered into the table
and the preview.
* The org preview
A mock org document painted from PKGMAP["org-mode"] plus the palette ground/fg.
One bespoke renderer (=renderOrgPreview()=) drawing a representative document:
#+begin_example
#+TITLE: Project Notes <- org-document-title
#+AUTHOR: ... <- org-meta-line / document-info
* Inbox :work: <- org-level-1 + org-tag
** TODO Draft the spec <- org-level-2 + org-todo
SCHEDULED: <2026-06-08 Sun> <- org-special-keyword + org-date
** DONE Ship the tool <- org-level-2 + org-done (headline-done)
*** Heading three <- org-level-3
A line with =inline code=, <- org-code
~verbatim~, and a [[link]]. <- org-verbatim + org-link
- [X] a checkbox item <- org-checkbox
#+begin_src elisp <- org-block-begin-line
(message "hi") <- org-block
#+end_src <- org-block-end-line
| name | hex | <- org-table (header row org-table-header)
|------+---------|
| blue | #67809c |
#+end_example
Each marked element is a span colored from the corresponding PKGMAP face. The
preview rebuilds whenever a package face or the palette changes, same as the
mock frame.
For later apps, each gets its own preview renderer (magit -> a status buffer
mock, elfeed -> a search-list mock). Until an app has a bespoke preview, it
falls back to a generic "face name rendered in its own colors" list, so a new
app is usable the moment its face list is added, and gets a real preview when
one is written.
* Export schema
=theme.json= gains a =packages= key:
#+begin_src json
{
"name": "dupre",
"palette": [...],
"assignments": {...},
"bold": [...], "italic": [...],
"ui": {...},
"packages": {
"org-mode": {
"org-level-1": {"fg":"#67809c","bg":null,"bold":true,"italic":false},
"org-todo": {"fg":"#cb6b4d","bg":null,"bold":true,"italic":false}
}
}
}
#+end_src
Only faces the user actually touched (or the curated defaults) are written. The
build step's converter sets each as a normal face. Backward compatible: a file
without =packages= loads fine.
* Build-step consumption
The eventual =theme.json= -> =dupre-*.el= converter already owns tiers 1 and 2.
Tier 3 adds, per package face:
#+begin_src elisp
(org-level-1 ((t (:foreground "#67809c" :weight bold))))
(org-todo ((t (:foreground "#cb6b4d" :weight bold))))
#+end_src
No new converter machinery — package faces are just more faces. This is the
TDD-worthy part (JSON in, valid faces out), same as the rest of the converter.
* Scope for v1
- Build the section, the app dropdown, the org-mode face table, and the org
preview.
- Seed org's ~18 curated faces (above), not all 104 org-* faces (most are
org-roam / superstar / agenda noise the core theme need not touch).
- Wire export/import of the =packages= key.
- Leave the converter for the separate build-step task; the spec only needs the
schema to be right.
* Extensibility (adding the next app)
1. Add an entry to =APPS= (label, curated face list with palette-name defaults,
preview key).
2. Optionally write a bespoke preview renderer; until then the generic fallback
renders.
3. Nothing else changes — the dropdown, table, export, and import are all
data-driven off =APPS= / =PKGMAP=.
* Open questions
1. *Curated set size.* ~18 org faces proposed. Add =org-level-4..8=, quote,
verse, drawer, footnote, priority? Or keep it tight and grow on demand?
2. *Bold/italic source.* org defaults carry weight (headings, todo). Seed those
as the curated defaults, or start everything normal and let the user set
weight? Proposed: seed the obvious ones (title, levels, todo, done bold).
3. *Inherit relationships.* Many org faces inherit (org-level-N from outline-N;
org-code/verbatim from =shr= / =fixed-pitch=). Model inheritance, or set
absolute values per face? Proposed: absolute values, simplest and matches
how the converter writes them.
4. *App order after org.* magit (111 faces, highest payoff), elfeed, dirvish,
marginalia, consult, telega — which next, and how many?
5. *Preview fidelity.* Bespoke per-app previews are the most work. Is the
generic fallback acceptable for the long tail, with bespoke previews only
for org and magit?
* Files touched
- =scripts/theme-selector/generate.py= — the section, =APPS= data, the package
face table, =renderOrgPreview()=, export/import of =packages=.
- =scripts/theme-selector/theme-selector.html= — regenerated.
- (later) the =theme.json= -> =dupre-*.el= converter — consumes =packages=.
|