diff options
author | Miguel Ángel Moreno <mail@migalmoreno.com> | 2022-11-09 19:18:12 +0100 |
---|---|---|
committer | Miguel Ángel Moreno <mail@migalmoreno.com> | 2022-11-09 19:19:44 +0100 |
commit | d36e83c5e7000896f986b43458ad8392e00c747b (patch) | |
tree | bd72668a74d4cec1f5e5558eb5e40cd2237edbdc | |
parent | 37d0f2d2c4b935f6316003d385f235a39a177198 (diff) |
tailor.lisp: Rework implementation
-rw-r--r-- | assets/cut-operandi.png | bin | 37869 -> 0 bytes | |||
-rw-r--r-- | assets/cut-vivendi.png | bin | 36597 -> 0 bytes | |||
-rw-r--r-- | tailor.lisp | 478 |
3 files changed, 227 insertions, 251 deletions
diff --git a/assets/cut-operandi.png b/assets/cut-operandi.png Binary files differdeleted file mode 100644 index 5330aac..0000000 --- a/assets/cut-operandi.png +++ /dev/null diff --git a/assets/cut-vivendi.png b/assets/cut-vivendi.png Binary files differdeleted file mode 100644 index 7c394da..0000000 --- a/assets/cut-vivendi.png +++ /dev/null diff --git a/tailor.lisp b/tailor.lisp index 6dbd8df..f096a7e 100644 --- a/tailor.lisp +++ b/tailor.lisp @@ -1,8 +1,12 @@ (in-package #:nx-tailor) (nyxt:use-nyxt-package-nicknames) -(defparameter *current-theme* nil - "Current `user-theme'.") +(defvar *styles* '() + "The list of `style' objects the user instantiates +by including `with-style' invocations in their configuration.") + +(defvar *current-theme* nil + "The currently-active `user-theme'.") (defvar *dark-theme-timer* nil "`sb-ext:timer' used to apply the dark theme.") @@ -11,127 +15,182 @@ "`sb-ext:timer' used to apply the light theme.") (sera:export-always 'make-theme) -(defun make-theme (name &rest extra-slots &key &allow-other-keys) - "Build a `nx-tailor' theme. NAME is required and EXTRA-SLOTS -can vary depending on the theme complexity." - (apply #'make-instance 'user-theme :name name extra-slots)) - -(sera:export-always 'make-important) -(defun make-important (value) - "Output a CSS `!important' rule for VALUE." - (format nil "~a !important" value)) - -(defun list-of-lists-p (object) - "Return non-nil of OBJECT consists of a list of lists." - (and (listp object) - (every #'listp object))) +(defun make-theme (id &rest extra-slots &key &allow-other-keys) + "Build a `nx-tailor' theme. ID is required and EXTRA-SLOTS +will be supplied to the `theme:theme' constructor" + (apply #'make-instance 'user-theme :id id extra-slots)) (defun today () - "Compute the correct timestamp for today according to the local timezone - given by `local-time:*default-timezone*'." + "Compute the correct timestamp for today according to + the local timezone given by `local-time:*default-timezone*'." (local-time:adjust-timestamp (local-time:now) (set :hour 0) (set :minute 0) (set :sec 0) (set :nsec 0))) -(define-class cut () - ((name - :type (or null list) - :documentation "The name of the cut.") - (buffer - :type (or null list) - :documentation "A list of CSS rules to tweak the style of `nyxt:buffer'.") - (prompt - :type (or null list) - :documentation "A list of CSS rules to tweak the style of `nyxt:prompt-buffer'.") - (status - :type (or null list) - :documentation "A list of CSS rules to tweak the style of `nyxt:status-buffer'.") - (message - :type (or null list) - :documentation "A list of CSS rules to tweak the style of `nyxt:window'.") - (hint - :type (or null list) - :documentation "A list of CSS rules to tweak the style of `nyxt/hint-mode:hint-mode'.")) +(define-class theme-source (prompter:source) + ((prompter:name "User themes") + (prompter:constructor (themes (current-tailor-mode))) + (prompter:active-attributes-keys '("Id")))) + +(define-class style () + ((sym + nil + :type (or null symbol) + :documentation "The symbol of the class the style belongs to.") + (fn + nil + :type (or null function) + :documentation "A function that takes a theme and returns a +new style based on its value.")) (:export-class-name-p t) (:export-accessor-names-p t) - (:export-slot-names-p t) - (:accessor-name-transformer (class*:make-name-transformer name)) - (:documentation "A cut is a theme's custom finishing which styles various bits of Nyxt's interface.") - (:metaclass user-class)) + (:accessor-name-transformer (class*:make-name-transformer name))) (define-class user-theme (theme:theme) - ((name - "" - :type string - :documentation "The name of the theme.") - (cut + ((id nil - :type (or null cut) - :documentation "Provide styling for interface elements and allow to dynamically -change themes within a browser session.")) + :type (or null symbol) + :documentation "The theme identifier to use for setting +criteria in `tailor-mode'.")) (:export-class-name-p t) (:export-accessor-names-p t) (:accessor-name-transformer (class*:make-name-transformer name))) -(define-class theme-source (prompter:source) - ((prompter:name "User themes") - (prompter:constructor (themes (current-tailor-mode))) - (prompter:active-attributes-keys '("Name")))) +(defun current-tailor-mode () + "Return `tailor-mode' if it's active in the current buffer." + (alex:when-let ((mode (resolve-symbol :tailor-mode :mode '(:nx-tailor)))) + (find-submode mode))) + +(sera:export-always 'with-style) +(defmacro with-style (class-sym &body rules) + "Add style RULES to CLASS-SYM and apply RULES. +This automatically adds the user-defined style to the list of +`*styles*' with the CLASS-SYM identifier and a function that takes +a single argument for the current theme to apply style RULES to." + `(progn + (pushnew + (make-instance 'style + :sym ,class-sym + :fn (lambda (theme) + (theme:themed-css theme + ,@rules))) + *styles* :key #'sym) + (str:concat nyxt:%slot-default% + (theme:themed-css (theme *browser*) + ,@rules)))) + +(defun compute-current-style (style obj &optional style-slot) + "Return the current STYLE of OBJ. +Optionally, retrieve the original style of OBJ via STYLE-SLOT." + (str:concat (eval (getf (mopu:slot-properties + (find-class (class-name (class-of obj))) + (or style-slot 'nyxt:style)) + :initform)) + (when style + (funcall (fn style) *current-theme*)))) -(defun get-original-style (element &optional style-slot parent-class) - "Find the original STYLE-SLOT slot value of ELEMENT. If PARENT-CLASS, -look through all the children class slots." - (sb-mop:slot-definition-initform - (find (or style-slot 'nyxt:style) - (if parent-class - (sb-mop:class-slots - (find-class element)) - (sb-mop:class-direct-slots - (find-class element))) - :key (lambda (el) - (slot-value el 'sb-pcl::name))))) +(defgeneric load-style (style interface) + (:documentation "Load the STYLE of INTERFACE.")) -(defun theme-handler (buffer) - "Handler function to re-calculate styles in BUFFER." - (setf (nyxt::style buffer) (compute-style - *current-theme* - :element 'nyxt:buffer - :accessor #'buffer)) - (when (find-submode (resolve-symbol :hint-mode :mode) buffer) - (setf (nyxt/hint-mode::style (find-submode (resolve-symbol :hint-mode :mode) buffer)) - (compute-style *current-theme* - :element 'nyxt/hint-mode:hint-mode - :style-slot 'nyxt/hint-mode::style - :accessor #'hint)))) +(defmethod load-style (style (buffer nyxt:web-buffer)) + (flet ((style-web-buffer (buffer) + (setf (nyxt:style buffer) + (compute-current-style style buffer)))) + (when (nyxt:internal-url-p (url buffer)) + (style-web-buffer buffer)) + (hooks:add-hook + (nyxt:buffer-before-make-hook *browser*) + (make-instance 'hooks:handler + :fn #'style-web-buffer + :name 'style-web-buffer)) + (loop for buffer in (nyxt::buffer-initial-suggestions) + do (when (nyxt:internal-url-p (url buffer)) + (style-web-buffer buffer) + (nyxt:buffer-load (url buffer) :buffer buffer))))) + +(defmethod load-style (style (prompt-buffer nyxt:prompt-buffer)) + (flet ((style-prompt-buffer (prompt) + (setf (nyxt:style prompt) + (compute-current-style style prompt-buffer)))) + (style-prompt-buffer prompt-buffer) + (hooks:add-hook + (nyxt:prompt-buffer-make-hook *browser*) + (make-instance 'hooks:handler + :fn #'style-prompt-buffer + :name 'style-prompt-buffer)))) + +(defmethod load-style (style (window nyxt:window)) + (flet ((style-message-buffer (window) + (setf (nyxt::message-buffer-style window) + (compute-current-style + style window + 'nyxt:message-buffer-style)))) + (style-message-buffer window) + (hooks:add-hook + (nyxt:window-make-hook *browser*) + (make-instance 'hooks:handler + :fn #'style-message-buffer + :name 'style-message-buffer)) + (nyxt:echo ""))) + +(defmethod load-style (style (status-buffer nyxt:status-buffer)) + (flet ((style-status-buffer (status-buffer) + (setf (nyxt:style status-buffer) + (compute-current-style style status-buffer)))) + (style-status-buffer status-buffer) + (hooks:add-hook + (nyxt:window-make-hook *browser*) + (make-instance 'hooks:handler + :fn #'style-status-buffer + :name 'style-status-buffer)) + (nyxt::print-status))) + +(defmethod load-style (style (nyxt-mode nyxt:mode)) + (flet ((style-mode (mode) + (setf (slot-value nyxt-mode 'nyxt:style) + (compute-current-style style nyxt-mode)))) + (style-mode nyxt-mode) + (hooks:add-hook (nyxt:enable-mode-hook (nyxt:buffer nyxt-mode)) + (make-instance 'hooks:handler + :fn #'style-mode + :name 'style-mode)))) (define-mode tailor-mode () - "Manage custom browser themes and apply them on predefined criteria." + "Manage and apply user-defined browser themes on predefined criteria." ((themes '() :type list - :documentation "`user-theme' objects among which to select the main interface theme.") + :documentation "`user-theme' objects among which to select the + main interface theme.") (main nil - :type (or boolean :dark :light string user-theme cons) - :documentation "If a single theme, this will be chosen at startup if `auto-p' is `nil'. If a cons -pair and `auto-p' is non-`nil', the light and dark theme variants will be selected from a cons of the form -(LIGHT-THEME . DARK-THEME) where each element is the name of the corresponding theme.") + :type (or symbol :dark :light cons null) + :documentation "If a single theme id is specified, it will be chosen +at startup if `auto-p' is `nil'. If either `:dark' or `:light' is passed, +the corresponding `user-theme' will be selected at startup. +If a cons pair is specified and `auto-p' is non-`nil', the light and dark +theme variants will be selected from the pair in the form +(LIGHT-THEME . DARK-THEME) where each cell is the id of the theme.") (auto-p nil :type (or boolean :time :gtk) - :documentation "Whether to automatically apply a `theme'. If `:time' or `t', it will apply itself -based on the time of the day, if `:gtk' it will be applied based on the value -of `GTK_THEME', or if a matching theme name, it will always choose that theme on startup.") + :documentation "Whether to automatically apply a `theme'. If `:time' or `t', +it will apply the corresponding theme automatically based on the time of the day. +If `:gtk' it will be applied based on the value of the `GTK_THEME' environment +variable.") (light-theme-threshold (* 6 60 60) :type number - :documentation "Number of seconds after midnight at which the light theme should be activated.") + :documentation "Number of seconds after midnight when the light theme +should be activated.") (dark-theme-threshold (* 21 60 60) :type number - :documentation "Number of seconds after midnight at which the dark theme should be activated."))) + :documentation "Number of seconds after midnight when the dark theme +should be activated.") + (nyxt:glyph "⏾"))) (defmethod nyxt:customize-instance :after ((mode tailor-mode) &key) (with-slots (light-theme-threshold @@ -139,67 +198,102 @@ of `GTK_THEME', or if a matching theme name, it will always choose that theme on main themes) mode - (flet ((find-theme (name) - (find name themes :key #'name :test #'string=))) + (flet ((find-theme (id) + (find id themes :key #'id))) (setf light-theme-threshold (round light-theme-threshold)) (setf dark-theme-threshold (round dark-theme-threshold)) (when (consp main) (setf main (cons (find-theme (car main)) (find-theme (cdr main)))))))) +(defun find-theme-variant (mode &key dark) + "Find the first light `user-theme' in MODE. +If DARK, find the first dark `user-theme'." + (if dark + (find-if #'theme:dark-p (themes mode)) + (find-if-not #'theme:dark-p (themes mode)))) + (defmethod nyxt:enable ((mode tailor-mode) &key) - (with-slots (main auto-p) mode + (with-slots (main themes auto-p) mode (let ((light-theme (or (when (consp main) (car main)) (find-theme-variant mode))) (dark-theme (or (when (consp main) (cdr main)) (find-theme-variant mode :dark t))) - (light-theme-threshold (local-time:timestamp+ (today) (light-theme-threshold mode) :sec)) - (dark-theme-threshold (local-time:timestamp+ (today) (dark-theme-threshold mode) :sec))) - (unless (or (not (themes mode)) - (find (theme *browser*) (themes mode) :test #'equal) + (light-theme-threshold + (local-time:timestamp+ (today) (light-theme-threshold mode) :sec)) + (dark-theme-threshold + (local-time:timestamp+ (today) (dark-theme-threshold mode) :sec))) + (unless (or (not themes) + (find (theme *browser*) themes :test #'equal) *current-theme*) - (or (select-automatic-theme mode) + (or (load-automatic-theme mode) (when main - (select-theme - (name - (cond - ((eq main :light) light-theme) - ((eq main :dark) dark-theme) - ((stringp main) (find main (themes mode) :key #'name :test #'string=)) - (t main))) + (load-theme + (id + (case main + (:light light-theme) + (:dark dark-theme) + (t (find main themes :key #'id)))) mode)) - (select-theme (name (car (themes mode))) mode)) - (hooks:add-hook (nyxt:buffer-before-make-hook *browser*) #'theme-handler)) + (load-theme (id (car themes)) mode))) (unless (or (not auto-p) (equal auto-p :gtk)) - (unless *light-theme-timer* - (sb-ext:schedule-timer (setf *light-theme-timer* - (sb-ext:make-timer (lambda () - (select-theme (name light-theme) mode)) - :thread t)) - (local-time:timestamp-to-universal light-theme-threshold) - :absolute-p t - :repeat-interval 86400)) - (sleep 0.01) - (unless *dark-theme-timer* - (sb-ext:schedule-timer (setf *dark-theme-timer* - (sb-ext:make-timer (lambda () - (select-theme (name dark-theme) mode)) - :thread t)) - (local-time:timestamp-to-universal dark-theme-threshold) - :absolute-p t - :repeat-interval 86400)))))) + (flet ((set-timer (timer theme threshold) + (unless timer + (sb-ext:schedule-timer + (setf timer (sb-ext:make-timer + (lambda () + (load-theme (id theme) mode)) + :thread t)) + (local-time:timestamp-to-universal threshold) + :absolute-p t + :repeat-interval 86400)))) + (set-timer *light-theme-timer* light-theme light-theme-threshold) + (sleep 0.01) + (set-timer *dark-theme-timer* dark-theme dark-theme-threshold)))))) (defmethod nyxt:disable ((mode tailor-mode) &key) - (hooks:remove-hook (nyxt:buffer-before-make-hook *browser*) #'theme-handler) + (hooks:remove-hook (nyxt:buffer-before-make-hook *browser*) 'style-web-buffer) (hooks:remove-hook (nyxt:prompt-buffer-make-hook *browser*) 'style-prompt-buffer) + (hooks:remove-hook (nyxt:window-make-hook *browser*) 'style-message-buffer) + (hooks:remove-hook (nyxt:window-make-hook *browser*) 'style-status-buffer) + (setf (theme *browser*) *current-theme*) (setf *current-theme* nil) (when *light-theme-timer* (sb-ext:unschedule-timer *light-theme-timer*)) (when *dark-theme-timer* (sb-ext:unschedule-timer *dark-theme-timer*))) -(defmethod select-automatic-theme ((mode tailor-mode)) +(define-command-global load-theme (&optional id (mode (current-tailor-mode))) + "Load a custom `user-theme' with ID from MODE, apply it, and return it." + (flet ((find-style (sym) + (find sym *styles* :key #'sym))) + (let ((theme (or (and id (find id (themes mode) :key #'id)) + (nyxt:prompt1 + :prompt "Load theme" + :sources (make-instance 'theme-source)))) + (prompt-buffer (make-instance + 'nyxt:prompt-buffer + :window (current-window) + :sources (make-instance 'prompter:raw-source))) + (modes-with-style + (remove-if-not (lambda (mode) + (some (lambda (slot) + (eq 'nyxt:style slot)) + (mopu:slot-names (class-of mode)))) + (nyxt:modes (nyxt:buffer mode))))) + + (setf *current-theme* theme) + (setf (theme *browser*) theme) + (load-style (find-style 'nyxt:window) (current-window)) + (load-style (find-style 'nyxt:web-buffer) (nyxt:buffer mode)) + (load-style (find-style 'nyxt:status-buffer) (nyxt:status-buffer (current-window))) + (load-style (find-style 'nyxt:prompt-buffer) prompt-buffer) + (loop for mode-style in modes-with-style + do (load-style (find-style (class-name (class-of mode-style))) mode-style)) + theme))) + +(defmethod load-automatic-theme ((mode tailor-mode)) "Automatically set the theme based on the specified criteria in MODE." (alex:when-let ((auto (auto-p mode))) (let* ((light-theme (or (when (consp (main mode)) @@ -208,140 +302,22 @@ of `GTK_THEME', or if a matching theme name, it will always choose that theme on (dark-theme (or (when (consp (main mode)) (cdr (main mode))) (find-theme-variant mode :dark t))) - (light-theme-threshold (local-time:timestamp+ (today) (light-theme-threshold mode) :sec)) - (dark-theme-threshold (local-time:timestamp+ (today) (dark-theme-threshold mode) :sec))) + (light-theme-threshold (local-time:timestamp+ + (today) (light-theme-threshold mode) :sec)) + (dark-theme-threshold (local-time:timestamp+ + (today) (dark-theme-threshold mode) :sec))) (case auto (:gtk (if (or (str:containsp ":light" (uiop:getenv "GTK_THEME")) (null (uiop:getenv "GTK_THEME"))) - (select-theme (name light-theme) mode) - (select-theme (name dark-theme) mode))) + (load-theme (id light-theme) mode) + (load-theme (id dark-theme) mode))) (t (cond ((and (local-time:timestamp> (local-time:now) dark-theme-threshold) (not (local-time:timestamp< (local-time:now) light-theme-threshold))) - (select-theme (name dark-theme) mode)) + (load-theme (id dark-theme) mode)) ((local-time:timestamp< (local-time:now) light-theme-threshold) - (select-theme (name dark-theme) mode)) + (load-theme (id dark-theme) mode)) ((local-time:timestamp> (local-time:now) light-theme-threshold) - (select-theme (name light-theme) mode)))))))) - -(defun find-theme-variant (mode &key dark) - "Find the first light theme variant from MODE. If DARK, find the first dark theme." - (if dark - (find-if #'theme:dark-p (themes mode)) - (find-if-not #'theme:dark-p (themes mode)))) - -(defun current-tailor-mode () - "Return `tailor-mode' if it's active in the current buffer." - (find-submode - (resolve-symbol :tailor-mode :mode '(:nx-tailor)))) - -(defun compute-style (theme &key element accessor (style-slot nil)) - (str:concat - (eval (get-original-style element (or style-slot))) - (and (cut theme) (funcall (eval `(lambda (theme) - (theme:themed-css theme - ,@(funcall accessor (cut theme))))) - theme)))) - -(defmethod reload-style ((element nyxt:window)) - (if (not element) - (hooks:add-hook (nyxt:window-make-hook *browser*) - (make-instance - 'hooks:handler - :fn (lambda (window) - (setf (nyxt::style (nyxt::status-buffer window)) - (compute-style *current-theme* - :element 'nyxt:status-buffer - :accessor #'status) - (nyxt:message-buffer-style window) - (compute-style *current-theme* - :element 'nyxt:window - :style-slot 'nyxt:message-buffer-style - :accessor #'message)) - (hooks:remove-hook (nyxt:window-make-hook *browser*) - 'style-window-on-startup)) - :name 'style-window-on-startup)) - (setf (nyxt::style (nyxt::status-buffer (current-window))) - (compute-style *current-theme* - :element 'nyxt:status-buffer - :accessor #'status) - (nyxt:message-buffer-style (current-window)) - (compute-style *current-theme* - :element 'nyxt:window - :style-slot 'nyxt:message-buffer-style - :accessor #'message))) - (nyxt::print-status) - (nyxt::echo "")) - -(defmethod reload-style ((element nyxt:prompt-buffer)) - (if (not (cut *current-theme*)) - (progn - (hooks:remove-hook (nyxt:prompt-buffer-make-hook *browser*) - 'style-prompt) - (hooks:add-hook (nyxt:prompt-buffer-make-hook *browser*) - (make-instance - 'hooks:handler - :fn (lambda (prompt) - (setf (nyxt:style prompt) - (compute-style - *current-theme* - :element 'nyxt:prompt-buffer - :accessor #'prompt))) - :name 'style-prompt-sans-cut))) - (progn - (hooks:remove-hook (nyxt:prompt-buffer-make-hook *browser*) - 'style-prompt-sans-cut) - (hooks:add-hook (nyxt:prompt-buffer-make-hook *browser*) - (make-instance - 'hooks:handler - :fn (lambda (prompt) - (setf (nyxt:style prompt) - (compute-style - *current-theme* - :element 'nyxt:prompt-buffer - :accessor #'prompt))) - :name 'style-prompt))))) - -(defmethod reload-style ((element nyxt/hint-mode:hint-mode)) - (setf (nyxt/hint-mode::style element) - (compute-style *current-theme* - :element 'nyxt/hint-mode:hint-mode - :style-slot 'nyxt/hint-mode::style - :accessor #'hint))) - -(defmethod reload-style ((element nyxt:buffer)) - (loop for buffer in (nyxt::buffer-initial-suggestions) - do (when (nyxt:internal-url-p (url buffer)) - (setf (nyxt::style buffer) (compute-style *current-theme* - :element 'nyxt:buffer - :accessor #'buffer)) - (nyxt:buffer-load (nyxt:url buffer) :buffer buffer)))) - -(define-command-global select-theme (&optional name (mode (current-tailor-mode))) - "Select a `user-theme' with NAME from MODE and apply it." - (let ((theme (or (and name (find name (themes mode) :key #'name :test #'string=)) - (nyxt:prompt1 - :prompt "Select theme" - :sources (make-instance 'theme-source)))) - (hint-mode (find-submode (resolve-symbol :hint-mode :mode) (nyxt:buffer mode)))) - (setf *current-theme* theme - (theme *browser*) theme) - (reload-style (current-window)) - ;; TODO: don't rely on creating a bogus prompt buffer - (reload-style (make-instance 'nyxt:prompt-buffer - :window (current-window) - :sources (make-instance 'prompter:raw-source))) - (when hint-mode - (reload-style hint-mode)) - (reload-style (current-buffer)) - theme)) - -(define-command-global apply-current-theme () - "Apply the `*current-theme*' color scheme to the current page." - (when *current-theme* - (nyxt::html-set-style - (funcall (buffer (cut *current-theme*)) - *current-theme*) - (current-buffer)))) + (load-theme (id light-theme) mode)))))))) |