aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-24 05:39:06 -0400
committerCraig Jennings <c@cjennings.net>2026-06-24 05:39:06 -0400
commita98dc772708f33e90aaeb4572a13bf22d065a022 (patch)
treee8c3682b2c82b5840999b3ad49a2ec947c9a38dd /scripts/theme-studio
parent11767454d306518997d06207f9fe792f7482ac50 (diff)
downloaddotemacs-a98dc772708f33e90aaeb4572a13bf22d065a022.tar.gz
dotemacs-a98dc772708f33e90aaeb4572a13bf22d065a022.zip
feat(theme-studio): capture the nerd-icons filetype legend (phase 1)
Add build-nerd-icons-legend.el, which resolves the curated v1 legend rows (glyph + owner color face per filetype) from the live nerd-icons alists and dumps them to nerd-icons-legend.json, a committed artifact like package-inventory.json. generate.py gains load_nerd_icons_legend, which validates the artifact and returns None — with a warning — when it is absent, malformed, empty, or missing a field, so the page can fall back to the generic nerd-icons app rather than error. Data only; the bespoke preview that renders it lands next. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
Diffstat (limited to 'scripts/theme-studio')
-rw-r--r--scripts/theme-studio/build-nerd-icons-legend.el96
-rw-r--r--scripts/theme-studio/generate.py32
-rw-r--r--scripts/theme-studio/nerd-icons-legend.json93
-rw-r--r--scripts/theme-studio/test_generate.py45
4 files changed, 266 insertions, 0 deletions
diff --git a/scripts/theme-studio/build-nerd-icons-legend.el b/scripts/theme-studio/build-nerd-icons-legend.el
new file mode 100644
index 000000000..6381294f5
--- /dev/null
+++ b/scripts/theme-studio/build-nerd-icons-legend.el
@@ -0,0 +1,96 @@
+;;; build-nerd-icons-legend.el --- emit nerd-icons filetype legend for theme-studio -*- lexical-binding: t -*-
+;;; Commentary:
+;; Loaded into a running Emacs (emacsclient -e '(load ".../build-nerd-icons-legend.el")')
+;; to write nerd-icons-legend.json next to itself: the curated v1 filetype legend
+;; for theme-studio's bespoke nerd-icons preview. Each row resolves its glyph and
+;; owner color face from the live nerd-icons alists at capture time, so the legend
+;; tracks the installed nerd-icons version. A curated key absent from the alist
+;; is skipped and logged. generate.py embeds the JSON; see
+;; docs/specs/theme-studio-nerd-icons-colors-spec.org.
+;;; Code:
+
+(require 'json)
+(require 'nerd-icons)
+
+;; Curated v1 rows: (KEY LABEL CATEGORY LOOKUP). CATEGORY selects the source
+;; alist and its face shape; LOOKUP is the alist key (nil for the dir row, which
+;; has a fixed owner face per the spec's dir-precedence decision).
+(defconst cj/--nerd-icons-legend-spec
+ '(("ext:el" "init.el" extension "el")
+ ("ext:py" "app.py" extension "py")
+ ("ext:org" "notes.org" extension "org")
+ ("ext:md" "README.md" extension "md")
+ ("ext:ts" "main.ts" extension "ts")
+ ("ext:html" "index.html" extension "html")
+ ("ext:rs" "lib.rs" extension "rs")
+ ("ext:js" "app.js" extension "js")
+ ("ext:yml" "ci.yml" extension "yml")
+ ("ext:c" "main.c" extension "c")
+ ("dir" "src/" dir nil)
+ ("cmd" "M-x command" command command)
+ ("buf" "*scratch*" buffer emacs-lisp-mode))
+ "The v1 legend rows: (KEY LABEL CATEGORY LOOKUP), spanning a representative
+set of the nerd-icons color faces rather than all 34.")
+
+(defun cj/--nerd-icons-legend-glyph (fn name)
+ "Return the bare glyph string for icon NAME drawn by FN, or nil."
+ (when (and (fboundp fn) (stringp name))
+ (let ((s (ignore-errors (funcall fn name))))
+ (and (stringp s)
+ (> (length (string-trim s)) 0)
+ (string-trim (substring-no-properties s))))))
+
+(defun cj/--nerd-icons-legend-make (key label category glyph face)
+ "Build the JSON alist for one legend row, or nil (logged) when GLYPH/FACE missing."
+ (if (and glyph face)
+ (list (cons "key" key)
+ (cons "label" label)
+ (cons "face" (symbol-name face))
+ (cons "category" (symbol-name category))
+ (cons "glyph" glyph))
+ (message "nerd-icons-legend: skipping %s (glyph=%S face=%S)" key glyph face)
+ nil))
+
+(defun cj/--nerd-icons-legend-row (key label category lookup)
+ "Resolve one curated row from the live nerd-icons alists, or nil if absent."
+ (pcase category
+ ('extension
+ (let ((e (assoc lookup nerd-icons-extension-icon-alist)))
+ (when e
+ (cj/--nerd-icons-legend-make
+ key label category
+ (cj/--nerd-icons-legend-glyph (nth 1 e) (nth 2 e))
+ (plist-get (nthcdr 3 e) :face)))))
+ ('buffer
+ (let ((e (assq lookup nerd-icons-mode-icon-alist)))
+ (when e
+ (cj/--nerd-icons-legend-make
+ key label category
+ (cj/--nerd-icons-legend-glyph (nth 1 e) (nth 2 e))
+ (plist-get (nthcdr 3 e) :face)))))
+ ('command
+ (let ((e (assq lookup nerd-icons-completion-category-icons)))
+ (when e
+ (cj/--nerd-icons-legend-make
+ key label category
+ (cj/--nerd-icons-legend-glyph (nth 1 e) (nth 2 e))
+ (nth 3 e)))))
+ ('dir
+ (cj/--nerd-icons-legend-make
+ key label category
+ (let ((s (ignore-errors (nerd-icons-icon-for-dir "src"))))
+ (and (stringp s) (string-trim (substring-no-properties s))))
+ 'nerd-icons-yellow))))
+
+(let ((rows (delq nil (mapcar (lambda (r) (apply #'cj/--nerd-icons-legend-row r))
+ cj/--nerd-icons-legend-spec))))
+ (with-temp-file (expand-file-name
+ "nerd-icons-legend.json"
+ (file-name-directory (or load-file-name buffer-file-name
+ "~/.emacs.d/scripts/theme-studio/")))
+ (let ((json-encoding-pretty-print t))
+ (insert (json-encode (apply #'vector rows)) "\n")))
+ (message "nerd-icons-legend: wrote %d rows" (length rows)))
+
+(provide 'build-nerd-icons-legend)
+;;; build-nerd-icons-legend.el ends here
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py
index 6baa67a91..4f390a850 100644
--- a/scripts/theme-studio/generate.py
+++ b/scripts/theme-studio/generate.py
@@ -12,6 +12,38 @@ def read_text(name):
def read_json(name):
return json.loads(read_text(name))
+NERD_ICONS_LEGEND_FIELDS = ("key", "label", "face", "category", "glyph")
+
+def load_nerd_icons_legend(path=None):
+ """Return the nerd-icons legend rows, or None when the artifact is unusable.
+
+ The legend is captured by build-nerd-icons-legend.el into nerd-icons-legend.json.
+ Absent, malformed, empty, or carrying a row without all five string fields
+ (key/label/face/category/glyph) -> None, with a warning, so the caller falls
+ back to the generic nerd-icons app instead of erroring. nerd-icons not being
+ installed at capture time yields an empty/absent file, which lands here as None.
+ """
+ path = path or os.path.join(HERE, "nerd-icons-legend.json")
+ if not os.path.exists(path):
+ print(f"WARNING: nerd-icons legend absent ({path}); generic nerd-icons app")
+ return None
+ try:
+ with open(path) as src:
+ rows = json.load(src)
+ except (json.JSONDecodeError, OSError) as exc:
+ print(f"WARNING: nerd-icons legend malformed ({path}: {exc}); generic nerd-icons app")
+ return None
+ if not isinstance(rows, list) or not rows:
+ print(f"WARNING: nerd-icons legend empty ({path}); generic nerd-icons app")
+ return None
+ for row in rows:
+ if not (isinstance(row, dict)
+ and all(isinstance(row.get(f), str) and row.get(f)
+ for f in NERD_ICONS_LEGEND_FIELDS)):
+ print(f"WARNING: nerd-icons legend row invalid ({row!r}); generic nerd-icons app")
+ return None
+ return rows
+
def strip_exports(src):
"""Drop ES-module `export`/`import` lines so the body loads as a classic <script>.
diff --git a/scripts/theme-studio/nerd-icons-legend.json b/scripts/theme-studio/nerd-icons-legend.json
new file mode 100644
index 000000000..1b315e98b
--- /dev/null
+++ b/scripts/theme-studio/nerd-icons-legend.json
@@ -0,0 +1,93 @@
+[
+ {
+ "key": "ext:el",
+ "label": "init.el",
+ "face": "nerd-icons-purple",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:py",
+ "label": "app.py",
+ "face": "nerd-icons-dblue",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:org",
+ "label": "notes.org",
+ "face": "nerd-icons-lgreen",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:md",
+ "label": "README.md",
+ "face": "nerd-icons-lblue",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:ts",
+ "label": "main.ts",
+ "face": "nerd-icons-blue-alt",
+ "category": "extension",
+ "glyph": "󰛦"
+ },
+ {
+ "key": "ext:html",
+ "label": "index.html",
+ "face": "nerd-icons-orange",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:rs",
+ "label": "lib.rs",
+ "face": "nerd-icons-maroon",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:js",
+ "label": "app.js",
+ "face": "nerd-icons-yellow",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:yml",
+ "label": "ci.yml",
+ "face": "nerd-icons-dyellow",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "ext:c",
+ "label": "main.c",
+ "face": "nerd-icons-blue",
+ "category": "extension",
+ "glyph": ""
+ },
+ {
+ "key": "dir",
+ "label": "src/",
+ "face": "nerd-icons-yellow",
+ "category": "dir",
+ "glyph": ""
+ },
+ {
+ "key": "cmd",
+ "label": "M-x command",
+ "face": "nerd-icons-blue",
+ "category": "command",
+ "glyph": ""
+ },
+ {
+ "key": "buf",
+ "label": "*scratch*",
+ "face": "nerd-icons-purple",
+ "category": "buffer",
+ "glyph": ""
+ }
+]
diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py
index 974fca68a..7ff207e43 100644
--- a/scripts/theme-studio/test_generate.py
+++ b/scripts/theme-studio/test_generate.py
@@ -597,5 +597,50 @@ class GeneratedDefaults(unittest.TestCase):
self.assertEqual(generate.SYNTAX["str"]["slant"], "italic")
+class NerdIconsLegend(unittest.TestCase):
+ """The committed nerd-icons-legend.json artifact and the loader fallback."""
+
+ def _write(self, content):
+ path = os.path.join(tempfile.mkdtemp(), "nerd-icons-legend.json")
+ with open(path, "w") as out:
+ out.write(content)
+ return path
+
+ def test_committed_artifact_has_valid_rows(self):
+ rows = generate.load_nerd_icons_legend()
+ self.assertIsNotNone(rows, "committed nerd-icons-legend.json should load")
+ self.assertTrue(rows)
+ for row in rows:
+ for field in generate.NERD_ICONS_LEGEND_FIELDS:
+ self.assertIsInstance(row.get(field), str)
+ self.assertTrue(row[field])
+ self.assertTrue(row["face"].startswith("nerd-icons-"))
+ self.assertIn(row["category"], ("extension", "dir", "command", "buffer"))
+
+ def test_absent_artifact_falls_back_to_none(self):
+ with redirect_stdout(io.StringIO()) as out:
+ self.assertIsNone(generate.load_nerd_icons_legend("/no/such/legend.json"))
+ self.assertIn("absent", out.getvalue())
+
+ def test_malformed_artifact_falls_back_to_none(self):
+ path = self._write("{not json")
+ with redirect_stdout(io.StringIO()) as out:
+ self.assertIsNone(generate.load_nerd_icons_legend(path))
+ self.assertIn("malformed", out.getvalue())
+
+ def test_empty_artifact_falls_back_to_none(self):
+ path = self._write("[]")
+ with redirect_stdout(io.StringIO()) as out:
+ self.assertIsNone(generate.load_nerd_icons_legend(path))
+ self.assertIn("empty", out.getvalue())
+
+ def test_row_missing_a_field_falls_back_to_none(self):
+ path = self._write(json.dumps([{"key": "ext:el", "label": "init.el",
+ "face": "nerd-icons-purple", "category": "extension"}]))
+ with redirect_stdout(io.StringIO()) as out:
+ self.assertIsNone(generate.load_nerd_icons_legend(path))
+ self.assertIn("invalid", out.getvalue())
+
+
if __name__ == "__main__":
unittest.main()