aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2022-05-13 00:14:56 +0200
committerMiguel Ángel Moreno <mail@migalmoreno.com>2022-07-06 20:20:26 +0200
commit42d74bc04aa39efff84945c664f90cae6b650deb (patch)
tree9ff7739ee4189863cfdb54f79489b0e42e993bc5
Initial commit.
-rw-r--r--LICENSE29
-rw-r--r--README192
-rw-r--r--assets/cut-operandi.pngbin0 -> 37869 bytes
-rw-r--r--assets/cut-vivendi.pngbin0 -> 36597 bytes
-rw-r--r--nx-tailor.asd9
-rw-r--r--package.lisp18
-rw-r--r--tailor.lisp249
7 files changed, 497 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..800951a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2017-2022, Atlas Engineer LLC.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file
diff --git a/README b/README
new file mode 100644
index 0000000..7abd086
--- /dev/null
+++ b/README
@@ -0,0 +1,192 @@
+# -*- mode: org; -*-
+#+title: nx-tailor
+=nx-tailor= is a tailor for all-things [[https://nyxt.atlas.engineer/][Nyxt]] themes which helps you manage them, change them on-the-fly, and discover new ones in a unified interface. It provides high customizability over the smallest bits on Nyxt's interface, for those that enjoy their browser to match their style.
+
+* Installation
+To install the extension, you should download the source and place it in Nyxt's extensions path, given by the value of =(nyxt-source-registry)= (by default =~/.local/share/nyxt/extensions=).
+
+#+begin_src sh
+git clone https://github.com/efimerspan/nx-tailor ~/.local/share/nyxt/nx-tailor
+#+end_src
+
+The extension works with *Nyxt 3 onward*, so ensure to use it with the latest version of Nyxt master for the time being.
+
+If you want to place the extension elsewhere in the system, such as for development purposes, you can configure so via the ASDF source registry mechanism. For this, you'll need to create a file in the source registry directory, =~/.config/common-lisp/source-registry.conf.d/=, and then put the following contents into it, replacing the path with the desired system path.
+
+#+name: 10-personal-lisp.conf
+#+begin_src lisp
+(:tree "/path/to/user/location")
+#+end_src
+
+Then, make sure to refresh the ASDF cache via =asdf:clear-source-registry=. ASDF will now be able to find the extension on the custom path. For more information on this utility, please refer to the [[https://asdf.common-lisp.dev/asdf.html][ASDF manual]].
+
+However, by default Nyxt won't read the custom source registry path we provided, so ensure to include a =reset-asdf-registries= invocation in the Nyxt's initialization file too.
+
+In your Nyxt initialization file, place the following.
+
+#+begin_src lisp
+(load-after-system :nx-tailor (nyxt-init-file "/path/to/tailor.lisp"))
+
+(define-configuration buffer
+ ((default-modes `(tailor:tailor-mode ,@%slot-default%))))
+#+end_src
+
+Where =/path/to/tailor.lisp= is a custom file that should be created to provide the extension settings after the =nx-tailor= system has been successfully loaded. Inside this file, you should provide the extension options, explained in the following section.
+
+* Configuration
+=nx-tailor='s configuration is very simple and it involves passing in a list of themes to apply to the browser.
+
+#+begin_src lisp
+(import 'nx-tailor:make-theme)
+
+(define-configuration tailor:tailor-mode
+ ((tailor:auto-p t) ; Set theme based on GTK_THEME
+ (tailor:themes
+ (list
+ (make-theme "Modus Operandi"
+ :background-color "white"
+ :text-color "black"
+ :primary-color "#093060"
+ :secondary-color "#f0f0f0"
+ :tertiary-color "#dfdfdf"
+ :quaternary-color "#005a5f"
+ :accent-color "#8f0075"
+ :font-family "Iosevka"
+ :cut (make-instance 'tailor:cut))
+ (make-theme "Modus Vivendi"
+ :dark-p t
+ :background-color "black"
+ :text-color "white"
+ :primary-color "#c6eaff"
+ :secondary-color "#323232"
+ :tertiary-color "#323232"
+ :quaternary-color "#a8a8a8"
+ :accent-color "#afafef"
+ :font-family "Iosevka"
+ :cut (make-instance 'tailor:cut))))))
+#+end_src
+
+Above, we define some themes with a name and a set of theme attributes from the built-in =nyxt/theme= library. Optionally, each internal theme can take a =cut=, a list of CSS rules for various UI elements to give the browser a more custom look and allowing dynamic theme change without having to restart the browser, useful if you're the kind of person who switches between light and dark themes depending on the time of the day. A =cut= effectively aims at simplifying themeing, which is normally achieved by having to look through the =style= slot of multiple classes.
+
+#+begin_src lisp
+(define-configuration tailor:cut
+ ((tailor:name "Minimal")
+ (tailor:prompt
+ '((* :font-family theme:font-family)
+ ("#prompt-modes"
+ :display "none")
+ ("#prompt-area"
+ :background-color theme:tertiary
+ :color theme:quaternary
+ :border "1px solid"
+ :border-color (if (theme:dark-p theme:theme) theme:quaternary theme:text))
+ ("#input"
+ :background-color theme:tertiary
+ :color theme:text)
+ (".source-content"
+ :border "none"
+ :border-collapse collapse)
+ (".source-name"
+ :background-color theme:background
+ :color theme:text
+ :font-style "italic")
+ (".source-content th"
+ :padding-left "0"
+ :background-color theme:background
+ :font-weight "bold")
+ (".source-content td"
+ :padding "0 2px")
+ ("#selection"
+ :font-weight "bold"
+ :background-color theme:secondary
+ :color theme:text)))
+ (tailor:user-buffer
+ '((body
+ :font-family theme:font-family
+ :background-color theme:background
+ :color theme:text)
+ ("h1,h2,h3,h4,h5,h6"
+ :font-family "IBM Plex Sans"
+ :color theme:primary)
+ ("p,pre,td"
+ :font-family "IBM Plex Sans"
+ :color theme:text)
+ (pre
+ :background-color theme:tertiary)
+ ("button,a:link"
+ :color theme:text
+ :font-family "IBM Plex Sans")
+ (".button, .button:hover , .button:visited, .button:active"
+ :background-color theme:secondary
+ :border "1px solid"
+ :border-color (if (theme:dark-p theme:theme) theme:quaternary theme:text)
+ :color theme:text)
+ (code
+ :font-family "Iosevka"
+ :background-color theme:tertiary)))
+ (tailor:status
+ '((body
+ :font-family theme:font-family
+ :height "100%"
+ :width "100%"
+ :border "1px solid"
+ :border-color (if (theme:dark-p theme:theme) theme:quaternary theme:text)
+ :box-sizing "border-box"
+ :line-height "20px"
+ :display "flex"
+ :flex-direction "column"
+ :background theme:tertiary
+ :flex-wrap "wrap")
+ ("#container"
+ :display "flex"
+ :height "100%"
+ :width "100%"
+ :line-height "20px"
+ :justify-content "space-between"
+ :align-items "center")
+ ("#buttons"
+ :display "flex"
+ :align-items "center"
+ :justify-content "center"
+ :line-height "20px"
+ :height "100%")
+ ("#url"
+ :font-weight "bold"
+ :max-width "60%"
+ :padding-right "0"
+ :padding-left "5px"
+ :background-color theme:tertiary
+ :color theme:text
+ :box-sizing "border-box"
+ :z-index "auto")
+ ("#tabs, #controls" :display "none")
+ ("#modes"
+ :padding-right "2px"
+ :background-color theme:tertiary
+ :box-sizing "border-box"
+ :color theme:text
+ :display "flex"
+ :justify-contents "flex-end"
+ :z-index "auto")
+ (.button
+ :color theme:text)))
+ (tailor:message
+ '((body
+ :color theme:text
+ :background-color theme:background
+ :font-family theme:font-family)))
+ (tailor:hint
+ '((".nyxt-hint"
+ :background-color theme:primary
+ :color theme:background
+ :font-weight "bold"
+ :padding "0px 3px"
+ :border-radius "2px"
+ :z-index #.(1- (expt 2 31)))))))
+#+end_src
+
+The above is an example =cut= which I use in my configuration to achieve a more compact look to Nyxt. However, you're free to modify or not each slot to your heart's content. You can pass different =cut='s to different themes, allowing you to change both the color palette and the layout of Nyxt per theme.
+
+[[file:assets/cut-operandi.png]]
+
+[[file:assets/cut-vivendi.png]]
diff --git a/assets/cut-operandi.png b/assets/cut-operandi.png
new file mode 100644
index 0000000..5330aac
--- /dev/null
+++ b/assets/cut-operandi.png
Binary files differ
diff --git a/assets/cut-vivendi.png b/assets/cut-vivendi.png
new file mode 100644
index 0000000..7c394da
--- /dev/null
+++ b/assets/cut-vivendi.png
Binary files differ
diff --git a/nx-tailor.asd b/nx-tailor.asd
new file mode 100644
index 0000000..7edd2c4
--- /dev/null
+++ b/nx-tailor.asd
@@ -0,0 +1,9 @@
+(defsystem #:nx-tailor
+ :description "An interface to manage themes in Nyxt."
+ :author "conses"
+ :license "BSD 3-Clause"
+ :version "0.0.1"
+ :serial t
+ :depends-on (#:nyxt)
+ :components ((:file "package")
+ (:file "tailor")))
diff --git a/package.lisp b/package.lisp
new file mode 100644
index 0000000..4383db8
--- /dev/null
+++ b/package.lisp
@@ -0,0 +1,18 @@
+(uiop:define-package #:nx-tailor
+ (:nicknames #:tailor)
+ (:use #:cl)
+ (:import-from #:nyxt
+ #:define-class
+ #:user-class
+ #:define-mode
+ #:define-command
+ #:define-command-global
+ #:*browser*
+ #:theme
+ #:current-buffer
+ #:current-window
+ #:find-submode
+ #:resolve-symbol
+ #:buffer
+ #:url)
+ (:documentation "An interface to manage themes in Nyxt."))
diff --git a/tailor.lisp b/tailor.lisp
new file mode 100644
index 0000000..fec7e53
--- /dev/null
+++ b/tailor.lisp
@@ -0,0 +1,249 @@
+(in-package #:nx-tailor)
+(nyxt:use-nyxt-package-nicknames)
+
+(defparameter *current-theme* nil
+ "Current `internal-theme'.")
+
+(sera:export-always 'make-theme)
+(defun make-theme (name &rest extra-slots &key &allow-other-keys)
+ "Builds a `nx-tailor' theme. NAME is required and EXTRA-SLOTS
+can vary depending on the theme complexity."
+ (apply #'make-instance 'internal-theme :name name extra-slots))
+
+(defun list-of-lists-p (object)
+ "Returns non-nil of OBJECT consists of a list of lists."
+ (and (listp object)
+ (every #'listp object)))
+
+(define-class cut ()
+ ((name
+ :type (or null list)
+ :documentation "The name of the cut.")
+ (user-buffer
+ :type (or null list)
+ :documentation "A list of CSS rules to tweak a user buffer.")
+ (prompt
+ :type (or null list)
+ :documentation "A list of CSS rules to tweak the prompt buffer.")
+ (status
+ :type (or null list)
+ :documentation "A list of CSS rules to tweak the status buffer.")
+ (message
+ :type (or null list)
+ :documentation "A list of CSS rules to tweak the message area.")
+ (hint
+ :type (or null list)
+ :documentation "A list of CSS rules to tweak the look of hints."))
+ (: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))
+
+(define-class internal-theme (theme:theme)
+ ((name
+ ""
+ :type string
+ :documentation "The name of the theme.")
+ (cut
+ nil
+ :type (or null cut)
+ :documentation "`cut' provides styling for interface parts and allows to dynamically
+change themes within a browser session."))
+ (: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 get-original-style (element &optional style-slot parent-class)
+ "Finds the original STYLE-SLOT slot value of ELEMENT. If PARENT-CLASS,
+looks 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)))))
+
+(defun theme-handler (buffer mode)
+ "Handler function to re-calculate styles in BUFFER with MODE."
+ (setf (nyxt::style buffer) (compute-style
+ *current-theme*
+ :element 'nyxt:buffer
+ :accessor #'user-buffer))
+ (when (find-submode (resolve-symbol :web-mode :mode) buffer)
+ (setf (nyxt/web-mode:box-style (find-submode (resolve-symbol :web-mode :mode) buffer))
+ (compute-style *current-theme*
+ :element 'nyxt/web-mode:web-mode
+ :style-slot 'nyxt/web-mode:box-style
+ :accessor #'hint))))
+
+(define-mode tailor-mode (nyxt/style-mode:style-mode)
+ "Mode that manages custom browser themes."
+ ((themes
+ '()
+ :type list
+ :documentation "`internal-theme' objects among which to select the main internal interface theme.")
+ (auto-p
+ nil
+ :type boolean
+ :documentation "Whether to automatically apply a `theme' variant based on the system environment.")))
+
+(defmethod nyxt:enable ((mode tailor-mode) &key)
+ (with-slots (auto-p) mode
+ (unless (or (not (themes mode))
+ (find (theme *browser*) (themes mode) :test #'equal)
+ *current-theme*)
+ (or (and auto-p
+ (if (string= (uiop:getenv "GTK_THEME") ":light")
+ (select-theme (name (find-theme-variant mode)) mode)
+ (setf (nyxt::style (buffer mode))
+ (compute-style
+ (select-theme
+ (name (find-theme-variant mode :dark t))
+ mode)
+ :element 'nyxt:buffer
+ :accessor #'user-buffer))))
+ (select-theme (name (car (themes mode))) mode))
+ (hooks:add-hook (nyxt:buffer-before-make-hook *browser*)
+ (make-instance
+ 'hooks:handler
+ :fn (lambda (buffer)
+ (theme-handler buffer mode))
+ :name 'handle-theme)))))
+
+(defmethod nyxt:disable ((mode tailor-mode) &key)
+ (hooks:remove-hook (nyxt:buffer-before-make-hook *browser*) #'theme-handler)
+ (hooks:remove-hook (nyxt:prompt-buffer-make-hook *browser*) 'style-prompt-buffer)
+ (setf *current-theme* nil))
+
+(defun find-theme-variant (mode &key dark)
+ "Finds the first light theme variant from MODE. If DARK, it finds the first dark theme."
+ (if dark
+ (find-if #'theme:dark-p (themes mode))
+ (find-if-not #'theme:dark-p (themes mode))))
+
+(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))))
+
+(defun current-tailor-mode ()
+ "Returns `tailor-mode' if it's active in the current buffer."
+ (find-submode
+ (resolve-symbol :tailor-mode :mode '(:nx-tailor))))
+
+(defun reload-window-style ()
+ "Reloads the window style."
+ (if (not (current-window))
+ (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 ""))
+
+(defun reload-prompt-style ()
+ "Reloads the prompt buffer style."
+ (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)))))
+
+(defun reload-hint-style (mode)
+ "Reloads hint styles in MODE."
+ (when (find-submode (resolve-symbol :web-mode :mode) (buffer mode))
+ (setf (nyxt/web-mode:box-style
+ (find-submode (resolve-symbol :web-mode :mode) (buffer mode)))
+ (compute-style *current-theme*
+ :element 'nyxt/web-mode:web-mode
+ :style-slot 'nyxt/web-mode:box-style
+ :accessor #'hint))))
+
+(defun reload-buffer-style ()
+ "Reloads user buffers styles"
+ (loop for buffer in (nyxt::buffer-initial-suggestions)
+ do (progn
+ (setf (nyxt::style buffer) (compute-style *current-theme*
+ :element 'nyxt:buffer
+ :accessor #'user-buffer))
+ (nyxt:buffer-load (nyxt:url buffer) :buffer buffer))))
+
+(define-command-global select-theme (&optional name (mode (current-tailor-mode)))
+ "Selects an `internal-theme' with NAME from MODE and applies it."
+ (let ((theme (or (and name
+ (find name (themes mode)
+ :key #'name :test #'string=))
+ (nyxt:prompt1
+ :prompt "Select theme"
+ :sources (make-instance 'theme-source)))))
+ (setf *current-theme* theme
+ (theme *browser*) theme)
+ (reload-window-style)
+ (reload-prompt-style)
+ (reload-hint-style mode)
+ (reload-buffer-style)
+ theme))
+
+(define-command-global apply-current-theme ()
+ "Applies the `current-theme''s color scheme to the current page."
+ (when *current-theme*
+ (nyxt::html-set-style
+ (funcall (user-buffer (cut (current-theme (current-tailor-mode))))
+ *current-theme*)
+ (current-buffer))))