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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
#+TITLE: Theme-driven nerd-icons colors + theme-studio filetype legend — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-23
#+TODO: TODO | DONE SUPERSEDED CANCELLED
* Metadata
| Status | Ready pending Craig's go — Codex review rounds 1-3 incorporated |
|----------+------------------------------------------------------------|
| Owner | Craig |
|----------+------------------------------------------------------------|
| Reviewer | Codex (spec-review) |
|----------+------------------------------------------------------------|
| Related | [[file:../../todo.org][todo.org: theme-driven nerd-icons colors]] |
|----------+------------------------------------------------------------|
* Summary
nerd-icons glyphs (completion icons, dirvish, dashboard, ibuffer) are forced to a single color by a runtime tint in =nerd-icons-config.el=, which flattens the per-filetype color information nerd-icons ships and prevents the theme from controlling icon color. This spec drops that tint so icon color becomes theme-driven, and adds a theme-studio representation — a filetype legend over the 34 =nerd-icons-*= color faces — so the assignments are visible and editable where the rest of the theme is built.
* Problem / Context
=nerd-icons-config.el= defines =cj/nerd-icons-tint-color= (default "darkgoldenrod") and =cj/nerd-icons-apply-tint=, which sets the foreground of all 34 =nerd-icons-*= color faces to that one color, applied in the =nerd-icons= =:config= and a =with-eval-after-load= safety net. Every nerd-icons glyph therefore renders darkgoldenrod regardless of type.
Two costs. First, the per-filetype color nerd-icons provides is lost: =nerd-icons-extension-icon-alist= (330 entries) maps each filetype onto one of the 34 shared color faces (e.g. =.el/.sh= → =nerd-icons-purple=, an M-x command → =nerd-icons-blue=, =.zsh= → =nerd-icons-lcyan=), and the tint collapses all of that to one hue. Second, the color is not theme-driven: the tint runs at load and overrides whatever the theme set, so a theme can't own icon color.
theme-studio already inventories the 34 =nerd-icons-*= faces (via the generic package-inventory path), so they are technically editable today — but only as a bare list of face names with no preview of what each color actually hits. There is no representation of the filetype→color mapping, which is the thing that makes coloring these faces meaningful.
* Goals and Non-Goals
** Goals
- Icon color is theme-driven: the 34 =nerd-icons-*= color faces are set by the theme, not by a runtime tint.
- theme-studio shows a filetype legend — a representative set of filetypes rendered with their real icon glyph in the assigned color — that updates live as a face is recolored.
- The =nerd-icons-*= color-face assignments export into the theme and round-trip on import, like every other themed face.
** Non-Goals
- Per-filetype unique colors. nerd-icons shares 34 faces across 330 filetypes; the model is "color the 34 faces", not "assign 330 colors".
- Changing nerd-icons' glyph selection (=icon-for-file= / =icon-for-buffer= logic) or its icon set.
- An exhaustive 330-row legend. The legend is a curated representative sample, not the full alist.
** Scope tiers
- v1: capture a curated filetype legend (=nerd-icons-legend.json=); a bespoke nerd-icons preview rendering it; assign the theme colors and drop the tint atomically; export/round-trip; live verification in completion/dirvish/dashboard. Native colors already ride the existing default-face seed pipeline, so there is no new color capture.
- Out of scope: per-filetype assignment; editing the filetype→face mapping itself (that lives in nerd-icons).
- vNext: extend the legend beyond file extensions to buffer-mode and command/symbol categories if the file set proves insufficient; a "reset to nerd-icons native palette" button.
* Design
** For the user
theme-studio gains a nerd-icons pane. On the left, the 34 =nerd-icons-*= color faces as editable foreground rows (icons are foreground-colored, so foreground is the only relevant attribute). On the right, a filetype legend: a representative list of filetypes — a handful of languages, a directory, a command, a buffer — each drawn as its real icon glyph plus a sample name, in the color of the face that filetype maps to. Recoloring =nerd-icons-purple= immediately repaints every legend row that resolves to purple (=.el=, =.sh=, …), so the assignment's reach is visible at edit time. Export writes the face colors into the theme; with the runtime tint gone, those colors are what render in completing-read, dirvish, dashboard, and ibuffer.
** For the implementer
Three integration points, plus the config removal.
1. Data capture — two artifacts, two owners. Native seed colors are *not* captured here. They already ride theme-studio's existing default-face pipeline (=capture-default-faces.py= → =emacs-default-faces.json= → =apply_default_face_seeds= in =app_inventory.py=), which runs in clean =emacs -Q --batch= and stores each face's untinted =face-default-spec=. All 34 color faces plus =nerd-icons-completion-dir-face= are already present there with native colors (e.g. =nerd-icons-blue= #6A9FB5), so the seed is correct and untinted with no new code. The only *new* capture is the legend metadata: =nerd-icons-legend.json=, emitted by =build-nerd-icons-legend.el= and embedded by =generate.py=, listing the curated legend rows. The schema is in "Legend data contract" below.
2. Bespoke preview. nerd-icons moves from a generic inventory app to a bespoke app (a =BESPOKE_APP_SPECS= / =PREVIEW_KEYS= entry) with a new renderer in =previews.js= that draws the legend rows: for each curated filetype, its glyph + name styled with the effective color of its mapped face, read through the same registry the other previews use. The 34 faces remain editable rows.
3. Export/theme. The package-export path already serializes edited package faces into the theme; nerd-icons rides it. Confirm =build-theme.el= emits =nerd-icons-*= face specs and that WIP round-trips.
4. Config. Remove =cj/nerd-icons-tint-color=, =cj/--nerd-icons-color-faces=, =cj/nerd-icons-apply-tint=, and its two call sites in =nerd-icons-config.el=. The dir-icon advice (=cj/--nerd-icons-color-dir=, which layers =nerd-icons-yellow= onto directory glyphs that otherwise carry no color face) stays — its problem is independent of the tint — but now points at a theme-owned face.
** Legend data contract
The legend artifact is an array of rows, captured once in Emacs (it reads the nerd-icons alists), embedded by =generate.py= like the rest of =APPS=, and rendered by a =previews.js= renderer. Each row is:
- =key= — unique row id (=ext:el=, =dir=, =cmd=, =buf=).
- =label= — the sample name the row shows (=init.el=, =src/=, =M-x command=, =*scratch*=).
- =face= — the owner =nerd-icons-*= color face being themed. The row reads its effective color through the same registry the other previews use, so recoloring the face repaints the glyph live.
- =category= — =extension= | =dir= | =command= | =buffer=, naming the source.
- =glyph= — the nerd-font glyph string. The v1 renderer draws it in the owner face's color (see the rendering decision).
Source per category: =extension= rows from the =:face= of =nerd-icons-extension-icon-alist= entries; the =dir= row from =nerd-icons-yellow= (the dir-advice face, which wins — see the dir decision); =command=/symbol rows from the face in =nerd-icons-completion-category-icons=; =buffer= rows from the face resolved through =nerd-icons-mode-icon-alist=. v1 includes the curated extension rows plus one representative row each for dir, command, and buffer — the categories that actually surface in completing-read.
*** v1 legend rows
The exact v1 table (=key= | =label= | =category= | source lookup → owner =face=), chosen to span a diverse set of the color faces rather than cover all 34:
- =ext:el= | init.el | extension | ext-alist "el" → =nerd-icons-purple=
- =ext:py= | app.py | extension | "py" → =nerd-icons-dblue=
- =ext:org= | notes.org | extension | "org" → =nerd-icons-lgreen=
- =ext:md= | README.md | extension | "md" → =nerd-icons-lblue=
- =ext:ts= | main.ts | extension | "ts" → =nerd-icons-blue-alt=
- =ext:html= | index.html | extension | "html" → =nerd-icons-orange=
- =ext:rs= | lib.rs | extension | "rs" → =nerd-icons-maroon=
- =ext:js= | app.js | extension | "js" → =nerd-icons-yellow=
- =ext:yml= | ci.yml | extension | "yml" → =nerd-icons-dyellow=
- =ext:c= | main.c | extension | "c" → =nerd-icons-blue=
- =dir= | src/ | dir | =nerd-icons-icon-for-dir= + advice → =nerd-icons-yellow=
- =cmd= | M-x command | command | completion-category =command= → =nerd-icons-blue=
- =buf= | *scratch* | buffer | mode-alist =emacs-lisp-mode= → =nerd-icons-purple=
The =face= column is *resolved at capture time* from the live nerd-icons alists, not hardcoded here — the values above are the current resolution, recorded so the spec is self-checking. Coverage target: a representative showcase (the ~10 distinct faces above), not all 34 and not all 330 filetypes. A curated source key absent from the installed alist at capture time is skipped and logged, so the legend degrades gracefully across nerd-icons versions.
* Alternatives Considered
** A — Keep the tint, make it themeable (theme sets cj/nerd-icons-tint-color)
- Good, because it is the smallest change and preserves the single-knob simplicity.
- Bad, because it keeps one color for all icons, which is the exact thing this feature exists to undo — there is no per-filetype representation to build.
- Neutral, because it would still move color into the theme, just at the wrong granularity.
** B — Per-filetype color assignment (one row per extension)
- Good, because it is maximally granular.
- Bad, because 330 rows is unusable, and nerd-icons shares faces, so most rows would be redundant edits of the same underlying face.
- Neutral, because it matches no real user intent — nobody colors 330 extensions by hand.
** C (chosen) — Theme the 34 shared faces + a filetype legend
- Good, because it matches nerd-icons' actual color model, keeps the editable surface small (34), and the legend gives the "what does this color hit" representation Craig asked for.
- Neutral, because the legend is a curated subset of the 330 filetypes, so a rarely-used extension may not appear in the preview (though its color still themes via its shared face).
** D — Enhance the generic preview instead of a bespoke one
- Good, because it is less code.
- Bad, because the generic preview renders face-name rows; it cannot draw icon glyphs grouped by filetype, which is the whole representation.
* Decisions [6/6]
** DONE Color model: theme the 34 shared faces, not per-filetype
- Context: 330 filetypes map onto 34 shared color faces; the data fixes the granularity.
- Decision: We will expose the 34 =nerd-icons-*= color faces as the editable surface and use the filetype list only as a read-only legend.
- Consequences: easier — small editable surface, matches nerd-icons. Harder — a user wanting one filetype a different color from its face-mates can't, without nerd-icons changes (out of scope).
** DONE Legend scope: curated representative filetypes in v1
- Context: 330 entries is too noisy to preview; a representative set communicates the mapping. The row schema is in "Legend data contract".
- Decision: We will hand-curate a representative legend (common languages, a dir, a command, a buffer) covering the faces that actually appear in Craig's completion/dirvish use, and mark the set as the v1 legend.
- Consequences: easier — a clean, readable preview. Harder — the curated set needs occasional maintenance as nerd-icons' alist shifts; an uncovered face has no legend row (still themeable).
** DONE Seed colors: native colors ride the existing default-face pipeline
- Context: theme-studio needs nerd-icons' native palette, not the runtime tint, to seed from.
- Decision: We rely on theme-studio's existing default-face capture (=capture-default-faces.py= → =emacs-default-faces.json= → =apply_default_face_seeds=), which already runs in clean =emacs -Q --batch= and stores each face's untinted =face-default-spec=. No new color capture in =build-inventory.el=.
- Consequences: easier — zero new color-capture code, and the seed is already untinted. Harder — none. Supersedes the original draft's "build-inventory.el emits native defaults" (finding 3), which would have double-seeded. Verified: all 34 color faces + =nerd-icons-completion-dir-face= already present with native colors (=nerd-icons-blue= #6A9FB5).
** DONE Config sequencing: assign theme colors in the change that drops the tint
- Context: dropping the tint before the theme assigns the 34 faces makes completion icons jump from uniform darkgoldenrod to nerd-icons' native multicolor palette until the theme overrides them.
- Decision: We land the theme's explicit nerd-icons color assignments (Phase 4) in or before the change that removes the tint (Phase 3), so there is no uncolored interim. (Proposed by the author; reopen if you'd rather drop the tint standalone and accept the native-palette interim.)
- Consequences: easier — no ugly interim, one coherent switch. Harder — couples the config change to a theme edit rather than landing independently.
** DONE Dir advice, precedence, and cross-package ownership
- Context: directory glyphs carry no intrinsic color face. =cj/--nerd-icons-color-dir= layers =nerd-icons-yellow= via =add-face-text-property … nil= — the =nil= APPEND *prepends* the face, so it outranks any =:face= already on the string. =nerd-icons-completion-get-icon= passes =:face 'nerd-icons-completion-dir-face= to =nerd-icons-icon-for-dir=, but the advice's prepended =nerd-icons-yellow= wins. =nerd-icons-completion-dir-face= is owned by a *different* package (=nerd-icons-completion=) in =package-inventory.json=, and =apply_package_overrides= merges seed JSON by app key, so a face cannot be relocated into another app's pane.
- Decision: directory icons are colored by =nerd-icons-yellow= (prepended, wins in both completion and dirvish), so the legend's dir row models =nerd-icons-yellow= — a =nerd-icons= face the bespoke pane already owns. We keep =cj/--nerd-icons-color-dir=. =nerd-icons-completion-dir-face= stays under its own generic =nerd-icons-completion= app (export / import / lock keys unchanged) and is *not* pulled into the bespoke pane. A precedence ERT probe locks "yellow wins" (see Testing).
- Consequences: easier — the bespoke pane owns only =nerd-icons= faces, the dir row points at the face that actually renders, no cross-package merge tangle. Harder — =nerd-icons-completion-dir-face= is effectively inert for color while the advice is active (documented, not a bug).
** DONE Legend rendering: render the real glyph in its color (v1)
- Context: the glyphs are private-use nerd-font codepoints, so they only render where a Nerd Font is available. theme-studio's CSS declares none — but Nerd Fonts are installed system-wide on Craig's machine (verified via =fc-list=: JetBrainsMono, Hack, Meslo Nerd Font Mono), and Chrome uses system fonts, so a =font-family= rule renders the glyphs with no =@font-face= and no embedded font file.
- Decision: v1 legend rows render the captured =glyph= in the owner face's effective color (inline color style), via =font-family: "Symbols Nerd Font Mono", "Hack Nerd Font Mono", monospace=. The headless gate asserts the glyph char and the inline color (both in the DOM, font-independent), not the rendered pixels. The monospace fallback shows a placeholder box only where no Nerd Font exists, which won't happen on Craig's setup.
- Consequences: easier — the preview mirrors completing-read authentically (real glyph in real color). Harder — a machine-dependent font assumption (fine for a personal tool), and the gate asserts the glyph char + inline color rather than the glyph's pixels.
* Review findings [10/10]
** DONE Open decisions block implementation readiness :blocking:
Disposition (accepted): all six decisions are now resolved — the original five plus the legend-rendering decision the schema work surfaced — and the =[6/6]= cookie reads complete. Sequencing (Phase 4 lands with/before Phase 3) and the =nerd-icons-completion-dir-face= scope are both settled; two author-proposed calls (sequencing, glyph rendering) are marked reopen-if-disagree.
The spec still has five =TODO= decisions, including the sequencing of the tint removal relative to theme assignment and whether =nerd-icons-completion-dir-face= is in scope. That blocks readiness because an implementer would have to decide whether Phase 3 can land before Phase 4, and whether the directory completion face is part of the exported/verified surface. Resolve or explicitly risk-accept every decision before implementation starts. (blocking)
** DONE Legend row contract is underspecified :blocking:
Disposition (accepted): added the "Legend data contract" section — row schema (=key= / =label= / =face= / =category= / =glyph=), the per-category source for each row, and the v1 scope call (curated extension rows plus one representative dir / command / buffer row each). The browser data shape and the non-extension sources are now concrete.
The design says the legend includes filetypes plus "a directory, a command, a buffer," but Phase 1 only names =nerd-icons-extension-icon-alist= as the source. In the current code, package preview data is embedded through =APPS= in =scripts/theme-studio/generate.py= and rendered by =scripts/theme-studio/previews.js=, so the implementer needs a concrete browser data shape: the curated row key, display label/sample name, glyph text, owner face, source category, and what source supplies non-extension rows. Define that schema and state whether v1 really includes directory/command/buffer rows or only extension-backed rows. (blocking)
** DONE Native-palette capture path conflicts with the current seed pipeline :blocking:
Disposition (accepted): the finding is right — native colors are already owned by the existing default-face pipeline, which runs in =emacs -Q --batch= and stores untinted =face-default-spec= (verified: 35 nerd-icons faces already in =emacs-default-faces.json= with native colors). Dropped the draft's "build-inventory.el emits native defaults" entirely (it would have double-seeded). The only new capture is the legend metadata; the Seed-colors decision was rewritten to match.
The spec says =build-inventory.el= should emit native defface defaults, but the current Theme Studio default-color path already lives in =scripts/theme-studio/capture-default-faces.py= / =emacs-default-faces.json= and is applied in =scripts/theme-studio/app_inventory.py= via =apply_default_face_seeds=. =build-inventory.el= currently emits only package→face ownership. If the implementation adds native colors in the wrong artifact, nerd-icons could be seeded twice or the package default snapshot could keep carrying the runtime tint. Specify the intended owner: either extend the default-face capture to preserve the untinted =face-default-spec= values for =nerd-icons-*=, or add a separate nerd-icons metadata artifact and define exactly how it overrides =apply_default_face_seeds=. (blocking)
** DONE Curated legend contents are still a product decision :blocking:
Disposition (accepted): added the explicit "v1 legend rows" table (13 rows spanning ~10 distinct color faces), the coverage target (a representative showcase, not all 34 faces or all 330 filetypes), and the missing-source-key rule (a curated key absent from the installed alist is skipped + logged at capture).
The spec says to "hand-curate a representative legend" covering common languages plus dir/command/buffer rows, but it never names the actual v1 rows or the coverage target. That would force the implementer to decide whether the preview should cover every one of the 34 color faces, only Craig's common filetypes, only rows currently visible in completion/dirvish, or a smaller showcase. Define the exact v1 legend table (at least =key=, =label= / sample name, category, and source lookup key), plus the rule for a curated source key disappearing from the installed nerd-icons alist. (blocking)
** DONE Cross-package face ownership is undefined :blocking:
Disposition (accepted): resolved in the dir decision — the bespoke =nerd-icons= pane owns only =nerd-icons= faces; =nerd-icons-completion-dir-face= stays under its own generic =nerd-icons-completion= app (=apply_package_overrides= keys merges by app, verified, so a cross-package face can't be relocated). The dir legend row's owner is =nerd-icons-yellow=, a =nerd-icons= face, which is what actually colors dir icons.
The spec puts =nerd-icons-completion-dir-face= in the nerd-icons story, but Theme Studio's current model keys package rows by app/package: =package-inventory.json= owns the 34 color faces under =nerd-icons= and =nerd-icons-completion-dir-face= under =nerd-icons-completion=; =apply_package_overrides= merges seed JSON by app key; and =BESPOKE_APPS= only suppresses generic inventory for keys listed in =BESPOKE_APP_SPECS=. If the implementer places =nerd-icons-completion-dir-face= inside the new =nerd-icons= bespoke app, import/reset/lock/generic-suppression behavior is a design call. Specify whether v1 creates a composite "nerd-icons" pane that intentionally owns this cross-package face, keeps a separate =nerd-icons-completion= app, or excludes the completion dir face from the pane; include the export/import key and generic-inventory suppression rule. (blocking)
** DONE Directory color precedence is not defined :blocking:
Disposition (accepted): resolved in the dir decision — =cj/--nerd-icons-color-dir= prepends =nerd-icons-yellow= (=add-face-text-property … nil=, verified in source), so it wins over the =:face 'nerd-icons-completion-dir-face= that completion passes. Dir icons render =nerd-icons-yellow= in both completion and dirvish; the dir row models it; a precedence ERT probe locks it (Testing).
=nerd-icons-completion-get-icon= passes =:face 'nerd-icons-completion-dir-face= to =nerd-icons-icon-for-dir=, while =cj/--nerd-icons-color-dir= later layers =nerd-icons-yellow= onto the returned string. The spec says the dir row falls back to =nerd-icons-yellow= while =nerd-icons-completion-dir-face= is unset, but live rendering depends on the resulting face stack and Emacs face precedence when both faces carry foregrounds. Define the intended precedence for directory icons in completion and dirvish: which face wins when both are themed, whether one should remain unset, and what the preview row should model. Require an ERT or live probe that locks that precedence. (blocking)
** DONE Legend metadata artifact and failure behavior are open :blocking:
Disposition (accepted): Phase 1 now names the concrete artifact (=scripts/theme-studio/nerd-icons-legend.json=, committed like =package-inventory.json=), the generator (=build-nerd-icons-legend.el= sibling dump that loads =nerd-icons=), the refresh step, and the =generate.py= failure behavior (absent / malformed / empty → fall back to the generic nerd-icons app, warn, never error).
Phase 1 still says the legend rows are emitted by =build-inventory.el= "or a sibling dump," which leaves implementers to choose the artifact name, refresh command, checked-in status, and package-loading behavior. Define the concrete artifact path and generator entry point, whether it is committed like =package-inventory.json= / =emacs-default-faces.json=, which features it loads (=nerd-icons=, =nerd-icons-completion=), and what =generate.py= does when the artifact is absent, stale, malformed, or a required package is missing. (blocking)
** DONE Phase order contradicts the sequencing decision :blocking:
Disposition (accepted): merged tint removal and theme assignment into one atomic Phase 3 (assign the 34 colors + remove the tint together, no interim), with verification as Phase 4 — now consistent with the sequencing decision.
The resolved sequencing decision says explicit theme assignments must land in or before the tint removal, but =Implementation phases= still lists "Drop the runtime tint" as Phase 3 and "Theme assignment + verification" as Phase 4. That leaves the implementer to decide whether to follow the written phase order or the decision. Reorder or combine the phases so the build path cannot land a broken/interim state: theme assignment before tint removal, or a single phase that updates the theme and removes the tint atomically. (blocking)
** DONE Test plan does not name the contracts this feature changes :blocking:
Disposition (accepted): added a "Testing / Verification" section naming each contract's target — delete the apply-tint test, extend color-dir with the precedence probe, Python legend-schema + fallback tests, browser gates (live recolor, valid =data-face= owners), export/import round-trip, and the config smoke.
The acceptance criteria say =run-tests.sh= and launch smoke should pass, but the feature changes specific contracts that need named tests. =tests/test-nerd-icons-config--apply-tint.el= will become obsolete when the tint functions are removed; =tests/test-nerd-icons-config--color-dir.el= should remain and probably gain a precedence case; Theme Studio needs Python tests for the generated legend schema/composite app behavior, browser gates for live recolor + valid =data-face= owners, and an export/import round-trip covering =nerd-icons= plus any cross-package completion face. Add these test targets to the spec so implementation does not invent the verification surface. (blocking)
** DONE Stale summary sections contradict resolved decisions :blocking:
Disposition (accepted): reconciled every flagged section with the resolved decisions — Scope tiers (no new color capture; legend → =nerd-icons-legend.json=; atomic assign+drop), For-the-implementer summary (named =build-nerd-icons-legend.el= / =nerd-icons-legend.json=), the legend source paragraph (dir row is =nerd-icons-yellow= only), Architecture-fit and Dev-tooling and External-APIs readiness dimensions (=build-nerd-icons-legend.el=, no defface capture), and the Risks section (replaced the moot defface-reading risk with the stale-artifact / fallback risk).
The resolved decisions and Phase 1 now say native colors ride =capture-default-faces.py= / =emacs-default-faces.json=, legend metadata comes from =build-nerd-icons-legend.el= into =nerd-icons-legend.json=, and the dir row models only =nerd-icons-yellow=. Earlier text still says v1 captures native default colors "into the inventory," that legend metadata may be emitted by =build-inventory.el= or a sibling dump, that dir rows come from both =nerd-icons-yellow= and =nerd-icons-completion-dir-face=, that architecture integration uses =build-inventory.el= for capture, and that defface-default capture remains a Phase 1 assumption. Those contradictions would force an implementer to decide which contract wins. Update the Scope, For-the-implementer summary, Legend source paragraph, Readiness dimensions, and Risks so they all match the resolved decisions and phases. (blocking)
* Implementation phases
** Phase 1 — Legend capture
Emit the v1 legend rows (=key= / =label= / =face= / =category= / =glyph=) to =scripts/theme-studio/nerd-icons-legend.json=, a committed artifact like =package-inventory.json= / =emacs-default-faces.json=. The generator is a new sibling dump =scripts/theme-studio/build-nerd-icons-legend.el= (loads =nerd-icons=; resolves each curated key's glyph + owner face from the live alists), run via the same =emacsclient -e '(load …)'= step the inventory dumps use. =generate.py= embeds the JSON; if it is absent, malformed, or empty (nerd-icons not installed), =generate.py= logs a warning and nerd-icons falls back to its generic inventory app (no bespoke legend) — never an error. Native seed colors are *not* captured here; they already ride the existing default-face pipeline (=capture-default-faces.py= / =emacs-default-faces.json= / =apply_default_face_seeds=), which stores untinted =face-default-spec= (verified: 35 nerd-icons faces present). Tree stays working (data only; no UI change yet).
** Phase 2 — Bespoke nerd-icons preview
Register nerd-icons as a bespoke app with a legend renderer in =previews.js= drawing each curated filetype's glyph + name in its mapped face's effective color, live-updating on recolor. Browser-gated; existing previews unaffected.
** Phase 3 — Theme assignment + tint removal (atomic)
One change: assign the 34 =nerd-icons-*= colors in the WIP theme (and =WIP-theme.el=) *and* remove the tint defcustom / defconst / function + its two call sites from =nerd-icons-config.el=, together, so the icons never pass through an uncolored native-palette interim (the sequencing decision). Keep =cj/--nerd-icons-color-dir=. Live-reload + launch smoke.
** Phase 4 — Verification
Export → =WIP-theme.el= and re-import round-trip over the =nerd-icons= faces; live check in completing-read / dirvish / dashboard / ibuffer; run the dir-precedence ERT probe (yellow wins). See Testing.
* Acceptance criteria
- [ ] theme-studio shows a nerd-icons pane: 34 editable foreground rows + a filetype-legend preview that renders each row's real nerd-font glyph in its assigned color (monospace fallback).
- [ ] Recoloring a =nerd-icons-*= face repaints every legend row mapped to it, live.
- [ ] theme-studio opens seeded with nerd-icons' native palette (not darkgoldenrod).
- [ ] Export includes the =nerd-icons-*= face specs; re-import round-trips to the same state.
- [ ] =nerd-icons-config.el= no longer tints; the 34 faces are owned by the theme.
- [ ] In a live frame, completion / dirvish / dashboard icons render the theme's per-filetype colors.
- [ ] run-tests.sh green (Node + browser gates + ERT + Python); =make validate-modules= + launch smoke clean.
* Readiness dimensions
- Data model & ownership: the 34 color faces are user-authored via theme-studio and owned by the theme; the filetype→face legend and native defaults are generated (captured from nerd-icons), read-only. No remote/cache.
- Errors, empty states & failure: a face with no curated legend row simply has no preview row (still editable); a missing nerd-icons (package absent) skips the bespoke app — capture must degrade to the generic path, not error.
- Security & privacy: N/A because no credentials or sensitive data.
- Observability: the legend preview *is* the observability — the user sees exactly which filetypes a color hits before committing.
- Performance & scale: 34 faces + a curated legend (tens of rows); trivial. The capture dump runs once in Emacs, not per render.
- Reuse & lost opportunities: reuses the existing inventory pipeline, the package-export path, and the preview registry from preview-locate; nerd-icons already supplies the extension→face mapping, so we read it rather than re-derive.
- Architecture fit & weak points: integration points are =build-nerd-icons-legend.el= → =nerd-icons-legend.json= (legend capture), =app_inventory.py= / =face_data.py= (bespoke registration), =previews.js= (renderer), =generate.py= (embed), =nerd-icons-config.el= (tint removal). Weak point: the curated legend can drift from nerd-icons' alist over versions — mitigated by deriving the mapping from the live alist at capture time, curating only which filetypes to show, plus the missing-key skip.
- Config surface: removes =cj/nerd-icons-tint-color= and its machinery; no new public knob (the theme is the surface). =cj/--nerd-icons-color-dir= advice retained.
- Documentation plan: update =nerd-icons-config.el= commentary; a note in theme-studio's =theme-coloring-guide.org= on the nerd-icons pane. No user-facing migration doc needed (personal config).
- Dev tooling: run-tests.sh (theme-studio), =make validate-modules= + launch smoke (config); the legend dump is an =emacsclient -e '(load build-nerd-icons-legend.el)'= step, mirroring the existing inventory dumps.
- Rollout, compatibility & rollback: the change alters the persisted theme (adds nerd-icons face specs) and removes a config knob. Rollback = restore the tint block and drop the nerd-icons specs from the theme. The sequencing decision exists to avoid an uncolored interim.
- External APIs & deps: depends on nerd-icons internals — =nerd-icons-extension-icon-alist= entry shape (=("ext" fn "glyph" :face SYM)=), the dir/mode/completion-category alists, and the 34 =nerd-icons-*= color faces. Verified live this session (330 entries, sample confirmed; 34 faces inventoried; native colors already in =emacs-default-faces.json=). No new defface capture — seeding rides the existing pipeline.
* Risks, Rabbit Holes, and Drawbacks
- The legend artifact (=nerd-icons-legend.json=) is captured once and committed, so it can go stale if nerd-icons' alists shift — mitigated by the =generate.py= fallback (absent/malformed → generic app), the missing-key skip at capture, and the refresh step in dev tooling. No defface reading in this feature: native seed colors already live untinted in =emacs-default-faces.json= (verified), so the earlier defface-introspection risk is moot.
- The legend is HTML, not Emacs faces: rows render the captured glyph char in a CSS color from the registry, so there's no live Emacs dependency at preview time. The one font dependency is a Nerd Font for the glyph shape — present system-wide on Craig's machine, with a monospace fallback to a placeholder box elsewhere. The gate asserts the glyph char and inline color, not the rendered pixels.
* Testing / Verification
The feature changes specific contracts; each gets a named target rather than leaning on a blanket "run-tests.sh passes".
- =tests/test-nerd-icons-config--apply-tint.el= — *delete* when the tint functions are removed (Phase 3); it tests code that no longer exists.
- =tests/test-nerd-icons-config--color-dir.el= — *keep and extend* with a precedence case: with both =nerd-icons-yellow= and =nerd-icons-completion-dir-face= carrying foregrounds, the advice's prepended =nerd-icons-yellow= is first in the face list (wins). This is the ERT probe that locks the dir-precedence decision.
- Python (=test_generate.py= / a new test): the =nerd-icons-legend.json= schema (every row has =key= / =label= / =face= / =category= / =glyph=; face is a known =nerd-icons-*= face), and the bespoke-vs-generic fallback (absent/malformed artifact → generic app, no crash).
- Browser gates: the nerd-icons legend renders; recoloring a face repaints every row mapped to it; every legend element carries a valid owner =data-face= (the owner-aware gate from preview-locate); the dir row models =nerd-icons-yellow=.
- Export/import round-trip: a WIP with assigned =nerd-icons-*= colors exports and re-imports to the same state; =nerd-icons-completion-dir-face= (separate app) is untouched by the nerd-icons pane.
- Config: =make validate-modules= + launch smoke after the tint removal (Phase 3).
* Review and iteration history
** 2026-06-23 Tue @ 22:17:25 -0400 — Claude — author
- What: initial draft.
- Why: Craig chose to spec the drop-the-tint + theme-studio filetype-legend feature before building (spans config + three theme-studio layers + the theme, with real trade-offs on color model, legend scope, seeding, and sequencing).
- Artifacts: docs/specs/theme-studio-nerd-icons-colors-spec.org; todo.org task (to be created at hand-off).
** 2026-06-23 Tue @ 22:29:01 -0400 — Codex (emacs-d) — reviewer
- *What changed or was recommended:* First review, =Not ready=. Added three blocking findings: unresolved decisions still gate readiness; the legend needs a concrete generated data contract, especially for non-extension rows; and native-palette capture must be assigned to the existing default-face seed pipeline or to a clearly defined metadata override.
- *Why:* The code read confirmed the main direction fits the current implementation: =modules/nerd-icons-config.el= owns the bulk tint and dir advice; Theme Studio package views are generated from =BESPOKE_APP_SPECS= / =APPS= and rendered through =PACKAGE_PREVIEWS=; package export already serializes package face specs. The remaining gaps would still force implementers to invent schema, seed ownership, and sequencing behavior mid-build.
- *Artifacts:* findings [0/3]; code read: =modules/nerd-icons-config.el= (=cj/nerd-icons-apply-tint=, =cj/--nerd-icons-color-dir=), =scripts/theme-studio/build-inventory.el=, =scripts/theme-studio/capture-default-faces.py= / =emacs-default-faces.json=, =scripts/theme-studio/app_inventory.py=, =scripts/theme-studio/face_data.py=, =scripts/theme-studio/generate.py=, =scripts/theme-studio/previews.js=, =scripts/theme-studio/build-theme.el=, =tests/test-nerd-icons-config--apply-tint.el=, =tests/test-nerd-icons-config--color-dir.el=, =todo.org=.
** 2026-06-23 Tue @ 22:37:50 -0400 — Claude — responder
- *What changed:* All three blocking findings accepted and folded in. Added a "Legend data contract" section (row schema + per-category sources + v1 scope). Corrected the data-capture story: native seed colors are owned by the existing default-face pipeline (verified untinted in =emacs-default-faces.json=), so the draft's =build-inventory.el= native-color capture is dropped; only the legend metadata is newly captured. Resolved all five open decisions and added a sixth (legend renders a swatch + label in v1, not the nerd-font glyph). Phase 1 retitled to "Legend capture".
- *Why:* The findings were correct against the code read. The seed-pipeline conflict was the load-bearing one — capturing native colors in =build-inventory.el= would have double-seeded against =apply_default_face_seeds=. Two calls (sequencing, glyph rendering) are author-proposed and marked reopen-if-disagree.
- *Artifacts:* findings [3/3]; decisions [6/6]; code verified this session: =capture-default-faces.py= (reads =face-default-spec= under =-Q=), =emacs-default-faces.json= (35 nerd-icons faces, native colors), =nerd-icons-extension-icon-alist= (330) / =nerd-icons-dir-icon-alist= (27) / =nerd-icons-completion-category-icons= sources.
** 2026-06-23 Tue @ 22:56:49 -0400 — Claude — responder (decision flip)
- *What changed:* Flipped the legend-rendering decision from "swatch + label, glyph deferred to vNext" to v1 rendering the real glyph in its color, with a monospace fallback. Updated the schema's glyph/face notes, the acceptance criterion, and the font-dependency risk.
- *Why:* Craig asked why glyph rendering was deferred. The deferral rested on an unverified font dependency. Verified Nerd Fonts are installed system-wide (=fc-list=), so Chrome renders the glyphs from a =font-family= rule with no =@font-face= or embedded font file — v1-cheap. The authentic glyph beats the swatch, and the gate can still assert the glyph char + inline color.
- *Artifacts:* =fc-list= (system Nerd Fonts present: JetBrainsMono / Hack / Meslo Nerd Font Mono); decisions unchanged [6/6].
** 2026-06-23 Tue @ 23:03:39 -0400 — Codex (emacs-d) — reviewer
- *What changed or was recommended:* Second review, =Not ready=. Added six blocking findings: exact curated legend rows are still unspecified; =nerd-icons-completion-dir-face= crosses package ownership boundaries; directory face precedence between =nerd-icons-completion-dir-face= and =nerd-icons-yellow= is undefined; the legend metadata artifact/generator/failure behavior is open; the implementation phase order contradicts the sequencing decision; and the test plan needs named contract tests.
- *Why:* Reading as the implementer, the remaining gaps are all answer-changing. The current Theme Studio model keys package faces by app in =APPS= / =PKGMAP= and merges seed JSON by that key, while the spec wants one preview pane to reason across =nerd-icons= and =nerd-icons-completion=. The installed packages confirm the source shapes: =nerd-icons-extension-icon-alist=, =nerd-icons-dir-icon-alist=, =nerd-icons-mode-icon-alist=, and =nerd-icons-completion-category-icons= exist, but they do not by themselves choose the curated UX rows, ownership, precedence, or tests.
- *Artifacts:* findings [3/9]; code read: =scripts/theme-studio/app_inventory.py= (=BESPOKE_APPS= / =apply_package_overrides=), =scripts/theme-studio/generate.py= (=APPS= construction), =scripts/theme-studio/app.js= (=PKGMAP= / package table / =PACKAGE_PREVIEWS=), =scripts/theme-studio/browser-gates.js= (=assertPreviewFaces= owner-aware validation), =elpa/nerd-icons-20260325.346/nerd-icons.el= (=nerd-icons-extension-icon-alist=, =nerd-icons-dir-icon-alist=, =nerd-icons-mode-icon-alist=, =nerd-icons-icon-for-dir=), =elpa/nerd-icons-completion-20251029.2106/nerd-icons-completion.el= (=nerd-icons-completion-category-icons=, =nerd-icons-completion-dir-face=, =nerd-icons-completion-get-icon=), =tests/test-nerd-icons-config--apply-tint.el=, =tests/test-nerd-icons-config--color-dir.el=, =scripts/theme-studio/run-tests.sh=.
** 2026-06-23 Tue @ 23:12:16 -0400 — Claude — responder
- *What changed:* All six round-2 findings accepted and folded in. Added the explicit "v1 legend rows" table (13 rows, ~10 distinct faces) + the missing-key rule. Rewrote the dir decision to settle precedence (=cj/--nerd-icons-color-dir= prepends =nerd-icons-yellow=, so it wins — verified in source) and cross-package ownership (the bespoke pane owns only =nerd-icons= faces; =nerd-icons-completion-dir-face= stays in its own generic app, since =apply_package_overrides= keys by app). Phase 1 names the concrete artifact (=nerd-icons-legend.json= + =build-nerd-icons-legend.el= generator + =generate.py= fallback). Reordered phases into an atomic Phase 3 (assign + drop-tint together) and a Phase 4 verification, matching the sequencing decision. Added a "Testing / Verification" section naming each changed contract's target.
- *Why:* The round was a strong implementer-eyed pass; every finding would have forced an invented contract mid-build. The load-bearing ones were the cross-package ownership (a face can't move between app-keyed panes) and the dir precedence (resolved by reading the prepend semantics), which together let the dir row point at =nerd-icons-yellow= and sidestep the tangle.
- *Artifacts:* findings [9/9]; decisions [6/6]; verified: extension/command/buffer face resolution from the live alists (the 13-row table), =app_inventory.py= =apply_package_overrides= (app-keyed merge), =cj/--nerd-icons-color-dir= prepend (=add-face-text-property … nil=).
** 2026-06-23 Tue @ 23:17:51 -0400 — Codex (emacs-d) — reviewer
- *What changed or was recommended:* Third review, =Not ready=. Added one blocking finding: stale summary/readiness/risk text still contradicts the resolved implementation contract for native-color capture, legend artifact ownership, directory row ownership, and Phase 1 verification.
- *Why:* The main decisions now look implementation-ready, but an implementer should not have to reconcile earlier prose against the later decisions. The contradictions point at different code paths (=build-inventory.el= vs =build-nerd-icons-legend.el=, color capture vs default-face seeding, =nerd-icons-completion-dir-face= vs =nerd-icons-yellow=) and could produce the wrong implementation if followed literally.
- *Artifacts:* findings [9/10]; code read: =modules/nerd-icons-config.el= (=cj/--nerd-icons-color-dir= and tint removal target), =scripts/theme-studio/app_inventory.py= (=BESPOKE_APPS=, app-keyed merge), =scripts/theme-studio/generate.py= (=APPS= construction), =scripts/theme-studio/face_data.py= (=BESPOKE_APP_SPECS=), =elpa/nerd-icons-20260325.346/nerd-icons.el= and =elpa/nerd-icons-completion-20251029.2106/nerd-icons-completion.el= (legend source facts), =todo.org=.
** 2026-06-23 Tue @ 23:24:56 -0400 — Claude — responder
- *What changed:* Accepted the one round-3 finding (pure consistency cleanup) and reconciled the stale summary text with the resolved decisions: Scope tiers, the For-the-implementer data-capture summary, the legend source paragraph (dir row = =nerd-icons-yellow= only), the Architecture-fit / Dev-tooling / External-APIs readiness dimensions (=build-nerd-icons-legend.el= → =nerd-icons-legend.json=, no defface capture), and the Risks section (dropped the moot defface-introspection risk for the stale-artifact / fallback risk). No decision or contract changed — only the prose that lagged behind them.
- *Why:* Round 2 settled the contracts but left earlier prose describing the superseded ones (color capture, =build-inventory.el=, dual dir sources), which an implementer could follow literally into the wrong path.
- *Artifacts:* findings [10/10]; decisions [6/6]; no code re-verification needed — the edits align prose to already-verified decisions.
|