diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..751a0c4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: guidoschmidt +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..3ae82a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to report a bug in circadian.el +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - OS: [e.g. macOS 14, Windows 11, Ubuntu etc.] + - Emacs: [e.g. emacs-plus 30.0.50] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 172fe48..c4c8ab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,24 +6,24 @@ jobs: strategy: matrix: emacs-version: - - 26.3 - 27.2 - 28.2 + - 29.3 - snapshot steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: purcell/setup-emacs@master with: version: ${{ matrix.emacs-version }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: cache-cask-packages with: path: .cask key: cache-cask-packages-000 - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: cache-cask-executable with: path: ~/.cask diff --git a/README.md b/README.md index 318c242..45eac3a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

Theme-switching for Emacs based on daytime

-### Conception +## Conception Circadian tries to help reducing eye strain that may arise from difference of your display brightness and the @@ -31,9 +31,63 @@ adaption software like: --- -### Example usage +## Usage & Configuration +Install circadian.el with +[use-package](https://www.gnu.org/software/emacs/manual/html_mono/use-package.html) +or [straight.el](https://github.com/radian-software/straight.el) -##### Switching themes on time of day +### Configuration with times +To auto-switch a theme on a specific time, use time strings: + +```elisp +(use-package circadian + :ensure t + :config + (setq circadian-themes '(("8:00" . wombat) + ("19:30" . adwaita))) + (circadian-setup)) +``` + + +### Configuration with `:sunrise` and `:sunset` +To auto-switch a theme based on your current locations sunrise and sunset times: + +1. Make sure to set your latitude and longitude (Get them e.g. at + [latlong.net](https://www.latlong.net/)): + ```elisp + (setq calendar-latitude 40.712776) + (setq calendar-longitude -74.005974) + ``` +2. Configure `circadian-themes` using the `:sunset` and `:sunset` + ```elisp + (use-package circadian + :ensure t + :config + (setq circadian-themes '((:sunrise . adwaita) + (:sunset . wombat))) + (circadian-setup)) + ``` + + +### Randomly selection from theme list +Circadian.el can randomly select a theme from a given list, e.g here using [doom-themes](https://github.com/doomemacs/themes) at sunset: + +```elisp +(use-package doom-themes) + +(use-package circadian + :config + (setq circadian-themes '((:sunrise . doom-gruvbox-light) + (:sunset . (doom-dracula doom-gruvbox)))) + (add-hook 'emacs-startup-hook #'circadian-setup) + (circadian-setup)) +``` + + +### Use with custom themes +To use custom themes, install them from MELPA using +e.g. [use-package](https://www.gnu.org/software/emacs/manual/html_mono/use-package.html) +or [straight.el](https://github.com/radian-software/straight.el). Example usage featuring [hemera-themes](https://github.com/GuidoSchmidt/emacs-hemera-theme) and [nyx-theme](https://github.com/GuidoSchmidt/emacs-nyx-theme) (with use-package). Make sure @@ -45,33 +99,16 @@ to use `:defer` keyword. Omitting it may lead to broken colors ;; make sure to use :defer keyword (use-package hemera-theme :ensure :defer) (use-package nyx-theme :ensure :defer) - -(use-package circadian - :ensure t - :config - (setq circadian-themes '(("8:00" . hemera) - ("19:30" . nyx))) - (circadian-setup)) ``` -##### Switching themes on sunrise & sunset -Be sure to set your latitude and longitude (Get them e.g. at [latlong.net](https://www.latlong.net/)): +### Verbose messages +By default circadian will not log any messages. however for development or just +getting more information, one can enable a more verbose message log: ```elisp -;; Install additinal themes from melpa -;; make sure to use :defer keyword -(use-package apropospriate-theme :ensure :defer) -(use-package nord-theme :ensure :defer) +(setq circadian-verbose t) -(use-package circadian - :ensure t - :config - (setq calendar-latitude 49.0) - (setq calendar-longitude 8.5) - (setq circadian-themes '((:sunrise . apropospriate-light) - (:sunset . nord))) - (circadian-setup)) ``` --- @@ -107,7 +144,7 @@ e.g. I like to override any themes cursor color to a very bright color via: --- -### Development +### Development & Testing Install Emacs [cask](https://github.com/cask/cask) environment. On macOS you canr use [homebrew](https://brew.sh/) with: `brew install cask`. diff --git a/circadian.el b/circadian.el index 6693e0c..4574be6 100644 --- a/circadian.el +++ b/circadian.el @@ -5,9 +5,9 @@ ;; Author: Guido Schmidt ;; Maintainer: Guido Schmidt ;; URL: https://github.com/GuidoSchmidt/circadian -;; Version: 0.3.3 +;; Version: 1.0.0 ;; Keywords: themes -;; Package-Requires: ((emacs "24.4")) +;; Package-Requires: ((emacs "27.2")) ;; This file is part of GNU Emacs. @@ -44,6 +44,16 @@ (require 'cl-lib) (require 'solar) +(defcustom circadian-verbose nil + "Timer to execute on next theme switch." + :type 'boolean + :group 'circadian) + +(defcustom circadian-next-timer nil + "Timer to execute on next theme switch." + :type 'timer + :group 'circadian) + (defcustom circadian-before-load-theme-hook nil "Functions to run before the theme is changed." :type 'hook @@ -62,36 +72,67 @@ (defun circadian-enable-theme (theme) "Clear previous `custom-enabled-themes' and load THEME." - (unless (equal (list theme) custom-enabled-themes) - ;; Only load the argument theme, when `custom-enabled-themes' - ;; does not contain it. - (mapc #'disable-theme custom-enabled-themes) - (condition-case nil - (progn - (run-hook-with-args 'circadian-before-load-theme-hook theme) - (load-theme theme t) - (let ((time (circadian-now-time))) - (message "circadian.el → Enabled %s theme @ %02d:%02d:%02d" - theme (nth 0 time) (nth 1 time) (nth 2 time))) - (run-hook-with-args 'circadian-after-load-theme-hook theme)) - (error "ERROR: circadian.el → Problem loading theme %s" theme)))) - -(defun circadian--encode-time (hour min) + ;; Only load the argument theme, when `custom-enabled-themes' + ;; does not contain it. + + (condition-case nil + (progn + (run-hook-with-args 'circadian-before-load-theme-hook theme) + + (if (equal nil (member theme custom-enabled-themes)) + (progn + (mapc #'disable-theme custom-enabled-themes) + (load-theme theme t) + (if (not (equal nil circadian-next-timer)) + (cancel-timer circadian-next-timer)) + (setq circadian-next-timer nil) + (if circadian-verbose + (message "[circadian.el] → Enabled %s theme @ %s" + theme + (format-time-string "%H:%M:%S %Z")))) + (progn + (if circadian-verbose + (message "[circadian.el] → %s already enabled" + theme)))) + + (circadian-schedule) + + (run-hook-with-args 'circadian-after-load-theme-hook theme)) + (error "[circadian.el/ERROR] → Problem loading theme %s" theme))) + +(defun circadian-encode-time (hour min) "Encode HOUR hours and MIN minutes into a valid format for `run-at-time'." - (let ((now (decode-time))) - (let ((day (nth 3 now)) - (month (nth 4 now)) - (year (nth 5 now)) - (zone (current-time-zone))) - (encode-time 0 min hour day month year zone)))) + (let* ((now (decode-time)) + (is-earlier (circadian-a-earlier-b-p (list hour min) (list (nth 2 now) (nth 1 now)))) + (tomorrow (decode-time (+ (* 24 60 60) + (time-to-seconds (current-time))))) + (day (if is-earlier + (nth 3 tomorrow) + (nth 3 now))) + (month (if is-earlier + (nth 4 now) + (nth 4 tomorrow))) + (year (if is-earlier + (nth 5 now) + (nth 5 tomorrow)))) + (encode-time 0 min hour day month year nil -1 nil))) (defun circadian-themes-parse () - "Parse `circadian-themes' and sort by time." + "Parse `circadian-themes', filter the list and sort it by time. +Uses `circadian-check-calendar' to filter out entries which use `:sunrise' +or `:sunset' if either `calendar-latitude' or `calendar-longitude' is not +set and and sort the final list by time." (sort - (mapcar - (lambda (entry) - (cons (circadian-match-sun (cl-first entry)) (cdr entry))) - circadian-themes) + (mapcar + (lambda (entry) + (cons (circadian-match-sun (cl-first entry)) (cdr entry))) + (seq-filter + (lambda (entry) + (if (equal nil (circadian-check-calendar)) + (and (not (equal :sunrise (cl-first entry))) + (not (equal :sunset (cl-first entry)))) + t)) + circadian-themes)) (lambda (a b) (circadian-a-earlier-b-p (car a) (car b))))) ;;; --- TIME COMPARISONS @@ -112,51 +153,109 @@ (not (circadian-a-earlier-b-p theme-time now-time)))) theme-list)) -(defun circadian-activate-latest-theme () - "Check which themes are overdue to be activated and load the last." - (interactive) +(defun circadian-activate-current () + "Check which theme should be active currently and return a time for the next run." + (let* ((themes (circadian-themes-parse)) + (now (circadian-now-time)) + (past-themes (circadian-filter-inactivate-themes themes now)) + (entry (car (last (or past-themes themes)))) + (theme-or-theme-list (cdr entry)) + (theme (if (listp theme-or-theme-list) + (progn + (nth (random (length theme-or-theme-list)) theme-or-theme-list)) + theme-or-theme-list))) + (circadian-enable-theme theme))) + +(defun circadian-schedule() + "Schedule the next timer for circadian." + (random (format-time-string "%H:%M" (decode-time))) (let* ((themes (circadian-themes-parse)) (now (circadian-now-time)) (past-themes (circadian-filter-inactivate-themes themes now)) (entry (car (last (or past-themes themes)))) - (theme (cdr entry)) (next-entry (or (cadr (member entry themes)) (if (circadian-a-earlier-b-p (circadian-now-time) (cl-first entry)) - (car themes)))) - (next-time (if next-entry - (circadian--encode-time - (cl-first (cl-first next-entry)) - (cl-second (cl-first next-entry))) - (+ (* (+ (- 23 (cl-first now)) (cl-first (cl-first (cl-first themes)))) 60 60) - (* (+ (- 60 (cl-second now)) (cl-second (cl-first (cl-first themes)))) 60))))) - (circadian-enable-theme theme) - (cancel-function-timers #'circadian-activate-latest-theme) - (run-at-time next-time nil #'circadian-activate-latest-theme))) + (car themes) + (cl-first past-themes)))) + (next-theme-or-theme-list (cdr next-entry)) + (next-theme (if (listp next-theme-or-theme-list) + (progn + (nth (random (length next-theme-or-theme-list)) next-theme-or-theme-list)) + next-theme-or-theme-list)) + (next-time (circadian-encode-time + (cl-first (cl-first next-entry)) + (cl-second (cl-first next-entry))))) + (if (equal nil circadian-next-timer) + (progn (setq circadian-next-timer + (run-at-time + next-time + nil + #'circadian-enable-theme next-theme)) + (if circadian-verbose + (message "[circadian.el] → Next theme %s @ %s" + (if (listp next-theme) + (concat "one of " (format "%s" next-theme)) + next-theme) + (format-time-string "%H:%M:%S %Z" next-time))))))) ;; --- Sunset-sunrise -(defun circadian--frac-to-time (f) +(defun circadian-frac-to-time (f) "Convert fractional time F to (HH MM)." (let ((l (cl-floor f))) (list (cl-first l) (floor (* 60 (cl-second l)))))) +(defun circadian-check-calendar () + "Check if either calendar-latitude or calendar-longitude is not set." + ;; 1. Check `calendar-latitude' ond message user if it's not set. + (if (and circadian-verbose (equal nil calendar-latitude)) + (message "calendar-latitude not set. Consider using fixed time strings, e.g. + +(setq circadian-themes '((\"9:00\" . wombat) + (\"20:00\" . tango)) + +or set calendar-latitude: + (setq calendar-latitude 49.0)")) + + ;; 2. Check `calendar-longitude' ond message user if it's not set. + (if (and circadian-verbose (equal nil calendar-longitude)) + (message "calendar-longitude not set. Consider using fixed time strings, e.g. + +(setq circadian-themes '((\"9:00\" . wombat) + (\"20:00\" . tango)) + +or set calendar-longitude: + (setq calendar-longitude 8.5)")) + + (cond ((equal nil calendar-latitude) + (progn + nil)) + + ((equal nil calendar-longitude) + (progn + nil)) + + (t))) + (defun circadian-sunrise () "Get clean sunrise time string from Emacs' `sunset-sunrise'`." (let ((solar-result (solar-sunrise-sunset (calendar-current-date)))) (let ((sunrise-numeric (cl-first (cl-first solar-result)))) (if (equal nil sunrise-numeric) - (error "No valid sunrise from solar-sunrise-sunset, consider using fixed time strings, e.g. (setq circadian-themes '((\"9:00\" . wombat) (\"20:00\" . tango)))") - (circadian--frac-to-time sunrise-numeric))))) + (if circadian-verbose + (message "[circadian.el/ERROR] No valid sunrise from solar-sunrise-sunset, consider using fixed time strings, e.g. (setq circadian-themes '((\"9:00\" . wombat) (\"20:00\" . tango)))")) + (circadian-frac-to-time sunrise-numeric))))) (defun circadian-sunset () "Get clean sunset time string from Emacs' `sunset-sunrise'`." (let ((solar-result (solar-sunrise-sunset (calendar-current-date)))) (let ((sunset-numeric (cl-first (cl-second solar-result)))) (if (equal nil sunset-numeric) - (error "No valid sunset from solar-sunrise-sunset, consider using fixed time strings, e.g. (setq circadian-themes '((\"9:00\" . wombat) (\"20:00\" . tango)))") - (circadian--frac-to-time sunset-numeric))))) + (if circadian-verbose + (message "[circadian.el/ERROR] No valid sunset from solar-sunrise-sunset, consider using fixed time strings, e.g. (setq circadian-themes '((\"9:00\" . wombat) (\"20:00\" . tango)))")) + (circadian-frac-to-time sunset-numeric))))) -(defun circadian--string-to-time (input) +(defun circadian-string-to-time (input) "Parse INPUT string to `(HH MM)'." (cl-map 'list #'string-to-number (split-string input ":"))) @@ -164,21 +263,34 @@ "Match INPUT to a case for setting up timers." (cond ((cl-equalp input :sunrise) (let ((sunrise (circadian-sunrise))) - (if (equal sunrise "not") - (error "Could not get valid sunset time — check your time zone settings")) - (circadian-sunrise))) + (if (equal sunrise nil) + (if circadian-verbose + (message "[circadian.el/ERROR] Could not get valid sunset time — check your time zone settings"))) + sunrise)) + ((cl-equalp input :sunset) (let ((sunset (circadian-sunset))) - (if (equal sunset "on") - (error "Could not get valid sunset time — check your time zone settings")) - (circadian-sunset))) - ((stringp input) (circadian--string-to-time input)))) + (if (equal sunset nil) + (if circadian-verbose + (message "[circadian.el/ERROR] Could not get valid sunset time — check your time zone settings"))) + sunset)) + + ((stringp input) (circadian-string-to-time input)))) ;;;###autoload (defun circadian-setup () "Setup circadian based on `circadian-themes'." (interactive) - (circadian-activate-latest-theme)) + (circadian-activate-current) + (circadian-schedule)) + +(defun circadian-stop () + "Stop `circadian-next-timer' - To re-schedule, call `circadian-setup' again." + (interactive) + (if (not (equal nil circadian-next-timer)) + (progn + (cancel-timer circadian-next-timer) + (setq circadian-next-timer nil)))) (provide 'circadian) ;;; circadian.el ends here diff --git a/test.el b/test.el index f8b2bff..a03cfd4 100644 --- a/test.el +++ b/test.el @@ -15,6 +15,7 @@ ("23:59" . adwaita))) (setq circadian-themes-parsed (circadian-themes-parse)) + ;; Before 5:01 (let ((time-now '(4 10))) (should (equal 0 (length (circadian-filter-inactivate-themes @@ -22,8 +23,9 @@ time-now))))) (with-mock (stub circadian-now-time => '(5 0 0)) - (circadian-activate-latest-theme) - (should (equal 'adwaita (cl-first custom-enabled-themes)))) + (circadian-setup) + + (should (equal (list 'adwaita) custom-enabled-themes))) ;; After 5:01, before 14:47 (let ((time-now '(5 2))) @@ -32,8 +34,8 @@ time-now))))) (with-mock (stub circadian-now-time => '(5 2 0)) - (circadian-activate-latest-theme) - (should (equal 'wombat (cl-first custom-enabled-themes)))) + (circadian-setup) + (should (equal (list 'wombat) custom-enabled-themes))) ;; After 14:47, before 23:59 (let ((time-now '(14 47))) @@ -42,8 +44,8 @@ time-now))))) (with-mock (stub circadian-now-time => '(14 47 1)) - (circadian-activate-latest-theme) - (should (equal 'tango (cl-first custom-enabled-themes)))) + (circadian-setup) + (should (equal (list 'tango) custom-enabled-themes))) ;; After 23:59 (let ((time-now '(23 59))) @@ -52,8 +54,8 @@ time-now))))) (with-mock (stub circadian-now-time => '(23 59 15)) - (circadian-activate-latest-theme) - (should (equal 'adwaita (cl-first custom-enabled-themes)))) + (circadian-setup) + (should (equal (list 'adwaita) custom-enabled-themes))) ;; Surpassing midnight (let ((time-now '(0 2))) @@ -62,45 +64,41 @@ time-now))))) (with-mock (stub circadian-now-time => '(0 2 10)) - (circadian-activate-latest-theme) - (should (equal 'adwaita (cl-first custom-enabled-themes))))) + (circadian-setup) + (should (equal (list 'adwaita) custom-enabled-themes)))) -(ert-deftest test-circadian-activate-latest-theme () - "Test `circadian-activate-latest-theme' used in `circadian-setup'." - ;; (print "→ TEST: circadian-activate-latest-theme") +(ert-deftest test-circadian-setup () + "Test `circadian-setup'." (setq circadian-themes '(("7:00" . wombat) ("16:00" . tango))) (with-mock (stub circadian-now-time => '(7 21 0)) - (circadian-activate-latest-theme) - (should (equal 'wombat (cl-first custom-enabled-themes)))) + (circadian-setup) + (should (equal (list 'wombat) custom-enabled-themes))) (with-mock (stub circadian-now-time => '(17 0 0)) - (circadian-activate-latest-theme) - (should (equal 'tango (cl-first custom-enabled-themes))))) + (circadian-setup) + (should (equal (list 'tango) custom-enabled-themes)))) (ert-deftest test-circadian-sunrise-sunset () "Test :sunrise and :sunset keywords for theme switching. @TODO currently failing, needs a fix" - (setq calendar-latitude 49.329896) - (setq calendar-longitude 8.570925) - (setq circadian-themes '((:sunrise . wombat) - (:sunset . adwaita))) - (circadian-setup) - (with-mock - (stub circadian-now-time => '(14 21 0)) - (circadian-activate-latest-theme) - (should (equal 'wombat (cl-first custom-enabled-themes)))) + (setq calendar-latitude 49.329896) + (setq calendar-longitude 8.570925) + (setq circadian-themes '((:sunrise . adwaita) + (:sunset . wombat))) + (stub circadian-now-time => '(14 21 0)) + (circadian-setup) + (should (equal 'adwaita (cl-first custom-enabled-themes))) - (with-mock - (stub circadian-now-time-string => '(16 50 0)) - (circadian-activate-latest-theme) - (should (equal 'adwaita (cl-first custom-enabled-themes))))) + (stub circadian-now-time => '(22 30 0)) + (circadian-setup) + (should (equal 'wombat (cl-first custom-enabled-themes))))) @@ -121,10 +119,38 @@ Time A: 17:59 Time B: 17:58 B should be earlier than A => `circadian-a-earlier-b-p' should return t." - ;; (print "→ TEST: time comparisons") + ;; ( (should (equal t (circadian-a-earlier-b-p '(7 50) '(7 51)))) (should (equal nil (circadian-a-earlier-b-p '(19 20) '(19 19)))) - (should (equal t (circadian-a-earlier-b-p '(20 20) '(20 20))))) + (should (equal t (circadian-a-earlier-b-p '(20 20) '(20 20)))) + + (with-mock + ;; tomorrow + (stub decode-time => '(0 30 16 28 4 2024 nil -1 nil)) + (let* ((next-time (decode-time (circadian-encode-time 0 0))) + (next-day (nth 3 next-time)) + (next-hour (nth 2 next-time))) + ;; today + (stub decode-time => '(0 30 14 28 4 2024 nil -1 nil)) + (let* ((now (decode-time)) + (day (nth 3 now)) + (hour (nth 2 now))) + (should (> next-hour hour)) + (should (equal day next-day))))) + + (with-mock + ;; tomorrow + (stub decode-time => '(0 30 7 29 4 2024 nil -1 nil)) + (let* ((next-time (decode-time (circadian-encode-time 0 0))) + (next-day (nth 3 next-time)) + (next-hour (nth 2 next-time))) + ;; today + (stub decode-time => '(0 30 14 28 4 2024 nil -1 nil)) + (let* ((now (decode-time)) + (day (nth 3 now)) + (hour (nth 2 now))) + (should (< next-hour hour)) + (should (equal (+ 1 day) next-day)))))) @@ -146,7 +172,7 @@ B should be earlier than A https://github.com/guidoschmidt/circadian.el/issues/27" (setq calendar-latitude 79.482623) (setq calendar-longitude 5.318703) - (setq circadian-themes '((:sunrise . wombat) + (setq circadian-themes '((:sunrise . adwaita) (:sunset . tango))) (circadian-setup)) @@ -154,12 +180,12 @@ https://github.com/guidoschmidt/circadian.el/issues/27" (defvar test-order '(member test-circadian-filter-and-activate-themes - test-circadian-activate-latest-theme + test-circadian-setup test-circadian-sunrise-sunset - test-circadian-sunrise-sunset-timezones test-circadian-time-comparisons test-circadian-setup-benchmark - test-circadian-invalid-solar-sunrise-sunset)) + test-circadian-invalid-solar-sunrise-sunset + test-circadian-sunrise-sunset-timezones)) (provide 'circadian.el-test) ;;; circadian.el-test.el ends here