protesilaos / modus-themes

Highly accessible themes for GNU Emacs, conforming with the highest standard for colour contrast between background and foreground values (WCAG AAA).
https://protesilaos.com/emacs/modus-themes
GNU General Public License v3.0
553 stars 30 forks source link

Cycle through themes #78

Open oliverepper opened 1 year ago

oliverepper commented 1 year ago

Hi @protesilaos,

first of all thanks a lot for these great themes. I am very new to Emacs and your themes help making the switch pleasurable! I enjoyed this video https://youtu.be/kPNMHrF4Lq8 and it even helped me understanding a bit of how things are handled in Emacs.

One thing occurred to me, though. I think that the API modus-themes-toggle is giving up chances:

Since I am even newer to Lisp than I am to Emacs I have not yet prepared a PR but I tried my best in Lisp so you know what I mean:

(defun oe/rotate-list (list)
  (unless (not list)
    (append (cdr list) (list (car list)))))

(defun oe/cycle-themes (theme-list)
  (load-theme (car theme-list) :no-confirm)
  (funcall (function oe/rotate-list) theme-list))

(set 'all-themes '(
           modus-operandi
           modus-vivendi
           modus-operandi-deuteranopia
           modus-vivendi-deuteranopia
           modus-operandi-tinted
           modus-vivendi-tinted
           ))

(set 'themes-to-toggle '(
             modus-operandi
             modus-vivendi
             ))

(defun oe/cycle-themes-and-print-log-msg ()
  (set 'all-themes (funcall (function oe/cycle-themes) all-themes))
  (message "Theme: %s selected" (car (reverse all-themes))))

(defun oe/toggle-themes-and-print-log-msg ()
  (set 'themes-to-toggle (funcall (function oe/cycle-themes) themes-to-toggle))
  (message "Theme: %s selected" (cadr themes-to-toggle)))

(oe/cycle-themes-and-print-log-msg)
(oe/toggle-themes-and-print-log-msg)

Please let me know if you like the idea. I might give it a shot but this is literally the first Lisp code I ever wrote.

protesilaos commented 1 year ago

this is literally the first Lisp code I ever wrote.

Wow, that is good!

The idea is promising! Though let's not interfere with the toggle for the time being.

I made some minor tweaks to your code:

(defun oe/rotate-list (list)
  (when list
    (append (cdr list) (list (car list)))))

(defun oe/cycle-themes (theme-list)
  (load-theme (car theme-list) :no-confirm)
  (oe/rotate-list theme-list))

(defvar modus-themes--cycle-themes modus-themes-items)

(defun oe/cycle-themes-and-print-log-msg ()
  (setq modus-themes--cycle-themes (oe/cycle-themes modus-themes--cycle-themes))
  (message "Theme: %s selected" (car (reverse modus-themes--cycle-themes))))

(defvar modus-themes--toggle-themes modus-themes-to-toggle)

(defun oe/toggle-themes-and-print-log-msg ()
  (setq modus-themes--toggle-themes (oe/cycle-themes modus-themes--toggle-themes))
  (message "Theme: %s selected" (cadr modus-themes--toggle-themes)))

(oe/cycle-themes-and-print-log-msg)
(oe/toggle-themes-and-print-log-msg)

The main difference is to set "private" variables (denoted by the -- in their name), so that we do not change global/public variables.

Below is my attempt at a "cycle" command:

(defun modus-themes--rotate-list-of-symbol (symbol)
  "Rotate list value of SYMBOL by moving its car to the end.
Return the first element before performing the rotation.

This means that if `sample-list' has an initial value of `(one
two three)', this function will first return `one' and update the
value of `sample-list' to `(two three one)'.  Subsequent calls
will continue rotating accordingly."
  (unless (symbolp symbol)
    (user-error "%s is not a symbol" symbol))
  (when-let* ((value (symbol-value symbol))
              (list (and (listp value) value))
              (first (car list)))
    (set symbol (append (cdr list) (list first)))
    first))

(defvar modus-themes--cycle-themes modus-themes-items)

(defun modus-themes-cycle ()
  "Cycle through the `modus-themes-items'."
  (interactive)
  (let ((theme (modus-themes--rotate-list-of-symbol
                'modus-themes--cycle-themes)))
    (modus-themes-load-theme theme)
    (message "Loaded %s theme" theme)))

Those granted, we probably want to have a modus-themes-to-cycle as a companion to such a command. Its default value could be modus-themes-items.

oliverepper commented 1 year ago

Hey Prot,

I made a few adjustments on my cycle-themes idea, so that it saves my last choosen theme and loads it again when I start Emacs. This works super nice for my cycle-function but since your toggle function does not return the selected theme it's hard to advice it with the oe/save-selected-theme-function.

(use-package modus-themes
  :demand
  :custom
  (modus-themes-to-toggle       '(modus-operandi modus-vivendi))
  (modus-themes-disable-other-themes    t)

  (modus-themes-italic-constructs   t)
  (modus-themes-variable-pitch-ui   t)
  (modus-themes-mixed-fonts     t)

  (modus-themes-prompts     '(bold))
  (modus-themes-org-blocks      'tinted-background)

  (modus-themes-headings
   '(
     (1         . (1.215))
     (2         . (1.138))
     (3         . (1.076))
     (4         . (1.0))
     (5         . (0.937))

     (agenda-date       . (variable-pitch italic 1.138))
     (agenda-structure  . (variable-pitch light 1.8))
     (t . (medium))
     ))

  (modus-themes-common-palette-overrides
   '(
     (border-mode-line-active       bg-mode-line-active)
     (border-mode-line-inactive bg-mode-line-inactive)
     ))
  :bind ("<f5>" . modus-themes-toggle)
  :config
  (let ((last (expand-file-name "selected-theme.el" user-emacs-directory)))
    (if (file-exists-p last)
    (load last)
      (load-theme (car modus-themes-to-toggle) :no-confirm))))

(defun oe/save-selected-theme (theme)
  (with-temp-file (expand-file-name "selected-theme.el" user-emacs-directory)
    (insert (format "(load-theme '%s :no-confirm)\n" theme))))

(defun oe/rotate-list (list)
  (when list
    (append (cdr list) (list (car list)))))

(defun oe/cycle-themes (theme-list)
  (load-theme (car theme-list) :no-confirm)
  (oe/rotate-list theme-list))

(defun oe/cycle-themes-and-print-log-msg ()
  (interactive)
  (defvar modus-themes--cycle modus-themes-items)
  (setq modus-themes--cycle (oe/cycle-themes modus-themes--cycle))
  (let ((selected-theme (car (reverse modus-themes--cycle))))
    (message "Theme: %s selected" (car (reverse modus-themes--cycle)))
    (oe/save-selected-theme selected-theme)))

(global-set-key (kbd "<f4>") #'oe/cycle-themes-and-print-log-msg)

Keep in mind that I am still new to Emacs/elisp. I'd glady take any idea to make f4 and f5 save the choosen theme.

protesilaos commented 1 year ago

Very well!

(defun oe/cycle-themes-and-print-log-msg ()
  (interactive)
  (defvar modus-themes--cycle modus-themes-items)
  (setq modus-themes--cycle (oe/cycle-themes modus-themes--cycle))
  (let ((selected-theme (car (reverse modus-themes--cycle))))
    (message "Theme: %s selected" (car (reverse modus-themes--cycle)))
    (oe/save-selected-theme selected-theme)))

The defvar declares the variable. This usually is a top-level form because then the variable does not depend on the call to a specific function (if you expect the variable elsewhere, it will fail). Put the defvar outside the function and in the function use the setq you already have.

Maybe I am misreading this now, but in this let form the second (car (reverse ... could be replaced by selected-theme.

About the choice of key, you can always use any key you want, though note that the default for f4 is to either conclude an in-progress keyboard macro or execute the last defined keyboard macro (kmacro definitions are done with f3). I find those very useful.

oliverepper commented 1 year ago

Yes. You're right the second let should read selected-theme. That was an oversight. Regarding the defvar I used it like one would use the static keyword in C (the var belongs to the function but storage needs to be outside of the function). Do you have an idea how I can save the last selected theme that is choosen by modus' own toggle function?

protesilaos commented 1 year ago

Do you have an idea how I can save the last selected theme that is choosen by modus' own toggle function?

Would this do what you need?

 modus-themes.el | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/modus-themes.el b/modus-themes.el
index 3d4c1b4..c854002 100644
--- a/modus-themes.el
+++ b/modus-themes.el
@@ -1257,7 +1257,8 @@ (defun modus-themes-load-theme (theme)
 after loading the THEME."
   (modus-themes--disable-themes)
   (load-theme theme :no-confirm)
-  (run-hooks 'modus-themes-after-load-theme-hook))
+  (run-hooks 'modus-themes-after-load-theme-hook)
+  theme)

 (defun modus-themes--retrieve-palette-value (color palette)
   "Return COLOR from PALETTE.
oliverepper commented 1 year ago

Yes! And for what's it worth I think it's a better function, now. It has a side effect (setting up the theme) and now it tells the world not only that it did that side-effect but even how it parametrized this side-effect. ❤️

protesilaos commented 1 year ago

Very well! I just made the change.

oliverepper commented 1 year ago

This is very nice:

:bind ("<f5>" . (lambda () (interactive) (oe/save-selected-theme (modus-themes-toggle))))