#+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 (gallery, SHIPPED): the nerd-icons pane becomes an icon grid — one row per color face, rows ordered by hue so families cluster, distinct icons (deduped within a color) drawn in their color with the icon name beneath, plus a per-size preview dropdown. Replaces the v1 legend in the pane (legend data retained). See "vNext — nerd-icons gallery" below. Subsumes the two roam asks (one representative icon per color; group the colors together). - vNext (later): 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). * vNext — nerd-icons gallery (icon grid by color) — SHIPPED This increment builds on shipped v1 and the shipped glyph-rendering infrastructure (the embedded =ThemeStudioNerd= woff2 + the unquoted-inline-font fix that lets nerd-font glyphs render in the browser). It is purely additive display — no config change, no theme change, no new editable surface. The 34 =nerd-icons-*= faces are already themed and editable from v1; the gallery is a read-only view of every distinct icon, organized by the color it renders in. The design evolved during the build. The first cut rendered the *full* catalog (every face-bearing mapping, ~700 glyphs, duplicates kept) as a sequence of flowing color sections below the v1 legend, ordered by glyph count, with the source key on hover. Craig redesigned it after a live look into the shipped shape below: a grid of *distinct* icons (deduped within a color), rows ordered by *hue* so families cluster, the icon *name* shown beneath each glyph, the v1 legend dropped from the pane, and a *per-size preview dropdown* so the designer can view the grid at different font sizes. The decisions below record the shipped choices; the superseded ones are noted inline. ** Summary The nerd-icons pane is a grid: one row per =nerd-icons-*= color face, the rows ordered by hue (ascending) so color families sit together (pinks/reds/oranges, yellows, greens, cyans, the grays, blues, purples). Each row is a swatch + face-name header over a wrapping set of cells; each cell draws one icon in the face's color with the icon's nerd-font name (=nf-dev-terminal=) beneath it. Icons are deduplicated within a color, so the ~700 face-bearing mappings collapse to ~314 distinct glyphs. Recoloring a face repaints its swatch and every icon in its row live. Above the grid, a "preview:" dropdown selects the glyph size. ** For the user The pane shows the grid (the v1 legend preview is gone from view; its data is still captured for round-trip and reference). Color-level locate is preserved: clicking a color in the faces table flashes every icon in that row, and clicking an icon flashes its color's row — icons aren't individually editable, only their color is. A "preview:" dropdown above the grid picks the font size; Left/Right arrows step through the sizes when it is focused. ** For the implementer Three integration points; no config or theme path is touched. 1. Catalog capture. =build-nerd-icons-legend.el= is now a library (capture functions + one entry point, =cj/nerd-icons-write-legend=), so the pure logic unit-tests without nerd-icons. It walks every =:face=-bearing alist (=nerd-icons-extension-icon-alist=, =nerd-icons-regexp-icon-alist=, =nerd-icons-mode-icon-alist=), dedupes icons by name within each owner face, sorts each face's icons by name, computes each face's native hue from its defface foreground, and orders the groups by hue (ascending, ties by descending lightness). It emits into the *same* =nerd-icons-legend.json= under a =gallery= key (v1 rows stay under =legend=). nerd-icons is required only inside the writer, so the file loads tint-free for tests; the daemon invocation is =(progn (load …) (cj/nerd-icons-write-legend))=. =generate.py='s fallback covers absent/malformed/empty → generic app, never an error. 2. Grid renderer. =renderNerdIconsPreview(sizePt)= in =previews.js= draws the =gallery= groups: per group a swatch + face-name header, then a cell per icon (the glyph in the face's color at =sizePt=, the icon name beneath). It reads color through the same effective-color registry, so recolor repaints live; glyphs render in =ThemeStudioNerd=. With no gallery it falls back to =genericPreview=. Registered under the existing bespoke nerd-icons app. 3. Preview-pane dropdown. =previewPanes(app)= / =buildPkgPreview= in =app.js= turn the old static preview label into "preview:" + a ==. One pane → disabled, names the preview. Multiple panes → enabled. nerd-icons gets one pane per font size in *points* (10/12/14/16/20/24/32/48, default 14) — pt because Emacs sizes fonts in =:height= (1/10 pt), so a pane maps to a real buffer size. The top sizes (32/48) are for inspecting a glyph's detail; the cell width scales with the size, so beyond ~48 pt the 314-icon grid becomes mostly scrolling. Left/Right arrows step the focused dropdown; the size persists per app. The dropdown is multi-pane only when the gallery actually exists, so a failed capture can't promise sizes the renderer can't draw. - Consequences: easier — size selection with no new UI surface; the mechanism generalizes to any future multi-pane app. Harder — a second piece of per-pane state (the selected index). *** DONE Font embed: full Symbols Nerd Font Mono stays embedded - Context: v1 deferred the full-font (2.1M HTML) vs glyph-subset call; the gallery forces it. - Decision: keep the full =ThemeStudioNerd= woff2 embedded as a data: URI — the grid draws the whole glyph set, so a v1-curated subset would not cover it. - Consequences: easier — every glyph renders, no subset bookkeeping. Harder — the ~2.1M self-contained HTML stays (fine for a personal tool). ** Locate under the grid (color-level association) The gallery inverts the studio's usual ~1:1 element↔face association: the visible unit is the icon, but the only editable, locatable handle is the color, which owns ~10-40 icons. Decision (Craig): keep it color-level. Clicking a color in the faces table flashes all its icons; clicking an icon flashes its color row. Icons get no individual editable identity — their name is already on screen, so no flash is needed to identify one. The size dropdown rides cleanly on top: only the selected pane is ever rendered, so a flash always targets the visible size. The alternative (icons as first-class selectable entities) was rejected — there's nothing per-icon to edit, so it would invent a selection concept that fights the rest of the studio. ** vNext implementation phases (as shipped) *** Phase G1 — Catalog capture (library + grid data) =build-nerd-icons-legend.el= refactored to a library with =cj/nerd-icons-write-legend= (runtime nerd-icons require). Emits the deduped, hue-ordered =gallery= groups (=face= / =hue= / =glyphs:[{glyph,name}]=) into =nerd-icons-legend.json=. =generate.py= parses it (=load_nerd_icons_gallery= with absent/malformed/legacy-array/invalid-group fallbacks) and attaches it via =add_nerd_icons_app=. Unit-tested by =test-nerd-icons-legend-dump.el= (synthetic alists/faces: dedupe, hue order, lightness tiebreak, name sort, the skip rules). *** Phase G2 — Grid renderer =renderNerdIconsPreview(sizePt)= draws the per-color grid (swatch + name header, a cell per icon with the name beneath), replacing the legend render. Live recolor; =genericPreview= fallback when no gallery. Browser-gated (=#nerdiconstest=: grid, hue order, dedupe, recolor). *** Phase G3 — Preview-pane dropdown =previewPanes= / =buildPkgPreview= + the template's "preview:" =