branch: externals/theme-buffet
commit c00d1e40974a2bdef3cb1bae3b774197be73bf78
Author: bboal <[email protected]>
Commit: bboal <[email protected]>
First Commit
---
theme-buffet.el | 497 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 497 insertions(+)
diff --git a/theme-buffet.el b/theme-buffet.el
new file mode 100644
index 0000000000..08934cdaf0
--- /dev/null
+++ b/theme-buffet.el
@@ -0,0 +1,497 @@
+;;; theme-buffet.el --- Time based theme switcher -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Bruno Boal <[email protected]>,
+;; Protesilaos Stavrou <[email protected]>
+;; Maintainer: Theme-Buffet Development <~bboal/[email protected]>
+;; URL: https://git.sr.ht/~bboal/theme-buffet
+;; Version: 0.0.0
+;; Package-Requires: ((emacs "29.1"))
+
+;; This file is NOT part of GNU Emacs.
+
+;; This program 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.
+;;
+;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Theme-Buffet lets the user specify different time periods of the day and for
+;; each period, a list of preferred themes to be randomly loaded accordingly.
+;; To install you just have to clone the repo from the url, add the path to the
+;; 'load-path variable and then require the library. Here's an example, for
+;; those who still love the Emacs 28 or earlier way of doing things:
+;;
+;; # In the terminal
+;; git clone https://git.sr.ht/~bboal/theme-buffet ~/.emacs.d/theme-buffet
+;;
+;; ;; In Emacs, evaluate
+;; (add-to-list 'load-path "~/.emacs.d/theme-buffet")
+;; (require 'theme-buffet)
+;;
+;; The newest way, from Emacs 29 onward:
+;;
+;; (package-vc-install "https://git.sr.ht/~bboal/theme-buffet")
+;;
+;;
+;; There are two templates preconfigured available for usage. One enabled by
+;; default, with the standard themes that come with vanilla Emacs; the other
+;; more fancier, can be easily enabled by evaluating the following:
+;;
+;; (setq theme-buffet-menu 'modus-ef)
+;;
+;; The binding above will set the themes to be either Modus or Ef, authored by
+;; Protesilaos Stavrou <https://git.sr.ht/~protesilaos>, distributed across six
+;; periods of the day (night, twilight, morning, day, afternoon and evening).
The
+;; library will require the aforementioned package, installing if
+;; necessary. Finally to start using Theme-Buffet, evaluate:
+;;
+;; (theme-buffet-mode 1)
+;;
+;;
+;; Following the appanage way of Emacs, both the names and number of themes and
+;; time periods can be freely changed while mantaining the same structure.
There
+;; is also a time-offset that can be set by the user to match a specific
+;; time-zone/personal preference. E.g
+;;
+;; (setq theme-buffet-time-offset 2)
+;;
+;; All this can be achieved by tweaking `theme-buffet-end-user'. For
+;; inspiration, take a look at `theme-buffet--modus-ef' which is used when
+;; setting `theme-buffet-menu' to 'modus-ef like demonstrated above.
+;;
+;;
+;; Disclaimer from Bruno Boal to the reader: This package was produced during
+;; my learning sessions with Protesilaos "Prot" Stavrou and improved as
+;; homework. Most of the credit goes to him, the mistakes you may find are my
+;; own. Personally, despite the disadvantages and advantages of not being a
+;; professional programmer, it is essential for me to always have fun and
+;; enjoyment during learning and programming. In this respect, mission
+;; accomplished, a big "thank you!" to my mentor. Also, keep in mind at least
+;; two things - the fact that this package, like many others before it, has its
+;; genesis in a collective effort, with didatic purposes and personal use in
+;; mind, but also that future improvements could and should come from people
+;; like you, a user of free software.
+;;
+;; Happy hacking!
+;;
+
+;;; Code:
+
+
+(defgroup theme-buffet nil
+ "Time based theme switcher.
+Assortment of preference based themes available for consumption according to
+the time of the day. A true theme feast for the eyes..."
+ :group 'faces)
+
+
+(defun theme-buffet--get-themes(&optional plist-usage)
+ "Get themes from default Emacs build directory and `custom-theme-load-path'.
+Return list for usage in `theme-buffet-menu' type options if PLIST-USAGE is
+non-nil."
+ (let* ((default (expand-file-name "themes/" data-directory))
+ (custom (butlast custom-theme-load-path))
+ (themes-dirs (cons default custom))
+ (themes (flatten-tree
+ (mapcar (lambda (f)
+ (if (symbolp f) (setq f (symbol-value f)))
+ (directory-files f nil ".*-theme\\.el\\'"))
+ themes-dirs))))
+ (mapcar (lambda (theme)
+ (let ((symbol-list (intern
+ (string-trim-right theme "-theme\\.el"))))
+ (if plist-usage (list 'const symbol-list) symbol-list)))
+ themes)))
+
+(defvar theme-buffet--const-themes (theme-buffet--get-themes t))
+
+
+
+(defconst theme-buffet--built-in
+ '(:night (wheatgrass manoj-dark modus-vivendi)
+ :morning (adwaita whiteboard leuven modus-operandi tango dichromacy
tsdh-light)
+ :afternoon (leuven-dark tango-dark tsdh-dark misterioso)
+ :evening (deeper-blue wombat))
+ "Emacs default themes distributed along 4 defined periods.")
+
+(defconst theme-buffet--modus-ef
+ '(:night (ef-autumn
+ ef-duo-dark
+ ef-night
+ ef-tritanopia-dark
+ ef-winter
+ ef-dark
+ modus-vivendi-deuteranopia)
+ :twilight (ef-bio
+ ef-cherie
+ modus-vivendi
+ modus-vivendi-tritanopia)
+ :morning (ef-elea-light
+ ef-maris-light
+ ef-spring
+ ef-tritanopia-light
+ modus-operandi-tritanopia)
+ :day (ef-deuteranopia-light
+ ef-frost
+ ef-light
+ ef-trio-light
+ modus-operandi
+ modus-operandi-deuteranopia)
+ :afternoon (ef-cyprus
+ ef-day
+ ef-duo-light
+ ef-kassio
+ ef-melissa-light
+ ef-summer
+ modus-operandi-tinted)
+ :evening (ef-deuteranopia-dark
+ ef-elea-dark
+ ef-maris-dark
+ ef-melissa-dark
+ ef-symbiosis
+ ef-trio-dark
+ modus-vivendi-tinted))
+ "Different periods of the day combined with Ef or Modus themes.
+For those who just don't have the time and want the best.")
+
+(defcustom theme-buffet--end-user
+ '(:night (wheatgrass manoj-dark modus-vivendi)
+ :morning (adwaita whiteboard leuven modus-operandi tango dichromacy
tsdh-light)
+ :afternoon (leuven-dark tango-dark tsdh-dark misterioso)
+ :evening (deeper-blue wombat))
+ "Associate day periods with list of themes.
+Each association is of the form `:KEYWORD (THEMES)' where :KEYWORD is one among
+:dark, :twilight, :dawn, etc, and (THEMES), a list of existent themes.
Prefilled
+with Emacs default themes as an example to change by the user."
+ :type `(plist
+ :options
+ (((const :tag "Darkness of the night" :night)
+ (repeat (choice symbol ,@theme-buffet--const-themes)))
+ ((const :tag "Bright sun is up" :morning)
+ (repeat (choice symbol ,@theme-buffet--const-themes)))
+ ((const :tag "Perhaps a clouded afternoon" :afternoon)
+ (repeat (choice symbol ,@theme-buffet--const-themes)))
+ ((const :tag "Close to the sunset" :evening)
+ (repeat (choice symbol ,@theme-buffet--const-themes))))))
+
+
+(defcustom theme-buffet-menu 'built-in
+ "Define which property list to use when selecting the theme list."
+ :type '(choice (const :tag "Built-in Emacs themes" built-in)
+ (const :tag "Modus and Ef themes" modus-ef)
+ (const :tag "User specified themes" end-user)))
+
+
+(defun theme-buffet--selected-menu ()
+ "Return property list based on given MENU."
+ (cond
+ ((eq theme-buffet-menu 'built-in)
+ theme-buffet--built-in)
+ ((eq theme-buffet-menu 'modus-ef)
+ theme-buffet--modus-ef)
+ ((eq theme-buffet-menu 'end-user)
+ theme-buffet--end-user)
+ (t
+ nil)))
+
+
+(defun theme-buffet--hours-secs(hours)
+ "Number of seconds in HOURS."
+ (* hours 60 60))
+
+
+(defconst theme-buffet--secs-in-day
+ (theme-buffet--hours-secs 24)
+ "Number of seconds in a day.")
+
+
+(defun theme-buffet--keywords()
+ "Get the name of the keywords defining the day periods."
+ (let ((selected-menu (theme-buffet--selected-menu)))
+ (if (plistp selected-menu)
+ (seq-filter #'keywordp selected-menu)
+ (user-error "The Theme-Buffet Chef cannot work with your supplied
themes. Check `theme-buffet-menu'"))))
+
+
+(defun theme-buffet--periods()
+ "Get the number of keywords that define the day periods."
+ (length (theme-buffet--keywords)))
+
+
+(defun theme-buffet--interval()
+ "Get the number of seconds that each given time period should remain active."
+ (/ theme-buffet--secs-in-day (theme-buffet--periods)))
+
+
+(defun theme-buffet--get-time()
+ "Get the `current-time' in seconds."
+ (let ((time-smh (take 3 (decode-time)))
+ seconds)
+ (while time-smh
+ (setq seconds (cons (pop time-smh) seconds)
+ time-smh (mapcar (lambda (n) (* 60 n))
+ time-smh)))
+ (apply #'+ seconds)))
+
+
+(defun theme-buffet--natnum-from-to (start end &optional step)
+ "Create a list for applying in defcustom's type choice customization.
+When not provided, STEP will default to 1.
+The final list is of the form ((const START) (const START+STEP) ... (const
+END-STEP) (const END))"
+ (mapcar (lambda (x)
+ (list 'const x))
+ (number-sequence start end step)))
+
+
+(defcustom theme-buffet-time-offset 0
+ "Added time in HOURS (integer number) to shift the day periods.
+Used for compensate winter/summer times or specific weather situations."
+ :type `(choice ,@(theme-buffet--natnum-from-to -12 12)))
+
+
+(defun theme-buffet--get-offset()
+ "Error checking for `theme-buffet-time-offset' variable.
+Has to be an integer number and no greater than 12h in absolute value"
+ (cond
+ ((or (not (integerp theme-buffet-time-offset))
+ (> (abs theme-buffet-time-offset) 12))
+ (message "Theme-Buffet time offset should be an integer number between -12
to 12 instead of `%s'. Resetting to 0."
+ theme-buffet-time-offset)
+ 0)
+ (t
+ (theme-buffet--hours-secs theme-buffet-time-offset))))
+
+
+(defun theme-buffet--current-period()
+ "Get the current period reference the number of keywords in `theme-buffet'."
+ (let ((offset (mod (+ (theme-buffet--get-time)
+ (theme-buffet--get-offset))
+ theme-buffet--secs-in-day)))
+ (ceiling offset (theme-buffet--interval))))
+
+
+(defun theme-buffet--get-period-keyword()
+ "Get the keyword of the current period as specified in `theme-buffet'."
+ (nth (1- (theme-buffet--current-period)) (theme-buffet--keywords)))
+
+
+(defun theme-buffet--reload-theme(chosen-theme &optional added-message)
+ "Load CHOSEN-THEME after disabling the current one.
+An additional ADDED-MESSAGE can be appended to the original string for added
+information."
+ (let* ((standard-message "Theme-Buffet served")
+ (message (if added-message (concat standard-message " " added-message)
+ standard-message)))
+ (mapc #'disable-theme custom-enabled-themes)
+ (load-theme chosen-theme :no-confirm)
+ (message "%s `%s'" message chosen-theme)))
+
+
+(defun theme-buffet--get-theme-list(period)
+ "Get list of themes of PERIOD, excluding the current if more are available."
+ (when-let ((selected-menu (theme-buffet--selected-menu))
+ (theme-list (plist-get selected-menu period)))
+ (or (remq (car custom-enabled-themes) theme-list)
+ theme-list)))
+
+
+(defun theme-buffet--load-random(period)
+ "Load random theme according to PERIOD.
+
+Omit current theme if it's not the only pertaining to the list of the
+corresponding period. Being this the case, the same theme shall be served.
+
+An error message will appear if the theme is not available to load through
+`load-theme'."
+ (if-let ((themes (theme-buffet--get-theme-list period))
+ (chosen-theme (seq-random-elt themes))
+ ((memq chosen-theme (theme-buffet--get-themes))))
+ (theme-buffet--reload-theme chosen-theme)
+ (user-error "Theme-Buffet Chef says `%s' is not known or installed!"
+ chosen-theme)))
+
+
+
+(defvar theme-buffet-theme-history nil
+ "Theme-Buffet period history.")
+
+(defun theme-buffet--theme-prompt()
+ "Prompt the user the theme to choose for the present period."
+ (let ((prompt "From current period choose a theme: ")
+ (collection (theme-buffet--get-theme-list
+ (theme-buffet--get-period-keyword)))
+ (history-var 'theme-buffet-theme-history))
+ (completing-read prompt collection nil t nil history-var)))
+
+;;;###autoload
+(defun theme-buffet-a-la-carte()
+ "Prompt user for theme according to the current period of the day."
+ (declare (interactive-only t))
+ (interactive)
+ (let ((chosen-theme (intern (theme-buffet--theme-prompt))))
+ (theme-buffet--reload-theme chosen-theme "as per your desires. Enjoy..."
)))
+
+
+
+(defvar theme-buffet-period-history nil
+ "Theme-Buffet period history.")
+
+(defun theme-buffet--period-prompt()
+ "Prompt user for the day period from the list of periods."
+ (let ((prompt "Choose a period of the day: ")
+ (collection (theme-buffet--keywords))
+ (history-var 'theme-buffet-order-history))
+ (completing-read prompt collection nil t nil history-var)))
+
+;;;###autoload
+(defun theme-buffet-order-other-period()
+ "Interactively load a random theme from the current day period."
+ (declare (interactive-only t))
+ (interactive)
+ (let ((period (intern (theme-buffet--period-prompt))))
+ (theme-buffet--load-random period)))
+
+
+;;;###autoload
+(defun theme-buffet-anything-goes()
+ "Interactively load an existing random theme."
+ (declare (interactive-only t))
+ (interactive)
+ (theme-buffet--reload-theme (seq-random-elt (theme-buffet--get-themes))
+ "as a suprise"))
+
+
+
+(defvar theme-buffet-user-timers-history nil
+ "Theme-Buffet user timers history.")
+
+
+;;;; Period timer
+(defvar theme-buffet-timer-periods nil
+ "Timer that calls Theme-Buffet's Chef into the kitchen.")
+
+;;;; Hourly timer
+(defvar theme-buffet-timer-hours nil
+ "Timer that calls one of Theme-Buffet's Sous-Chef into the kitchen.")
+
+;;;; Minutely timer
+(defvar theme-buffet-timer-mins nil
+ "Timer that calls another Theme-Buffet's Sous-Chef into the kitchen.")
+
+
+(defun theme-buffet--free-timer(timer-obj)
+ "Cancel and set to nil the timer TIMER-OBJ."
+ (when-let (((boundp timer-obj))
+ (obj (symbol-value timer-obj)))
+ (cancel-timer obj)
+ (set timer-obj nil)))
+
+
+(defmacro theme-buffet--define-timer(units)
+ "Define interactive functions to set timer in UNITS.
+UNITS is an unquoted symbol, mins or hours and refers to timer of the same
+naming."
+ (let ((fn-name (intern (format "theme-buffet-timer-%s" units)))
+ (max-mins 180) (min-mins 1)
+ (max-hours 12) (min-hours -12)
+ (factor
+ (cond
+ ((eq units 'mins) 60)
+ ((eq units 'hours) 3600)
+ (t (user-error
+ "Bad `units' argument on `theme-buffet--define-timer %s'"
units)))))
+ `(defun ,fn-name (number)
+ ,(format "Set interactively the timer for NUMBER of %s." units)
+ (interactive
+ (list (read-number ,(format "Theme Buffet service in how many %s? "
units)
+ nil
+ 'theme-buffet-user-timers-history)))
+ (let ((max-num ,(if (eq units 'mins) max-mins max-hours))
+ (min-num ,(if (eq units 'mins) min-mins min-hours)))
+ (if (or (not (natnump number))
+ (> number max-num)
+ (< number min-num))
+ (message "The input number should be a natural between %s and %s
instead of `%s'."
+ min-num max-num number)
+ (let ((timer-secs (* ,factor number)))
+ (setq ,fn-name (run-at-time timer-secs timer-secs
+ #'theme-buffet--load-random
+ (theme-buffet--get-period-keyword)))
+ (message "Theme-Buffet Sous-Chef is rushing into the
kitchen...")))))))
+
+;;;###autoload (autoload 'theme-buffet-timer-mins "theme-buffet")
+(theme-buffet--define-timer mins) ; (theme-buffet-timer-mins n)
+;;;###autoload (autoload 'theme-buffet-timer-hours "theme-buffet")
+(theme-buffet--define-timer hours) ; (theme-buffet-timer-hours n)
+
+
+
+(defmacro theme-buffet--define-menu-defuns(menu)
+ "Define interactive functions to choose property list with themes to use.
+The timer is clean, the chosen MENU is set with it's corresponding keywords."
+ (let* ((doc-built-in "Built-in Emacs themes. If you like minimalism and
standard suits your needs.")
+ (doc-modus-ef "The way to go when you're in a hurry and need to feast
fast but in style.
+Theme-Buffet uses both Modus and Ef themes, mixed and matched for a maximum
+\"Wow!!\" factor of pleasure and professionalism. At least in this developer's
+opinion.")
+ (doc-end-user "End user selected themes")
+ (docstring (cond
+ ((eq menu 'built-in) doc-built-in)
+ ((eq menu 'modus-ef) doc-modus-ef)
+ ((eq menu 'end-user) doc-end-user)
+ (t "This is not correct!"))))
+ `(defun ,(intern (format "theme-buffet-%s" menu)) ()
+ ,docstring
+ (interactive)
+ (theme-buffet--free-timer 'theme-buffet-timer-periods)
+ (setq theme-buffet-menu (quote ,menu)
+ theme-buffet-timer-periods
+ (run-at-time t (theme-buffet--interval)
+ #'theme-buffet--load-random
+ (theme-buffet--get-period-keyword)))
+ (message "Sucess! Theme-Buffet Chef is firing up %s themes..."
',menu))))
+
+;;;###autoload (autoload 'theme-buffet-built-in "theme-buffet")
+(theme-buffet--define-menu-defuns built-in) ; (theme-buffet-built-in)
+;;;###autoload (autoload 'theme-buffet-modus-ef "theme-buffet")
+(theme-buffet--define-menu-defuns modus-ef) ; (theme-buffet-modus-ef)
+;;;###autoload (autoload 'theme-buffet-end-user "theme-buffet")
+(theme-buffet--define-menu-defuns end-user) ; (theme-buffet-end-user)
+
+
+
+;;;###autoload
+(define-minor-mode theme-buffet-mode
+ "Theme-Buffet serves your preferred themes according to the time of day.
+You eyes will thank you. Or not...
+
+The preference for the themes is specified in the `theme-buffet-menu'"
+ :init-value nil
+ :global t
+ :keymap nil
+ (if theme-buffet-mode
+ ;; 2023-11-20 FIXME => The `unless' below is because `theme-buffet-mode'
is
+ ;; called every time the timer runs without an explanation.
+ (unless theme-buffet-timer-periods
+ (pcase theme-buffet-menu
+ ('built-in (theme-buffet-built-in))
+ ('modus-ef (theme-buffet-modus-ef))
+ ('end-user (theme-buffet-end-user))
+ (_ (user-error "`theme-buffet-menu' isn't passing the health
inspections as it is!"))))
+ (cancel-function-timers #'theme-buffet--load-random)))
+
+
+(provide 'theme-buffet)
+;;; theme-buffet.el ends here