diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-24 05:39:06 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-24 05:39:06 -0400 |
| commit | a98dc772708f33e90aaeb4572a13bf22d065a022 (patch) | |
| tree | e8c3682b2c82b5840999b3ad49a2ec947c9a38dd /scripts | |
| parent | 11767454d306518997d06207f9fe792f7482ac50 (diff) | |
| download | dotemacs-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')
| -rw-r--r-- | scripts/theme-studio/build-nerd-icons-legend.el | 96 | ||||
| -rw-r--r-- | scripts/theme-studio/generate.py | 32 | ||||
| -rw-r--r-- | scripts/theme-studio/nerd-icons-legend.json | 93 | ||||
| -rw-r--r-- | scripts/theme-studio/test_generate.py | 45 |
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() |
