aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-selector/build-theme.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-08 08:56:47 -0500
committerCraig Jennings <c@cjennings.net>2026-06-08 08:57:05 -0500
commit7f57c6467054f6762a40b683d6585ee0a2b5527c (patch)
treed787bae9ba1c958b00b0f31cd868f252b4ddcaeb /scripts/theme-selector/build-theme.el
parent3e8d5651a3fddcf4afccd46a382ab12d915bbd8c (diff)
downloaddotemacs-7f57c6467054f6762a40b683d6585ee0a2b5527c.tar.gz
dotemacs-7f57c6467054f6762a40b683d6585ee0a2b5527c.zip
refactor(theme-studio): rename theme-selector to theme-studio
The tool authors themes from scratch -- palette, faces across every tier, live preview, export to a loadable deftheme. It never selects among existing themes, so "selector" mis-described it. Renamed the directory, the generated HTML and its title, the design spec, and every reference in the code, README, tests, and todo. No behavior change.
Diffstat (limited to 'scripts/theme-selector/build-theme.el')
-rw-r--r--scripts/theme-selector/build-theme.el244
1 files changed, 0 insertions, 244 deletions
diff --git a/scripts/theme-selector/build-theme.el b/scripts/theme-selector/build-theme.el
deleted file mode 100644
index fe080c0d..00000000
--- a/scripts/theme-selector/build-theme.el
+++ /dev/null
@@ -1,244 +0,0 @@
-;;; build-theme.el --- Convert a theme-selector theme.json into a deftheme -*- lexical-binding: t -*-
-
-;; Author: Craig Jennings <c@cjennings.net>
-
-;;; Commentary:
-
-;; The last link in the theme-selector pipeline: turn a theme.json exported by
-;; the tool (see scripts/theme-selector/README.md and
-;; docs/design/theme-selector-package-faces-spec.org) into a single,
-;; self-contained, loadable Emacs deftheme written to themes/<name>-theme.el.
-;;
-;; Four tiers come out of the JSON:
-;; - default -- background from assignments.bg, foreground from .p
-;; - syntax -- assignments.<cat> -> font-lock / tree-sitter faces, with
-;; the bold / italic category sets applied
-;; - ui -- the ui keys are already real face names; fg/bg passthrough
-;; - packages -- per-package face specs with :inherit / :height / weight /
-;; slant
-;;
-;; Usage (from a shell or a running Emacs):
-;;
-;; emacsclient -e '(progn (load ".../build-theme.el")
-;; (build-theme/convert-file ".../dupre-revised.json"))'
-;;
-;; or in batch:
-;;
-;; emacs --batch -l build-theme.el \
-;; --eval '(build-theme/convert-file "dupre-revised.json" "themes")'
-;;
-;; The output is a flat generated deftheme, not the hand-authored
-;; palette/faces/theme trio that the original dupre theme ships -- a theme.json
-;; carries resolved per-face hex, not dupre's semantic-mapping layer, so a flat
-;; deftheme is the faithful output and never clobbers the curated dupre files.
-
-;;; Code:
-
-(require 'json)
-(require 'subr-x)
-
-(defconst build-theme/--syntax-face-map
- '((kw . (font-lock-keyword-face))
- (bi . (font-lock-builtin-face))
- (pp . (font-lock-preprocessor-face))
- (fnd . (font-lock-function-name-face))
- (fnc . (font-lock-function-call-face))
- (ty . (font-lock-type-face))
- (prop . (font-lock-property-name-face font-lock-property-use-face))
- (con . (font-lock-constant-face))
- (num . (font-lock-number-face))
- (str . (font-lock-string-face))
- (esc . (font-lock-escape-face))
- (re . (font-lock-regexp-face))
- (doc . (font-lock-doc-face))
- (cm . (font-lock-comment-face))
- (cmd . (font-lock-comment-delimiter-face))
- (var . (font-lock-variable-name-face font-lock-variable-use-face))
- (op . (font-lock-operator-face))
- (punc . (font-lock-punctuation-face font-lock-bracket-face
- font-lock-delimiter-face font-lock-misc-punctuation-face)))
- "Map each theme.json syntax-category key to the font-lock faces it colors.
-A category may fan out to several faces (e.g. punc covers bracket and
-delimiter too). The dec (decorator) key is deliberately absent: Emacs has
-no dedicated decorator face -- it renders decorators with
-`font-lock-type-face', which the ty key already owns -- so coloring dec
-independently is not possible without clobbering types.")
-
-;;; ---------------------------------------------------------------------------
-;;; Pure helpers
-
-(defun build-theme/--hex-p (s)
- "Non-nil when S is a \"#rrggbb\" hex color string."
- (and (stringp s) (string-match-p "\\`#[0-9a-fA-F]\\{6\\}\\'" s)))
-
-(defun build-theme/--attrs (inherit fg bg bold italic underline strike height)
- "Build a face-attribute plist from the given fields, in canonical order.
-INHERIT is a face symbol or nil. FG and BG are hex strings or nil. BOLD,
-ITALIC, UNDERLINE, and STRIKE are booleans. HEIGHT is a float multiplier; 1.0
-(or nil) is omitted as the default. Only set attributes are written, so a
-fully-nil face yields an empty plist."
- (let (plist)
- (when (and height (numberp height) (/= height 1.0))
- (setq plist (list :height height)))
- (when strike (setq plist (append (list :strike-through t) plist)))
- (when underline (setq plist (append (list :underline t) plist)))
- (when italic (setq plist (append (list :slant 'italic) plist)))
- (when bold (setq plist (append (list :weight 'bold) plist)))
- (when bg (setq plist (append (list :background bg) plist)))
- (when fg (setq plist (append (list :foreground fg) plist)))
- (when inherit (setq plist (append (list :inherit inherit) plist)))
- plist))
-
-(defun build-theme/--face-spec (face attrs)
- "Wrap FACE and its ATTRS plist as a `custom-theme-set-faces' spec.
-Return nil when ATTRS is empty, so cleared faces emit nothing."
- (when attrs
- (list face (list (list t attrs)))))
-
-(defun build-theme/--obj-get (obj key)
- "Value of KEY in alist OBJ, or nil."
- (cdr (assq key obj)))
-
-(defun build-theme/--inherit-symbol (value)
- "Coerce an inherit VALUE (a face-name string, symbol, or nil) to a symbol."
- (cond ((null value) nil)
- ((symbolp value) value)
- ((stringp value) (intern value))
- (t nil)))
-
-;;; ---------------------------------------------------------------------------
-;;; Tiers
-
-(defun build-theme/--default-spec (assignments)
- "Build the `default' face spec from ASSIGNMENTS bg / p."
- (let ((bg (build-theme/--obj-get assignments 'bg))
- (fg (build-theme/--obj-get assignments 'p)))
- (build-theme/--face-spec 'default (build-theme/--attrs nil fg bg nil nil nil nil nil))))
-
-(defun build-theme/--syntax-face-specs (assignments bold italic)
- "Build syntax-tier face specs from ASSIGNMENTS plus the BOLD and ITALIC sets.
-BOLD and ITALIC are lists of category-key symbols. Each category fans out to
-the font-lock faces in `build-theme/--syntax-face-map'."
- (let (specs)
- (dolist (pair build-theme/--syntax-face-map)
- (let* ((cat (car pair))
- (faces (cdr pair))
- (hex (build-theme/--obj-get assignments cat)))
- (when hex
- (let ((attrs (build-theme/--attrs nil hex nil
- (memq cat bold) (memq cat italic) nil nil nil)))
- (dolist (face faces)
- (when-let ((spec (build-theme/--face-spec face attrs)))
- (push spec specs)))))))
- (nreverse specs)))
-
-(defun build-theme/--ui-face-specs (ui)
- "Build UI-tier face specs from the UI alist (face -> {fg,bg,bold,italic})."
- (let (specs)
- (dolist (entry ui)
- (let* ((face (car entry))
- (obj (cdr entry))
- (attrs (build-theme/--attrs nil
- (build-theme/--obj-get obj 'fg)
- (build-theme/--obj-get obj 'bg)
- (build-theme/--obj-get obj 'bold)
- (build-theme/--obj-get obj 'italic)
- (build-theme/--obj-get obj 'underline)
- (build-theme/--obj-get obj 'strike)
- nil)))
- (when-let ((spec (build-theme/--face-spec face attrs)))
- (push spec specs))))
- (nreverse specs)))
-
-(defun build-theme/--package-face-specs (packages)
- "Build package-tier face specs from the PACKAGES alist (app -> face -> spec)."
- (let (specs)
- (dolist (app packages)
- (dolist (entry (cdr app))
- (let* ((face (car entry))
- (obj (cdr entry))
- (attrs (build-theme/--attrs
- (build-theme/--inherit-symbol (build-theme/--obj-get obj 'inherit))
- (build-theme/--obj-get obj 'fg)
- (build-theme/--obj-get obj 'bg)
- (build-theme/--obj-get obj 'bold)
- (build-theme/--obj-get obj 'italic)
- (build-theme/--obj-get obj 'underline)
- (build-theme/--obj-get obj 'strike)
- (build-theme/--obj-get obj 'height))))
- (when-let ((spec (build-theme/--face-spec face attrs)))
- (push spec specs)))))
- (nreverse specs)))
-
-(defun build-theme/--all-specs (data)
- "Build the full ordered face-spec list from parsed theme.json DATA."
- (let ((assignments (build-theme/--obj-get data 'assignments))
- (bold (mapcar #'intern (build-theme/--obj-get data 'bold)))
- (italic (mapcar #'intern (build-theme/--obj-get data 'italic)))
- (ui (build-theme/--obj-get data 'ui))
- (packages (build-theme/--obj-get data 'packages)))
- (delq nil
- (append
- (list (build-theme/--default-spec assignments))
- (build-theme/--syntax-face-specs assignments bold italic)
- (build-theme/--ui-face-specs ui)
- (build-theme/--package-face-specs packages)))))
-
-;;; ---------------------------------------------------------------------------
-;;; Rendering
-
-(defun build-theme/--render (name specs)
- "Render a deftheme file body for theme NAME from face SPECS, as a string."
- (concat
- (format ";;; %s-theme.el --- Generated by theme-selector -*- lexical-binding: t -*-\n" name)
- "\n;;; Commentary:\n"
- (format ";; Generated from %s.json by scripts/theme-selector/build-theme.el.\n" name)
- ";; Do not hand-edit; re-run the converter.\n"
- "\n;;; Code:\n\n"
- (format "(deftheme %s\n \"Generated by theme-selector.\")\n\n" name)
- (format "(custom-theme-set-faces\n '%s\n" name)
- ;; Each spec is quoted: custom-theme-set-faces is a function, so an
- ;; unquoted (face ((t ...))) would be evaluated as a call. Specs hold
- ;; only literal strings, symbols, and numbers, so a plain quote suffices.
- (mapconcat (lambda (spec) (concat " '" (prin1-to-string spec))) specs "\n")
- ")\n\n"
- (format "(provide-theme '%s)\n" name)
- (format ";;; %s-theme.el ends here\n" name)))
-
-(defun build-theme/--parse (json-file)
- "Parse JSON-FILE into an alist, with null/false as nil and arrays as lists.
-Signal a `file-missing' error when JSON-FILE does not exist."
- (unless (file-readable-p json-file)
- (signal 'file-missing (list "Cannot read theme.json" json-file)))
- (with-temp-buffer
- (insert-file-contents json-file)
- (goto-char (point-min))
- (json-parse-buffer :object-type 'alist :array-type 'list
- :null-object nil :false-object nil)))
-
-;;; ---------------------------------------------------------------------------
-;;; Entry point
-
-(defun build-theme/convert-file (json-file &optional out-dir)
- "Convert JSON-FILE (a theme.json export) into a deftheme file.
-Write themes/<name>-theme.el, where <name> is the JSON name field, into
-OUT-DIR (default: the themes/ directory of this repo). Return the written
-path."
- (let* ((data (build-theme/--parse json-file))
- (name (build-theme/--obj-get data 'name))
- (specs (build-theme/--all-specs data))
- (dir (or out-dir
- (expand-file-name
- "../../themes"
- (file-name-directory (or load-file-name buffer-file-name
- default-directory)))))
- (out (expand-file-name (format "%s-theme.el" name) dir)))
- (unless (and (stringp name) (string-match-p "\\`[a-zA-Z][a-zA-Z0-9-]*\\'" name))
- (error "Invalid theme name in %s: %S" json-file name))
- (make-directory dir t)
- (with-temp-file out
- (insert (build-theme/--render name specs)))
- out))
-
-(provide 'build-theme)
-;;; build-theme.el ends here