;;; cus-theme.el --- custom theme creation user interface -*- lexical-binding: t -*- ;; Copyright (C) 2001-2023 Free Software Foundation, Inc. ;; Author: Alex Schroeder ;; Maintainer: emacs-devel@gnu.org ;; Keywords: help, faces ;; Package: emacs ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;;; Code: (require 'widget) (require 'cus-edit) (eval-when-compile (require 'wid-edit)) (defvar-keymap custom-new-theme-mode-map :doc "Keymap for `custom-new-theme-mode'." :full t :suppress t :parent (make-composed-keymap widget-keymap special-mode-map) "C-x C-s" #'custom-theme-write "q" #'Custom-buffer-done "n" #'widget-forward "p" #'widget-backward) (define-derived-mode custom-new-theme-mode nil "Custom-Theme" "Major mode for editing Custom themes. Do not call this mode function yourself. It is meant for internal use." (use-local-map custom-new-theme-mode-map) (custom--initialize-widget-variables) (setq-local revert-buffer-function #'custom-theme-revert)) (put 'custom-new-theme-mode 'mode-class 'special) (defvar custom-theme-name nil) ;; Each element has the form (VAR CHECKBOX-WIDGET VAR-WIDGET) (defvar custom-theme-variables nil) ;; Each element has the form (FACE CHECKBOX-WIDGET FACE-WIDGET) (defvar custom-theme-faces nil) (defvar custom-theme-description nil) (defvar custom-theme--migrate-settings nil) (defvar custom-theme-insert-variable-marker nil) (defvar custom-theme-insert-face-marker nil) (defvar custom-theme--listed-faces '(default cursor fixed-pitch variable-pitch escape-glyph homoglyph minibuffer-prompt highlight region shadow secondary-selection trailing-whitespace font-lock-bracket-face font-lock-builtin-face font-lock-comment-delimiter-face font-lock-comment-face font-lock-constant-face font-lock-delimiter-face font-lock-doc-face font-lock-doc-markup-face font-lock-escape-face font-lock-function-call-face font-lock-function-name-face font-lock-keyword-face font-lock-negation-char-face font-lock-number-face font-lock-misc-punctuation-face font-lock-operator-face font-lock-preprocessor-face font-lock-property-name-face font-lock-property-use-face font-lock-punctuation-face font-lock-regexp-grouping-backslash font-lock-regexp-grouping-construct font-lock-string-face font-lock-type-face font-lock-variable-name-face font-lock-variable-use-face font-lock-warning-face button link link-visited fringe header-line tooltip mode-line mode-line-buffer-id mode-line-emphasis mode-line-highlight mode-line-inactive isearch isearch-fail lazy-highlight match next-error query-replace) "Faces listed by default in the *Custom Theme* buffer.") (defvar custom-theme--save-name) ;;;###autoload (defun customize-create-theme (&optional theme buffer) "Create or edit a custom theme. THEME, if non-nil, should be an existing theme to edit. If THEME is `user', the resulting *Custom Theme* buffer also contains a checkbox for removing the theme settings specified in the buffer from the Custom save file. BUFFER, if non-nil, should be a buffer to use; the default is named *Custom Theme*." (interactive) (switch-to-buffer (get-buffer-create (or buffer "*Custom Theme*"))) (let ((inhibit-read-only t)) (erase-buffer) (delete-all-overlays)) (custom-new-theme-mode) (make-local-variable 'custom-theme-name) (setq-local custom-theme--save-name theme) (setq-local custom-theme-faces nil) (setq-local custom-theme-variables nil) (setq-local custom-theme-description "") (setq-local custom-theme--migrate-settings nil) (make-local-variable 'custom-theme-insert-face-marker) (make-local-variable 'custom-theme-insert-variable-marker) (make-local-variable 'custom-theme--listed-faces) (when (called-interactively-p 'interactive) (unless (y-or-n-p "Include basic face customizations in this theme? ") (setq custom-theme--listed-faces nil))) (let (vars values faces face-specs) ;; Load the theme settings. (when theme (if (eq theme 'user) (widget-insert "This buffer contains all the Custom settings you have made. You can convert them into a new custom theme, and optionally remove them from your saved Custom file.\n\n") (load-theme theme nil t)) (dolist (setting (get theme 'theme-settings)) (if (eq (car setting) 'theme-value) (progn (push (nth 1 setting) vars) (push (nth 3 setting) values)) (push (nth 1 setting) faces) (push (nth 3 setting) face-specs)))) (widget-create 'push-button :tag " Visit Theme " :help-echo "Insert the settings of a pre-defined theme." :action (lambda (_widget &optional _event) (call-interactively #'custom-theme-visit-theme))) (widget-insert " ") (widget-create 'push-button :tag " Merge Theme " :help-echo "Merge in the settings of a pre-defined theme." :action (lambda (_widget &optional _event) (call-interactively #'custom-theme-merge-theme))) (widget-insert " ") (widget-create 'push-button :tag " Revert " :help-echo "Revert this buffer to its original state." :action (lambda (&rest _ignored) (revert-buffer))) (widget-insert "\n\nTheme name : ") (setq custom-theme-name (widget-create 'editable-field :value (if (and theme (not (eq theme 'user))) (symbol-name theme) ""))) (widget-insert "Description: ") (setq custom-theme-description (widget-create 'text :format "%v" :value (or (get theme 'theme-documentation) (format-time-string "Created %Y-%m-%d.")))) (widget-create 'push-button :notify #'custom-theme-write " Save Theme ") (when (eq theme 'user) (setq custom-theme--migrate-settings t) (widget-insert " ") (widget-create 'checkbox :value custom-theme--migrate-settings :action (lambda (widget &optional event) (when (widget-value widget) (widget-toggle-action widget event) (setq custom-theme--migrate-settings (widget-value widget))))) (widget-insert (propertize " Remove saved theme settings from Custom save file." 'face '(variable-pitch (:height 0.9))))) ;; If THEME is non-nil, insert all of that theme's faces. ;; Otherwise, insert those in `custom-theme--listed-faces'. (widget-insert "\n\n Theme faces:\n ") (if theme (while faces (custom-theme-add-face-1 (pop faces) (pop face-specs))) (dolist (face custom-theme--listed-faces) (custom-theme-add-face-1 face nil))) (setq custom-theme-insert-face-marker (point-marker)) (widget-insert " ") (widget-create 'push-button :tag "Insert Additional Face" :help-echo "Add another face to this theme." :follow-link 'mouse-face :button-face 'custom-link :mouse-face 'highlight :pressed-face 'highlight :action (lambda (_widget &optional _event) (call-interactively #'custom-theme-add-face))) ;; If THEME is non-nil, insert all of that theme's variables. (widget-insert "\n\n Theme variables:\n ") (if theme (while vars (if (eq (car vars) 'custom-enabled-themes) (progn (pop vars) (pop values)) (custom-theme-add-var-1 (pop vars) (eval (pop values)))))) (setq custom-theme-insert-variable-marker (point-marker)) (widget-insert " ") (widget-create 'push-button :tag "Insert Variable" :help-echo "Add another variable to this theme." :follow-link 'mouse-face :button-face 'custom-link :mouse-face 'highlight :pressed-face 'highlight :action (lambda (_widget &optional _event) (call-interactively #'custom-theme-add-variable))) (widget-insert ?\n) (widget-setup) (goto-char (point-min)) (message ""))) (defun custom-theme-revert (_ignore-auto noconfirm) "Revert the current *Custom Theme* buffer. This is the `revert-buffer-function' for `custom-new-theme-mode'." (when (or noconfirm (y-or-n-p "Discard current changes? ")) (customize-create-theme custom-theme--save-name (current-buffer)))) ;;; Theme variables (defun custom-theme-add-variable (var value) "Add a widget for VAR (a symbol) to the *New Custom Theme* buffer. VALUE should be a value to which to set the widget; when called interactively, this defaults to the current value of VAR." (interactive (let ((v (read-variable "Variable name: "))) (list v (symbol-value v)))) (let ((entry (assq var custom-theme-variables))) (cond ((null entry) ;; If VAR is not yet in the buffer, add it. (save-excursion (goto-char custom-theme-insert-variable-marker) (custom-theme-add-var-1 var value) (move-marker custom-theme-insert-variable-marker (point)) (widget-setup))) ;; Otherwise, alter that var widget. (t (widget-value-set (nth 1 entry) t) (let ((widget (nth 2 entry))) (widget-put widget :shown-value (list value)) (custom-redraw widget)))))) (defun custom-theme-add-var-1 (symbol val) (widget-insert " ") (push (list symbol (prog1 (widget-create 'checkbox :value t :help-echo "Enable/disable this variable.") (widget-insert " ")) (widget-create 'custom-variable :tag (custom-unlispify-tag-name symbol) :value symbol :shown-value (list val) :notify #'ignore :custom-level 0 :custom-state 'hidden :custom-style 'simple)) custom-theme-variables) (widget-insert " ")) ;;; Theme faces (defun custom-theme-add-face (face &optional spec) "Add a widget for FACE (a symbol) to the *New Custom Theme* buffer. SPEC, if non-nil, should be a face spec to which to set the widget." (interactive (list (read-face-name "Face name" (face-at-point t)))) (unless (or (facep face) spec) (error "`%s' has no face definition" face)) (let ((entry (assq face custom-theme-faces))) (cond ((null entry) ;; If FACE is not yet in the buffer, add it. (save-excursion (goto-char custom-theme-insert-face-marker) (custom-theme-add-face-1 face spec) (move-marker custom-theme-insert-face-marker (point)) (widget-setup))) ;; Otherwise, if SPEC is supplied, alter that face widget. (spec (widget-value-set (nth 1 entry) t) (let ((widget (nth 2 entry))) (widget-put widget :shown-value spec) (custom-redraw widget))) ((called-interactively-p 'interactive) (error "`%s' is already present" face))))) (defun custom-theme-add-face-1 (symbol spec) (widget-insert " ") (push (list symbol (prog1 (widget-create 'checkbox :value t :help-echo "Enable/disable this face.") (widget-insert " ")) (widget-create 'custom-face :tag (custom-unlispify-tag-name symbol) :documentation-shown t :value symbol :custom-state 'hidden :custom-style 'simple :shown-value spec :sample-indent 34)) custom-theme-faces) (widget-insert " ")) ;;; Reading and writing ;;;###autoload (defun custom-theme-visit-theme (theme) "Set up a Custom buffer to edit custom theme THEME." (interactive (list (intern (completing-read "Find custom theme: " (mapcar #'symbol-name (custom-available-themes)))))) (unless (custom-theme-name-valid-p theme) (error "No valid theme named `%s'" theme)) (cond ((not (eq major-mode 'custom-new-theme-mode)) (customize-create-theme theme)) ((y-or-n-p "Discard current changes? ") (setq custom-theme--save-name theme) (custom-theme-revert nil t)))) (defun custom-theme-merge-theme (theme) "Merge the custom theme THEME's settings into the current buffer." (interactive (list (intern (completing-read "Merge custom theme: " (mapcar #'symbol-name (custom-available-themes)))))) (unless (eq theme 'user) (unless (custom-theme-name-valid-p theme) (error "Invalid theme name `%s'" theme)) (load-theme theme nil t)) (let ((settings (reverse (get theme 'theme-settings)))) (dolist (setting settings) (let ((option (eq (car setting) 'theme-value)) (name (nth 1 setting)) (value (nth 3 setting))) (unless (and option (memq name '(custom-enabled-themes custom-safe-themes))) (funcall (if option #'custom-theme-add-variable #'custom-theme-add-face) name value))))) theme) ;; From cus-edit.el (defvar custom-reset-standard-faces-list) (defvar custom-reset-standard-variables-list) (defun custom-theme-write (&rest _ignore) "Write the current custom theme to its theme file." (interactive) (let* ((name (widget-value custom-theme-name)) (doc (widget-value custom-theme-description)) (vars custom-theme-variables) (faces custom-theme-faces) filename) (when (string-equal name "") (setq name (read-from-minibuffer "Theme name: " (user-login-name))) (widget-value-set custom-theme-name name)) (unless (custom-theme-name-valid-p (intern name)) (error "Custom themes cannot be named `%s'" name)) (setq filename (expand-file-name (concat name "-theme.el") custom-theme-directory)) (and (file-exists-p filename) (not (y-or-n-p (format "File %s exists. Overwrite? " filename))) (error "Aborted")) (with-temp-buffer (emacs-lisp-mode) (unless (file-directory-p custom-theme-directory) (make-directory (file-name-as-directory custom-theme-directory) t)) (setq buffer-file-name filename) (erase-buffer) (insert "(deftheme " name) (if doc (insert "\n \"" doc "\"")) (insert ")\n") (custom-theme-write-variables name (reverse vars)) (custom-theme-write-faces name (reverse faces)) (insert "\n(provide-theme '" name ")\n") (save-buffer)) (message "Theme written to %s" filename) (when custom-theme--migrate-settings ;; Remove these settings from the Custom file. (let ((custom-reset-standard-variables-list '(t)) (custom-reset-standard-faces-list '(t))) (dolist (var vars) (when (and (not (eq (car var) 'custom-enabled-themes)) (widget-get (nth 1 var) :value)) (widget-apply (nth 2 var) :custom-mark-to-reset-standard))) (dolist (face faces) (when (widget-get (nth 1 face) :value) (widget-apply (nth 2 face) :custom-mark-to-reset-standard))) (custom-save-all)) (let ((custom-theme-load-path (list 'custom-theme-directory))) (load-theme (intern name)))))) (defun custom-theme-write-variables (theme vars) "Write a `custom-theme-set-variables' command for THEME. It includes all variables in list VARS." (when vars (let ((standard-output (current-buffer))) (princ "\n(custom-theme-set-variables\n") (princ " '") (princ theme) (princ "\n") (dolist (spec vars) (when (widget-get (nth 1 spec) :value) (let* ((symbol (nth 0 spec)) (widget (nth 2 spec)) (child (car-safe (widget-get widget :children))) (value (if child (widget-value child) ;; Child is null if the widget is closed (hidden). (car (widget-get widget :shown-value))))) (unless (bolp) (princ "\n")) (princ " '(") (prin1 symbol) (princ " ") (prin1 (custom-quote value)) (princ ")")))) (if (bolp) (princ " ")) (princ ")") (when (/= (following-char) ?\n) (princ "\n"))))) (defun custom-theme-write-faces (theme faces) "Write a `custom-theme-set-faces' command for THEME. It includes all faces in list FACES." (when faces (let ((standard-output (current-buffer))) (princ "\n(custom-theme-set-faces\n") (princ " '") (princ theme) (princ "\n") (dolist (spec faces) ;; Insert the face iff the checkbox widget is checked. (when (widget-get (nth 1 spec) :value) (let* ((symbol (nth 0 spec)) (widget (nth 2 spec)) (value (cond ((car-safe (widget-get widget :children)) (custom-face-widget-to-spec widget)) ;; Child is null if the widget is closed (hidden). ((widget-get widget :shown-value)) (t (custom-face-get-current-spec symbol))))) (when value (princ (if (bolp) " '(" "\n '(")) (prin1 symbol) (princ " ") (prin1 value) (princ ")"))))) (if (bolp) (princ " ")) (princ ")") (when (/= (following-char) ?\n) (princ "\n"))))) ;;; Describing Custom themes. ;;;###autoload (defun describe-theme (theme) "Display a description of the Custom theme THEME (a symbol)." (interactive (list (intern (completing-read "Describe custom theme: " (mapcar #'symbol-name (custom-available-themes)))))) (unless (custom-theme-name-valid-p theme) (error "Invalid theme name `%s'" theme)) (help-setup-xref (list 'describe-theme theme) (called-interactively-p 'interactive)) (with-help-window (help-buffer) (with-current-buffer standard-output (describe-theme-1 theme)))) (defun describe-theme-1 (theme) (prin1 theme) (princ " is a custom theme") (let ((fn (locate-file (concat (symbol-name theme) "-theme.el") (custom-theme--load-path) '("" "c"))) doc) (when fn (princ (substitute-command-keys " in `")) (help-insert-xref-button (file-name-nondirectory fn) 'help-theme-def fn) (princ (substitute-quotes "'"))) (princ ".\n") (if (custom-theme-p theme) (progn (if (custom-theme-enabled-p theme) (princ "It is loaded and enabled.") (princ "It is loaded but disabled.")) (setq doc (get theme 'theme-documentation))) (princ "It is not loaded.") ;; Attempt to grab the theme documentation (when fn (with-temp-buffer (insert-file-contents fn) (let ((sexp (let ((read-circle nil)) (condition-case nil (read (current-buffer)) (end-of-file nil))))) (and (eq (car-safe sexp) 'deftheme) (setq doc (nth 2 sexp))))))) (princ "\n\nDocumentation:\n") (princ (if (stringp doc) (substitute-command-keys doc) "No documentation available."))) (princ "\n\nYou can ") (help-insert-xref-button "customize" 'help-theme-edit theme) (princ " this theme.")) ;;; Theme chooser (defvar custom--listed-themes) (defcustom custom-theme-allow-multiple-selections nil "Whether to allow multi-selections in the *Custom Themes* buffer." :version "24.1" :type 'boolean :group 'custom-buffer) (defvar-keymap custom-theme-choose-mode-map :doc "Keymap for `custom-theme-choose-mode'." :full t :suppress t :parent (make-composed-keymap widget-keymap special-mode-map) "C-x C-s" #'custom-theme-save "n" #'widget-forward "p" #'widget-backward "?" #'custom-describe-theme) (define-derived-mode custom-theme-choose-mode special-mode "Themes" "Major mode for selecting Custom themes. Do not call this mode function yourself. It is meant for internal use." (use-local-map custom-theme-choose-mode-map) (custom--initialize-widget-variables) (setq-local revert-buffer-function (lambda (_ignore-auto noconfirm) (when (or noconfirm (y-or-n-p "Discard current choices? ")) (customize-themes (current-buffer)))))) (put 'custom-theme-choose-mode 'mode-class 'special) ;;;###autoload (defun customize-themes (&optional buffer) "Display a selectable list of Custom themes. When called from Lisp, BUFFER should be the buffer to use; if omitted, a buffer named *Custom Themes* is used." (interactive) (switch-to-buffer (get-buffer-create (or buffer "*Custom Themes*"))) (let ((inhibit-read-only t)) (erase-buffer)) (custom-theme-choose-mode) (setq-local custom--listed-themes nil) (make-local-variable 'custom-theme-allow-multiple-selections) (and (null custom-theme-allow-multiple-selections) (> (length custom-enabled-themes) 1) (setq custom-theme-allow-multiple-selections t)) (widget-insert (substitute-command-keys "Type RET or click to enable/disable listed custom themes. Type \\[custom-describe-theme] to describe the theme at point. Theme files are named *-theme.el in `")) (widget-create 'link :value "custom-theme-load-path" :button-face 'custom-link :mouse-face 'highlight :pressed-face 'highlight :help-echo "Describe `custom-theme-load-path'." :keymap custom-mode-link-map :follow-link 'mouse-face :action (lambda (_widget &rest _ignore) (describe-variable 'custom-theme-load-path))) (widget-insert (substitute-command-keys "'.\n\n")) ;; If the user has made customizations, display a warning and ;; provide buttons to disable or convert them. (let ((user-settings (get 'user 'theme-settings))) (unless (or (null user-settings) (and (null (cdr user-settings)) (eq (caar user-settings) 'theme-value) (eq (cadr (car user-settings)) 'custom-enabled-themes))) (widget-insert (propertize " Note: Your custom settings take precedence over theme settings. To migrate your settings into a theme, click " 'face 'font-lock-warning-face)) (widget-create 'link :value "here" :button-face 'custom-link :mouse-face 'highlight :pressed-face 'highlight :help-echo "Migrate." :keymap custom-mode-link-map :follow-link 'mouse-face :action (lambda (_widget &rest _ignore) (customize-create-theme 'user))) (widget-insert ".\n\n"))) (widget-create 'push-button :tag " Save Theme Settings " :help-echo "Save the selected themes for future sessions." :action #'custom-theme-save) (widget-insert ?\n) (widget-create 'checkbox :value custom-theme-allow-multiple-selections :action #'custom-theme-selections-toggle) (widget-insert (propertize " Select more than one theme at a time" 'face '(variable-pitch (:height 0.9)))) (widget-insert "\n\nAvailable Custom Themes:\n") (let ((help-echo "mouse-2: Enable this theme for this session") widget) (dolist (theme (custom-available-themes)) ;; Don't list obsolete themes. (unless (get theme 'byte-obsolete-info) (setq widget (widget-create 'checkbox :value (custom-theme-enabled-p theme) :theme-name theme :help-echo help-echo :action #'custom-theme-checkbox-toggle)) (push (cons theme widget) custom--listed-themes) (widget-create-child-and-convert widget 'push-button :button-face-get 'ignore :mouse-face-get 'ignore :value (format " %s" theme) :action #'widget-parent-action :help-echo help-echo) (widget-insert " -- " (propertize (custom-theme-summary theme) 'face 'shadow) ?\n)))) (goto-char (point-min)) (widget-setup)) (defun custom-theme-summary (theme) "Return the summary line of THEME." (let (doc) (if (custom-theme-p theme) (setq doc (get theme 'theme-documentation)) (let ((fn (locate-file (concat (symbol-name theme) "-theme.el") (custom-theme--load-path) '("" "c")))) (when fn (with-temp-buffer (insert-file-contents fn) (let ((sexp (let ((read-circle nil)) (condition-case nil (progn (re-search-forward "^(deftheme") (beginning-of-line) (read (current-buffer))) (error nil))))) (setq doc (nth 2 sexp))))))) (cond ((null doc) "(no documentation available)") ((string-match ".*" doc) (match-string 0 doc)) (t doc)))) (defun custom-theme-checkbox-toggle (widget &optional event) (let ((this-theme (widget-get widget :theme-name))) (if (widget-value widget) ;; Disable the theme. (progn (disable-theme this-theme) (widget-toggle-action widget event)) ;; Enable the theme. (unless custom-theme-allow-multiple-selections ;; If only one theme is allowed, disable all other themes and ;; uncheck their boxes. (dolist (theme custom-enabled-themes) (and (not (eq theme this-theme)) (assq theme custom--listed-themes) (disable-theme theme))) (dolist (theme custom--listed-themes) (unless (eq (car theme) this-theme) (widget-value-set (cdr theme) nil) (widget-apply (cdr theme) :notify (cdr theme) event)))) (when (load-theme this-theme) (widget-toggle-action widget event))) ;; Mark `custom-enabled-themes' as "set for current session". (put 'custom-enabled-themes 'customized-value (list (custom-quote custom-enabled-themes))))) (defun custom-describe-theme () "Describe the Custom theme on the current line." (interactive) (let ((widget (widget-at (line-beginning-position)))) (and widget (describe-theme (widget-get widget :theme-name))))) (defun custom-theme-save (&rest _ignore) (interactive) (customize-save-variable 'custom-enabled-themes custom-enabled-themes) (message "Custom themes saved for future sessions.")) (defun custom-theme-selections-toggle (widget &optional event) (when (widget-value widget) ;; Deactivate multiple-selections. (if (< 1 (length (delq nil (mapcar (lambda (x) (widget-value (cdr x))) custom--listed-themes)))) (error "More than one theme is currently selected"))) (widget-toggle-action widget event) (setq custom-theme-allow-multiple-selections (widget-value widget))) (provide 'cus-theme) ;;; cus-theme.el ends here