branch: elpa/casual
commit 8588b9e5cf2263960e3ffd1fd91b9494c078d409
Merge: 006b3d4ba1 b7aeefece3
Author: Charles Choi <[email protected]>
Commit: GitHub <[email protected]>
Merge pull request #219 from
kickingvegas/merge-development-to-main-20250601_134840
Merge development to main 20250601_134840
---
README.org | 7 +
docs/casual.info | 145 ++++--
docs/casual.org | 60 ++-
docs/images/casual-editkit-main-screenshot.png | Bin 228117 -> 295134 bytes
docs/images/casual-editkit-tools-screenshot.png | Bin 205798 -> 130880 bytes
docs/images/casual-timezone-planner-screenshot.png | Bin 0 -> 486962 bytes
docs/images/casual-timezone-tmenu-screenshot.png | Bin 0 -> 112513 bytes
docs/timezone.org | 51 +++
lisp/Makefile | 7 +-
lisp/Makefile-timezone.make | 30 ++
lisp/casual-calc-time.el | 28 +-
lisp/casual-editkit-utils.el | 34 +-
lisp/casual-editkit.el | 6 +-
lisp/casual-timezone-settings.el | 142 ++++++
lisp/casual-timezone-utils.el | 490 +++++++++++++++++++++
lisp/casual-timezone.el | 44 ++
lisp/casual.el | 2 +-
tests/Makefile | 9 +-
tests/casual-timezone-test-utils.el | 40 ++
tests/test-casual-calc-time.el | 2 +
tests/test-casual-editkit-utils.el | 32 +-
tests/test-casual-editkit.el | 7 +-
tests/test-casual-timezone-settings.el | 61 +++
tests/test-casual-timezone-utils.el | 144 ++++++
tests/test-casual-timezone.el | 50 +++
25 files changed, 1324 insertions(+), 67 deletions(-)
diff --git a/README.org b/README.org
index f92bd3e53a..841dd37649 100644
--- a/README.org
+++ b/README.org
@@ -43,6 +43,7 @@ Editorially, all design decisions for Casual are ultimately
the opinion of Charl
- [[#i-search-elisp-library-casual-isearch][I-Search (Elisp library:
~casual-isearch~)]]
- [[#make-elisp-library-casual-make][Make (Elisp library: ~casual-make~)]]
- [[#re-builder-elisp-library-casual-re-builder][Re-Builder (Elisp library:
~casual-re-builder~)]]
+ - [[#timezone-elisp-library-casual-timezone][Timezone (Elisp library:
~casual-timezone~)]]
- [[#requirements][Requirements]]
- [[#install][Install]]
- [[#upgrading-to-casual-2x][Upgrading to Casual 2.x]]
@@ -124,6 +125,12 @@ An interface for the Emacs regular expression tool.
Users can choose any or all of the user interfaces made available by Casual at
their pleasure.
+** [[file:docs/timezone.org][Timezone]] (Elisp library: ~casual-timezone~)
+Casual Timezone is a library of commands to work with different time zones.
Answer the questions "what time is it over there?" or conversely "what is the
time over there, here?" with ease using this.
+
+Casual Timezone only supports systems that have a
[[https://en.wikipedia.org/wiki/Tz_database][tz database]].
+
+[[file:docs/images/casual-timezone-planner-screenshot.png]]
* Requirements
Casual requires usage of
diff --git a/docs/casual.info b/docs/casual.info
index 3cb201b4eb..072aa56b16 100644
--- a/docs/casual.info
+++ b/docs/casual.info
@@ -11,7 +11,7 @@ File: casual.info, Node: Top, Next: Motivations, Up: (dir)
Casual User Guide
*****************
-Version: 2.4.1
+Version: 2.5.0
Casual is a project to re-imagine the primary user interface for Emacs
using keyboard-driven menus.
@@ -67,6 +67,7 @@ Casual Modes
* I-Search::
* Make::
* RE-Builder::
+* Timezone::
File: casual.info, Node: Motivations, Next: Requirements, Prev: Top, Up:
Top
@@ -212,6 +213,7 @@ The following modes are supported by Casual:
* I-Search::
* Make::
* RE-Builder::
+* Timezone::
File: casual.info, Node: Agenda, Next: Bookmarks, Up: Casual Modes
@@ -1008,7 +1010,7 @@ References
• *note Automatic Variables: (make)Automatic Variables.
-File: casual.info, Node: RE-Builder, Prev: Make, Up: Casual Modes
+File: casual.info, Node: RE-Builder, Next: Timezone, Prev: Make, Up:
Casual Modes
4.12 RE-Builder
===============
@@ -1099,6 +1101,91 @@ References
• *note Regular Expressions: (elisp)Regular Expressions.
+
+File: casual.info, Node: Timezone, Prev: RE-Builder, Up: Casual Modes
+
+4.13 Timezone
+=============
+
+Casual Timezone is a library of commands to work with different time
+zones. Answer the questions "what time is it over there?" or
+conversely "what is the time over there, here?" with ease using this.
+Its top level library is ‘casual-timezone’. Commands from Casual
+Timezone are found in the menu ‘casual-timezone-tmenu’, which itself is
+integrated into the menu ‘casual-editkit-tools-tmenu’.
+
+Casual Timezone only supports systems that have a tz database
+(https://en.wikipedia.org/wiki/Tz_database).
+
+Configuration
+=============
+
+Casual Timezone is configured as part of Casual EditKit in the Tools
+menu (‘casual-editkit-tools-tmenu’). Refer to the EditKit Install
+section for instructions on how to install it.
+
+The main menu for Casual Timezone is ‘casual-timezone-tmenu’ for users
+who wish to access it directly.
+
+For more info, *note Casual EditKit: EditKit.
+
+Usage
+=====
+
+Casual Timezone offers the following commands in the menu
+‘casual-timezone-tmenu’.
+
+ • ‘casual-timezone-local-time-to-remote’ (menu binding: ‘l’) will
+ convert a local date to its equivalent in remote time zone.
+
+ • ‘casual-timezone-remote-time-to-local’ (menu binding: ‘r’) will
+ convert a date in a remote time zone to its local equivalent.
+
+ • ‘casual-timezone-planner’ (menu binding: ‘z’) will generate a table
+ comparing hours between the local and a remote timezone on a
+ certain date.
+
+Formatting
+==========
+
+The formatted representation of time in Casual Timezone is set by the
+following customizable variables:
+
+ • ‘casual-timezone-datestamp-format’
+ • ‘casual-timezone-convert-datestamp-format’
+
+The format specification of these variables conforms to the
+specification defined in *note format-time-string: (elisp)Time Parsing.
+
+The following table shows which format variable applies to which
+command.
+
+Command Format
+--------------------------------------------------------------------------------------
+‘casual-timezone-planner’ ‘casual-timezone-datestamp-format’
+‘casual-timezone-local-time-to-remote’
‘casual-timezone-convert-datestamp-format’
+‘casual-timezone-remote-time-to-local’
‘casual-timezone-convert-datestamp-format’
+
+These variables can be customized via the Transient menu
+‘casual-timezone-settings-tmenu’.
+
+Planner Configuration
+=====================
+
+The following variables can control how working hours are displayed in
+the timezone planner.
+
+ • ‘casual-timezone-working-hours-range’ will set the range (start,
+ stop) of working hours. The values are integers that map to
+ 24-hour time (0..23).
+ • ‘casual-timezone-working-hour-glyph’ will set the glyph used to
+ denote a working hour (default is ☼).
+ • ‘casual-timezone-planner-working-highlight’ will set the face used
+ to highlight a working hour.
+
+These variables can be customized via the Transient menu
+‘casual-timezone-settings-tmenu’.
+
File: casual.info, Node: UX Conventions, Next: Customization, Prev: Casual
Modes, Up: Top
@@ -1294,6 +1381,11 @@ File: casual.info, Node: Index, Next: Variable Index,
Prev: Acknowledgments,
* RE-Builder Usage: RE-Builder. (line 21)
* Requirements: Requirements. (line 6)
* Sponsorship: Sponsorship. (line 6)
+* Timezone: Timezone. (line 6)
+* Timezone Configuration: Timezone. (line 19)
+* Timezone Formatting: Timezone. (line 47)
+* Timezone Planner Configuration: Timezone. (line 71)
+* Timezone Usage: Timezone. (line 31)
* Transient Conventions: Transient Conventions.
(line 6)
* UX Conventions: UX Conventions. (line 6)
@@ -1313,30 +1405,31 @@ File: casual.info, Node: Variable Index, Prev: Index,
Up: Top
Tag Table:
Node: Top228
-Node: Motivations1923
-Node: Requirements3416
-Node: Transient Conventions3680
-Node: Casual Modes5408
-Node: Agenda6400
-Node: Bookmarks7957
-Node: Calc10232
-Node: Calendar13373
-Node: Dired14721
-Node: EditKit17946
-Node: IBuffer19670
-Node: Image21735
-Node: Info22976
-Node: I-Search24442
-Node: Make25603
-Node: RE-Builder27987
-Node: UX Conventions31394
-Node: Customization34095
-Node: Feedback & Discussion34469
-Node: Sponsorship34887
-Node: About35181
-Node: Acknowledgments35458
-Node: Index35840
-Node: Variable Index39742
+Node: Motivations1936
+Node: Requirements3429
+Node: Transient Conventions3693
+Node: Casual Modes5421
+Node: Agenda6426
+Node: Bookmarks7983
+Node: Calc10258
+Node: Calendar13399
+Node: Dired14747
+Node: EditKit17972
+Node: IBuffer19696
+Node: Image21761
+Node: Info23002
+Node: I-Search24468
+Node: Make25629
+Node: RE-Builder28013
+Node: Timezone31437
+Node: UX Conventions34567
+Node: Customization37268
+Node: Feedback & Discussion37642
+Node: Sponsorship38060
+Node: About38354
+Node: Acknowledgments38631
+Node: Index39013
+Node: Variable Index43280
End Tag Table
diff --git a/docs/casual.org b/docs/casual.org
index a980dfaaec..4b7d9ca2bf 100644
--- a/docs/casual.org
+++ b/docs/casual.org
@@ -6,7 +6,7 @@
#+OPTIONS: ':t toc:t author:t email:t compact-itemx:t
#+LANGUAGE: en
-#+MACRO: version 2.4.1
+#+MACRO: version 2.5.0
#+TEXINFO_FILENAME: casual.info
#+TEXINFO_HEADER: @syncodeindex pg cp
@@ -804,6 +804,64 @@ Select (q) *Quit* to exit the RE-Builder tool.
- [[info:elisp#Regular Expressions][Regular Expressions]]
+** Timezone
+#+CINDEX: Timezone
+
+Casual Timezone is a library of commands to work with different time zones.
Answer the questions "what time is it over there?" or conversely "what is the
time over there, here?" with ease using this. Its top level library is
~casual-timezone~. Commands from Casual Timezone are found in the menu
~casual-timezone-tmenu~, which itself is integrated into the menu
~casual-editkit-tools-tmenu~.
+
+Casual Timezone only supports systems that have a
[[https://en.wikipedia.org/wiki/Tz_database][tz database]].
+
+#+TEXINFO: @unnumberedsec Configuration
+#+CINDEX: Timezone Configuration
+
+Casual Timezone is configured as part of Casual EditKit in the Tools menu
(~casual-editkit-tools-tmenu~). Refer to the EditKit Install section for
instructions on how to install it.
+
+The main menu for Casual Timezone is ~casual-timezone-tmenu~ for users who
wish to access it directly.
+
+For more info, [[*EditKit][Casual EditKit]].
+
+#+TEXINFO: @unnumberedsec Usage
+#+CINDEX: Timezone Usage
+
+Casual Timezone offers the following commands in the menu
~casual-timezone-tmenu~.
+
+- ~casual-timezone-local-time-to-remote~ (menu binding: ~l~) will convert a
local date to its equivalent in remote time zone.
+
+- ~casual-timezone-remote-time-to-local~ (menu binding: ~r~) will convert a
date in a remote time zone to its local equivalent.
+
+- ~casual-timezone-planner~ (menu binding: ~z~) will generate a table
comparing hours between the local and a remote timezone on a certain date.
+
+#+TEXINFO: @unnumberedsec Formatting
+#+CINDEX: Timezone Formatting
+
+The formatted representation of time in Casual Timezone is set by the
following customizable variables:
+
+- ~casual-timezone-datestamp-format~
+- ~casual-timezone-convert-datestamp-format~
+
+The format specification of these variables conforms to the specification
defined in [[info:elisp#Time Parsing][format-time-string]].
+
+The following table shows which format variable applies to which command.
+
+| Command | Format
|
+|--------------------------------------+------------------------------------------|
+| ~casual-timezone-planner~ | ~casual-timezone-datestamp-format~
|
+| ~casual-timezone-local-time-to-remote~ |
~casual-timezone-convert-datestamp-format~ |
+| ~casual-timezone-remote-time-to-local~ |
~casual-timezone-convert-datestamp-format~ |
+
+These variables can be customized via the Transient menu
~casual-timezone-settings-tmenu~.
+
+#+TEXINFO: @unnumberedsec Planner Configuration
+#+CINDEX: Timezone Planner Configuration
+
+The following variables can control how working hours are displayed in the
timezone planner.
+
+- ~casual-timezone-working-hours-range~ will set the range (start, stop) of
working hours. The values are integers that map to 24-hour time (0..23).
+- ~casual-timezone-working-hour-glyph~ will set the glyph used to denote a
working hour (default is ☼).
+- ~casual-timezone-planner-working-highlight~ will set the face used to
highlight a working hour.
+
+These variables can be customized via the Transient menu
~casual-timezone-settings-tmenu~.
+
* UX Conventions
#+CINDEX: UX Conventions
diff --git a/docs/images/casual-editkit-main-screenshot.png
b/docs/images/casual-editkit-main-screenshot.png
index 42cb971bfb..fb5d9e948d 100644
Binary files a/docs/images/casual-editkit-main-screenshot.png and
b/docs/images/casual-editkit-main-screenshot.png differ
diff --git a/docs/images/casual-editkit-tools-screenshot.png
b/docs/images/casual-editkit-tools-screenshot.png
index ddb159fa8f..52854e213b 100644
Binary files a/docs/images/casual-editkit-tools-screenshot.png and
b/docs/images/casual-editkit-tools-screenshot.png differ
diff --git a/docs/images/casual-timezone-planner-screenshot.png
b/docs/images/casual-timezone-planner-screenshot.png
new file mode 100644
index 0000000000..987ba5a000
Binary files /dev/null and b/docs/images/casual-timezone-planner-screenshot.png
differ
diff --git a/docs/images/casual-timezone-tmenu-screenshot.png
b/docs/images/casual-timezone-tmenu-screenshot.png
new file mode 100644
index 0000000000..61c767d2bf
Binary files /dev/null and b/docs/images/casual-timezone-tmenu-screenshot.png
differ
diff --git a/docs/timezone.org b/docs/timezone.org
new file mode 100644
index 0000000000..10bf07d1db
--- /dev/null
+++ b/docs/timezone.org
@@ -0,0 +1,51 @@
+[[../README.org][❮ Back to Casual]]
+
+* Casual Timezone
+
+Casual Timezone is a library of commands to work with different time zones.
Answer the questions "what time is it over there?" or conversely "what is the
time over there, here?" with ease using this. Its top level library is
~casual-timezone~. Commands from Casual Timezone are found in the menu
~casual-timezone-tmenu~, which itself is integrated into the menu
~casual-editkit-tools-tmenu~.
+
+Casual Timezone only supports systems that have a
[[https://en.wikipedia.org/wiki/Tz_database][tz database]].
+
+[[file:images/casual-timezone-planner-screenshot.png]]
+
+* Install
+
+Casual Timezone is configured as part of [[file:editkit.org][Casual Editkit]]
in the Tools menu (~casual-editkit-tools-tmenu~). Refer to the
[[file:editkit.org::*Install][EditKit Install]] section for instructions on how
to install it.
+
+The main menu for Casual Timezone is ~casual-timezone-tmenu~ for users who
wish to access it directly.
+
+* Usage
+** Basic Usage
+
+Casual Timezone offers the following commands:
+
+- ~casual-timezone-local-time-to-remote~ (menu binding: ~l~) will convert a
local date to its equivalent in remote time zone.
+
+- ~casual-timezone-remote-time-to-local~ (menu binding: ~r~) will convert a
date in a remote time zone to its local equivalent.
+
+- ~casual-timezone-planner~ (menu binding: ~z~) will generate a table
comparing hours between the local and a remote timezone on a certain date.
+
+These commands are offered in the menu ~casual-timezone-tmenu~ shown below.
+
+[[file:images/casual-timezone-tmenu-screenshot.png]]
+
+** Unicode Symbol Support
+By enabling “Use Unicode Symbols” from the Settings menu, Casual Timezone will
use Unicode symbols as appropriate in its menus.
+
+* Sponsorship
+If you enjoy using Casual Timezone, consider making a modest financial
contribution to help support its development and maintenance.
+
+[[https://www.buymeacoffee.com/kickingvegas][file:images/default-yellow.png]]
+
+* See Also
+- [[file:agenda.org][Agenda]]
+- [[file:bookmarks.org][Bookmarks]]
+- [[file:calc.org][Calc]]
+- [[file:calendar.org][Calendar]]
+- [[file:dired.org][Dired]]
+- [[file:editkit.org][EditKit (numerous editing commands)]]
+- [[file:ibuffer.org][IBuffer]]
+- [[file:image.org][Image]]
+- [[file:info.org][Info]]
+- [[file:isearch.org][I-Search]]
+- [[file:re-builder.org][RE-Builder]]
diff --git a/lisp/Makefile b/lisp/Makefile
index 3e6d5d8623..3eb825b835 100644
--- a/lisp/Makefile
+++ b/lisp/Makefile
@@ -29,7 +29,8 @@ ibuffer-tests \
info-tests \
isearch-tests \
make-mode-tests \
-re-builder-tests
+re-builder-tests \
+timezone-tests
.PHONY: lib-tests
lib-tests:
@@ -82,3 +83,7 @@ make-mode-tests:
.PHONY: re-builder-tests
re-builder-tests:
$(MAKE) -C $(SRC_DIR) -f Makefile-re-builder.make tests
+
+.PHONY: timezone-tests
+timezone-tests:
+ $(MAKE) -C $(SRC_DIR) -f Makefile-timezone.make tests
diff --git a/lisp/Makefile-timezone.make b/lisp/Makefile-timezone.make
new file mode 100644
index 0000000000..b293c6824e
--- /dev/null
+++ b/lisp/Makefile-timezone.make
@@ -0,0 +1,30 @@
+##
+# Copyright (C) 2025 Charles Y. Choi
+#
+# 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/>.
+
+include Makefile--defines.make
+
+PACKAGE_NAME=casual-timezone
+ELISP_INCLUDES=casual-timezone-utils.el \
+casual-timezone-settings.el
+ELISP_PACKAGES=
+ELISP_TEST_INCLUDES=casual-timezone-test-utils.el
+PACKAGE_PATHS= \
+-L $(EMACS_ELPA_DIR)/compat-current \
+-L $(EMACS_ELPA_DIR)/seq-current \
+-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(CASUAL_LIB_LISP_DIR)
+
+include Makefile--rules.make
diff --git a/lisp/casual-calc-time.el b/lisp/casual-calc-time.el
index e5e501ff0f..17948f48b5 100644
--- a/lisp/casual-calc-time.el
+++ b/lisp/casual-calc-time.el
@@ -24,19 +24,39 @@
;;; Code:
(require 'calc)
+(require 'calc-forms)
+(require 'org)
(require 'transient)
(require 'casual-lib)
(require 'casual-calc-utils)
+(defun casual-calc-push-timestamp ()
+ "Push timestamp on stack using `org-read-date' interface."
+ (interactive)
+ (unless (derived-mode-p 'calc-mode) (error "Not in calc mode"))
+ (let* ((ts (org-read-date t nil nil nil nil
+ (format-time-string
+ "%H:%M"
+ (current-time))))
+ (calc-ts (math-parse-date ts)))
+ (calc-push calc-ts)))
+
(transient-define-prefix casual-calc-time-tmenu ()
"Casual time functions menu."
- [["Time"
+ ["Time"
+ ["Stack"
("n" "Now" calc-now :transient t)
+ ("t" "Timestamp" casual-calc-push-timestamp :transient t)]
+
+ ["Functions"
("f" "First Day of›" casual-calc-first-day-tmenu)
("i" "Increment Month" calc-inc-month :transient t)
- ("u" "To/From Unix Time" calc-unix-time :transient t)
- ("a" "Add Business Days" calc-business-days-plus :transient t)
- ("s" "Subtract Business Days" calc-business-days-minus :transient t)]
+ ("u" "To/From Unix Time" calc-unix-time :transient t)]
+
+ ["Business Days"
+ ("a" "Add" calc-business-days-plus :transient t)
+ ("s" "Subtract" calc-business-days-minus :transient t)]
+
casual-calc-operators-group]
casual-calc-navigation-group)
diff --git a/lisp/casual-editkit-utils.el b/lisp/casual-editkit-utils.el
index c19d0f1150..cba11853b5 100644
--- a/lisp/casual-editkit-utils.el
+++ b/lisp/casual-editkit-utils.el
@@ -32,6 +32,7 @@
(require 'electric)
(require 'casual-editkit-constants)
(require 'casual-editkit-settings)
+(require 'casual-timezone)
;;; Predicates
@@ -588,35 +589,38 @@ accessed here."
Commands pertaining to invoking different tools can be accessed here."
["Tools"
["Shells & REPLs"
- ("s" "Shell" shell)
+ :pad-keys t
+ ("sh" "Shell" shell)
("!" "Shell Command…" shell-command)
("&" "Shell Command &…" async-shell-command)
- ("e" "Eshell" eshell)
- ("i" "IELM" ielm)
- ("t" "term" term)
- ("p" "Python" run-python)]
+ ("es" "Eshell" eshell)
+ ("ie" "IELM" ielm)
+ ("te" "term" term)
+ ("py" "Python" run-python)]
["Utilities"
- ("c" "Calc" calc)
- ("r" "RE-Builder" re-builder)
- ("w" "Word Count" (lambda ()
+ ("cc" "Calc" calc)
+ ("re" "RE-Builder" re-builder)
+ ("wc" "Word Count" (lambda ()
(interactive)
(call-interactively #'count-words)))]
["Almanac"
:pad-keys t
- ("a" "Calendar" calendar)
- ("C" "World Clock" world-clock)
- ("S" "Sunrise/Sunset" sunrise-sunset)]
+ ("ca" "Calendar" calendar)
+ ("cl" "World Clock" world-clock)
+ ("su" "Sunrise/Sunset" sunrise-sunset)
+ ("tz" "Time Zone›" casual-timezone-tmenu
+ :if-not (lambda () (eq system-type 'windows-nt)))]
["Misc"
:pad-keys t
- ("E" "erc" erc)
- ("M-e" "eww" eww)]
+ ("er" "erc" erc)
+ ("ew" "eww" eww)]
["Fun"
- ("T" "Tetris" tetris)
- ("z" "Zone" zone)]]
+ ("ts" "Tetris" tetris)
+ ("zo" "Zone" zone)]]
casual-editkit-navigation-group)
diff --git a/lisp/casual-editkit.el b/lisp/casual-editkit.el
index 6487f4df17..fdd934a1cc 100644
--- a/lisp/casual-editkit.el
+++ b/lisp/casual-editkit.el
@@ -57,7 +57,11 @@ user-customized menu."
:inapt-if-not buffer-modified-p
:if-not (lambda () buffer-read-only))
("s" "Save" save-buffer
- :if-not (lambda () buffer-read-only))]
+ :if-not (lambda () buffer-read-only))
+ ("S" "Save as…" write-file
+ :if-not (lambda () buffer-read-only))
+ ("y" "Write region…" write-region
+ :if use-region-p)]
["Edit"
:pad-keys t
diff --git a/lisp/casual-timezone-settings.el b/lisp/casual-timezone-settings.el
new file mode 100644
index 0000000000..ae54c63f32
--- /dev/null
+++ b/lisp/casual-timezone-settings.el
@@ -0,0 +1,142 @@
+;;; casual-timezone-settings.el --- Casual Timezone Settings -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+;;
+
+;;; Code:
+(require 'casual-lib)
+(require 'casual-timezone-utils)
+
+(transient-define-prefix casual-timezone-settings-tmenu ()
+ "Casual Timezone settings menu."
+ ["Timezone: Settings"
+ ["Working Hours"
+ ("r" "Range" casual-timezone--customize-working-hours-range
+ :description (lambda ()
+ (format
+ "Range (%d..%d)"
+ (map-elt casual-timezone-working-hours-range :start)
+ (map-elt casual-timezone-working-hours-range :stop))))
+
+ ("g" "Glyph" casual-timezone--customize-working-hour-glyph
+ :description (lambda ()
+ (format
+ "Glyph (%s)"
+ casual-timezone-working-hour-glyph)))
+
+ ("F" "Face" casual-timezone--customize-planner-working-highlight)]
+
+ ["Formats"
+ ("c" "Convert" casual-timezone--customize-convert-timestamp-format
+ :description (lambda ()
+ (format
+ "Convert: (ex: %s)"
+ (format-time-string
+ casual-timezone-convert-datestamp-format
+ (current-time)))))
+
+ ("p" "Planner" casual-timezone--customize-datestamp-format
+ :description (lambda ()
+ (format
+ "Planner: (ex: %s)"
+ (format-time-string
+ casual-timezone-datestamp-format
+ (current-time)))))
+ ("f" "Describe Format" casual-timezone--describe-format-time-string)]]
+
+ [:class transient-row
+ (casual-lib-customize-unicode)
+ (casual-lib-customize-hide-navigation)]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("a" "About" casual-timezone-about :transient nil)
+
+ (casual-lib-quit-all)])
+
+(defun casual-timezone--customize-working-hour-glyph ()
+ "Set working hour glyph.
+
+This customizes the variable `casual-timezone-working-hour-glyph'."
+ (interactive)
+ (customize-variable 'casual-timezone-working-hour-glyph))
+
+(defun casual-timezone--customize-planner-working-highlight ()
+ "Set working hour highlight face.
+
+This customizes the face `casual-timezone-working-highlight'."
+ (interactive)
+ (customize-face 'casual-timezone-planner-working-highlight))
+
+(defun casual-timezone--customize-working-hours-range ()
+ "Set working hours range.
+
+This customizes the variable `casual-timezone-working-hours-range'."
+ (interactive)
+ (customize-variable 'casual-timezone-working-hours-range))
+
+(defun casual-timezone--customize-convert-timestamp-format ()
+ "Set conversion timestamp format.
+
+This customizes the variable `casual-timezone-convert-datestamp-format'."
+ (interactive)
+ (customize-variable 'casual-timezone-convert-datestamp-format))
+
+(defun casual-timezone--customize-datestamp-format ()
+ "Set planner timestamp format.
+
+This customizes the variable `casual-timezone-datestamp-format'."
+ (interactive)
+ (customize-variable 'casual-timezone-datestamp-format))
+
+(defun casual-timezone--describe-format-time-string ()
+ "Describe time string format.
+
+This describes the command `format-time-string'."
+ (interactive)
+ (describe-command #'format-time-string))
+
+(defun casual-timezone-about ()
+ "Casual Timezone is a Transient menu for working with timezones.
+
+Learn more about using Casual Timezone at our discussion group on GitHub.
+Any questions or comments about it should be made there.
+URL `https://github.com/kickingvegas/casual/discussions'
+
+If you find a bug or have an enhancement request, please file an issue.
+Our best effort will be made to answer it.
+URL `https://github.com/kickingvegas/casual/issues'
+
+If you enjoy using Casual Timezone, consider making a modest financial
+contribution to help support its development and maintenance.
+URL `https://www.buymeacoffee.com/kickingvegas'
+
+Casual Timezone was conceived and crafted by Charles Choi in
+San Francisco, California.
+
+Thank you for using Casual Timezone.
+
+Always choose love."
+ (interactive)
+ (describe-function #'casual-timezone-about))
+
+(provide 'casual-timezone-settings)
+;;; casual-timezone-settings.el ends here
diff --git a/lisp/casual-timezone-utils.el b/lisp/casual-timezone-utils.el
new file mode 100644
index 0000000000..8793b0abc1
--- /dev/null
+++ b/lisp/casual-timezone-utils.el
@@ -0,0 +1,490 @@
+;;; casual-timezone-utils.el --- Casual Timezone Utils -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+;;
+
+;;; Code:
+(require 'map)
+(require 'org)
+(require 'vtable)
+(require 'casual-lib)
+
+(defconst casual-timezone-unicode-db
+ '((:previous . '("↑" "Previous"))
+ (:next . '("↓" "Next"))
+ (:forward . '("→" "Forward"))
+ (:backward . '("←" "Backward"))
+ (:current . '("⨀" "Current Hour")))
+
+ "Unicode symbol DB to use for Timezone Transient menus.")
+
+(defun casual-timezone-unicode-get (key)
+ "Lookup Unicode symbol for KEY in DB.
+
+- KEY symbol used to lookup Unicode symbol in DB.
+
+If the value of customizable variable `casual-lib-use-unicode'
+is non-nil, then the Unicode symbol is returned, otherwise a
+plain ASCII-range string."
+ (casual-lib-unicode-db-get key casual-timezone-unicode-db))
+
+(defcustom casual-timezone-working-hours-range '((:start . 9)(:stop . 17))
+ "Working hours range.
+The range of hour values are between 0 to 23, inclusive."
+ :type '(alist :key-type symbol :value-type natnum)
+ :group 'casual)
+
+(defcustom casual-timezone-convert-datestamp-format "%Y-%m-%d %H:%M:%S %Z"
+ "Datestamp format used for timezone conversion.
+
+This customizable variable determines the reporting format used by the
+commands `casual-timezone-local-time-to-remote' and
+`casual-timezone-remote-time-to-local'.
+
+The specification of this variable conforms to the format string used by
+`format-time-string' as described in Info node `(elisp) Time Parsing'."
+ :type 'string
+ :group 'casual)
+
+(defcustom casual-timezone-datestamp-format "%a %b %-e %Y, %l:%M %p"
+ "Datestamp format used by `casual-timezone-planner'.
+
+This customizable variable determines the reporting format used
+by the command `casual-timezone-planner'. The specification of this
+variable conforms to the format string used by
+`format-time-string' as described in Info node `(elisp) Time
+Parsing'.
+
+If 24 hour clock time is preferred, use ‘%k’ instead of ‘%l’."
+ :type 'string
+ :group 'casual)
+
+(defcustom casual-timezone-working-hour-glyph "☼"
+ "Working hour glyph used by `casual-timezone-planner'.
+
+This customizable variable contains the glyph used to annotate a
+working hour in `casual-timezone-planner'."
+ :type 'string
+ :group 'casual)
+
+(defface casual-timezone-planner-working-highlight
+ '((((type tty) (class color))
+ :background "gray25")
+ (((class color) (min-colors 88) (background light))
+ :background "#FDFEB1" :foreground "black")
+ (((class color) (min-colors 88) (background dark))
+ :background "gray25")
+ (((class color) (min-colors 16) (background light))
+ :background "yellow")
+ (((class color) (min-colors 16) (background dark))
+ :background "olive")
+ (((class color) (min-colors 8))
+ :background "olive" :foreground "black")
+ (t :inverse-video t))
+ "Casual Timezone Planner working hours highlight."
+ :group 'casual)
+
+(defun casual-timezone-zone-info ()
+ "List of timezones in zoneinfo database.
+
+This function reads the local zoneinfo database to obtain the
+list of timezones.
+
+This function requires that /usr/share/zoneinfo/tzdata.zi exists
+and that awk is installed."
+ (unless (not (eq system-type 'windows-nt))
+ (error "Not available on Windows"))
+
+ (with-temp-buffer
+ (call-process
+ "awk"
+ nil
+ (current-buffer)
+ nil
+ "/^Z/ { print $2 }; /^L/ { print $3 }" "/usr/share/zoneinfo/tzdata.zi")
+ (split-string (buffer-string))))
+
+(defun casual-timezone-map-local-to-timezone (ts remote-tz)
+ "Map local TS to REMOTE-TZ."
+ (let* ((parse-ts (string-split (org-read-date nil nil ts)))
+ (datestamp (nth 0 parse-ts))
+ (timestamp (nth 1 parse-ts))
+ (local-tz (nth 1 (current-time-zone))))
+ (format-time-string
+ "%Y-%m-%d %H:%M:%S %Z"
+ (date-to-time
+ (concat datestamp "T" timestamp ":00" " " local-tz))
+ remote-tz)))
+
+(defun casual-timezone-local-time-to-remote (&optional datestr remote-tz)
+ "Convert local date string DATESTR to remote timezone REMOTE-TZ.
+
+The result is both copied to the `kill-ring' and messaged to the
+mini-buffer.
+
+If run interactively, the user will be prompted for a date string via
+the `calendar' interface and the timezone via a completion interface.
+
+The format of the timestamp is defined in the variable
+`casual-timezone-convert-datestamp-format'."
+ (interactive)
+ (unless (not (eq system-type 'windows-nt))
+ (error "Not available on Windows"))
+
+ (let* ((datestr (or datestr
+ (org-read-date t nil nil nil nil
+ (format-time-string
+ "%H:%M"
+ (current-time)))))
+ (remote-tz (or remote-tz
+ (completing-read-default
+ "Remote Timezone: "
+ (casual-timezone-zone-info))))
+ (parse-ts (string-split datestr))
+ (datestamp (nth 0 parse-ts))
+ (timestamp (nth 1 parse-ts))
+ (local-tz (nth 1 (current-time-zone)))
+ (remote-time
+ (format-time-string
+ casual-timezone-convert-datestamp-format
+ (date-to-time
+ (concat datestamp "T" timestamp ":00" " " local-tz))
+ remote-tz))
+ (remote-time-tz (concat remote-tz " " remote-time)))
+ (kill-new remote-time-tz)
+ (message remote-time-tz)
+ remote-time-tz))
+
+(defun casual-timezone-remote-time-to-local (&optional datestr remote-tz)
+ "Convert date string DATESTR in remote timezone REMOTE-TZ to local.
+
+The result is both copied to the `kill-ring' and messaged to the
+mini-buffer.
+
+The format of the timestamp is defined in the variable
+`casual-timezone-convert-datestamp-format'."
+ (interactive)
+ (unless (not (eq system-type 'windows-nt))
+ (error "Not available on Windows"))
+
+ (let* ((remote-tz (or remote-tz
+ (completing-read-default
+ "Remote Timezone: "
+ (casual-timezone-zone-info))))
+ (datestr (or datestr
+ (org-read-date t nil nil nil nil
+ (format-time-string
+ "%H:%M"
+ (current-time)))))
+ (tzcode (casual-timezone-offset-8601
+ (nth 0 (current-time-zone nil remote-tz))))
+ (parse-ts (string-split datestr))
+ (datestamp (nth 0 parse-ts))
+ (timestamp (nth 1 parse-ts))
+ ;; (local-tz (nth 1 (current-time-zone)))
+ (index-time
+ (format-time-string
+ casual-timezone-convert-datestamp-format
+ (date-to-time (concat datestamp "T" timestamp ":00" " " tzcode)))))
+ (kill-new index-time)
+ (message index-time)
+ index-time))
+
+(defun casual-timezone-offset-8601 (offset)
+ "Compute OFFSET for ISO 8601 date."
+ (let* ((hours (/ offset 3600))
+ (fractional (% offset 3600))
+ (minutes (if (zerop fractional)
+ fractional
+ (round (* (/ fractional 3600.0) 60))))
+ (abs-hours (abs hours))
+ (remote-tz (concat (format "%02d" abs-hours) (format "%02d"
minutes))))
+
+ (if (> hours 0)
+ (concat "+" remote-tz)
+ (concat "-" remote-tz))))
+
+;; !!!: Unused code.
+(defun casual-timezone--gen-hour-sequence (start duration)
+ "Generate hour sequence given START, DURATION."
+ (mapcar (lambda (x) (% x 24)) (number-sequence start (+ start duration))))
+
+(defvar-keymap casual-timezone-planner-mode-map
+ "C-o" #'casual-timezone-planner-tmenu
+ "." #'casual-timezone-jump-to-relative-now
+ "t" #'casual-timezone-planner-current-time
+ "l" #'casual-timezone-planner-current-local
+ "r" #'casual-timezone-planner-current-remote
+ "f" #'casual-timezone-planner-forward-day
+ "b" #'casual-timezone-planner-backward-day
+ "p" #'previous-line
+ "n" #'next-line
+ "q" #'quit-window
+ "j" #'next-line
+ "k" #'previous-line
+ "w" #'world-clock
+ "z" #'casual-timezone-planner
+ "c" #'calendar)
+
+(define-derived-mode casual-timezone-planner-mode
+ special-mode "Timezone Planner"
+ "Major mode for Timezone Planner."
+ (hl-line-mode t))
+
+(defun casual-timezone-planner ()
+ "Generate table comparing hours between local and a remote timezone.
+
+This command will prompt the user twice:
+ 1. to specify a remote timezone
+ 2. to specify a calendar day
+
+Upon entering the above, a new buffer will be created (or
+updated) comparing the hours between the two timezones.
+
+The report datestamp format can be customized via the variable
+`casual-timezone-datestamp-format'.
+
+Working hours are annotated with a ☼. The range of working hours can be
+customized via the variable `casual-timezone-working-hours-range'."
+ (interactive)
+ (unless (not (eq system-type 'windows-nt))
+ (error "Not available on Windows"))
+
+ (let* ((remote-tz (completing-read-default "Remote Timezone: "
(casual-timezone-zone-info)))
+ ;; (tzcode (casual-timezone-offset-8601 (nth 0 (current-time-zone nil
remote-tz))))
+ (datestamp (org-read-date))
+ (local-tz (nth 1 (current-time-zone)))
+ (start-time (date-to-time (concat datestamp " " "05:00")))
+ (increments (seq-map (lambda (x) (seconds-to-time (* x
3600)))(number-sequence 0 25)))
+ (tztimes (seq-map (lambda (x) (time-add start-time x)) increments))
+ (local-times
+ (seq-map
+ (lambda (x) (format-time-string "%Y-%m-%d %H:%M:%S" x))
+ tztimes))
+ (remote-times
+ (seq-map
+ (lambda (x) (time-to-seconds (date-to-time (format-time-string
+ "%Y-%m-%dT%H:%M:%S"
+ (date-to-time (format-time-string
(concat x " " local-tz)))
+ remote-tz))))
+ local-times))
+ (tz-data (seq-mapn #'list tztimes remote-times))
+ (tz-buffer-name (format "*%s - %s*" local-tz remote-tz)))
+
+ (get-buffer-create tz-buffer-name)
+ (switch-to-buffer (set-buffer tz-buffer-name))
+ (casual-timezone-planner-mode)
+
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (make-vtable
+ :columns `((:name ,local-tz :width 30 :align left) ;; !!! For some
reason I can't pass in local-tz
+ (:name ,remote-tz :width 30 :align left))
+
+ :objects tz-data
+
+ :getter `(lambda (issue column table)
+ (pcase (vtable-column table column)
+ (,local-tz (nth 0 issue))
+ (,remote-tz (nth 1 issue))))
+
+ :formatter `(lambda (value column table)
+ (casual-timezone--date-formatter value))
+
+ :displayer `(lambda (fvalue index max-width table)
+ (propertize fvalue 'default 'bold)))
+ (casual-timezone-jump-to-relative-now))))
+
+
+(defun casual-timezone-planner-current-local ()
+ "Copy local time on current line to `kill-ring'.
+
+The format of the timestamp is defined in the variable
+`casual-timezone-datestamp-format'."
+ (interactive)
+ (let ((result (casual-timezone-planner--format-current-index 0)))
+ (kill-new result)
+ (message result)))
+
+(defun casual-timezone-planner-current-remote ()
+ "Copy remote time on current line to `kill-ring'.
+
+The format of the timestamp is defined in the variable
+`casual-timezone-datestamp-format'."
+ (interactive)
+ (let ((result (casual-timezone-planner--format-current-index 1)))
+ (kill-new result)
+ (message result)))
+
+(defun casual-timezone-planner-current-time ()
+ "Copy times on current line to `kill-ring'.
+
+The format of the timestamp is defined in the variable
+`casual-timezone-datestamp-format'."
+ (interactive)
+ (let* ((local (casual-timezone-planner--format-current-index 0))
+ (remote (casual-timezone-planner--format-current-index 1))
+ (result (format "%s, %s" local remote)))
+ (kill-new result)
+ (message result)))
+
+(defun casual-timezone-planner--format-current-index (arg)
+ "Copy element with ARG index in current vtable object into `kill-ring'."
+ (unless (vtable-current-table) (error "No planner table"))
+
+ (let* ((name (vtable-column (vtable-current-table) arg))
+ (obj (vtable-current-object))
+ (index-time (nth arg obj))
+ (result (format
+ "%s - %s"
+ name
+ (format-time-string
+ casual-timezone-datestamp-format
+ index-time))))
+ result))
+
+(defun casual-timezone-jump-to-relative-now ()
+ "Jump to current relative hour in timezone planner view.
+
+This command is used for the planner generated by
+`casual-timezone-planner'."
+ (interactive)
+ (unless (vtable-current-table) (error "No planner table"))
+
+ (let ((current-table (vtable-current-table)))
+ (if current-table
+ (let* ((table-data (vtable-objects current-table))
+ (idxObj (car table-data))
+ (now (casual-timezone--relative-now (car idxObj))))
+
+ (while table-data
+ (let* ((obj (car table-data))
+ (index-time (car obj)))
+ (if (< index-time now)
+ (setq idxObj obj)))
+ (setq table-data (cdr table-data)))
+ (vtable-goto-object idxObj)))))
+
+(defun casual-timezone--relative-now (plan-time)
+ "Adjust current time (hour:minutes) to date relative to planner date
PLAN-TIME.
+
+This command is used for the planner generated by
+`casual-timezone-planner'."
+ (let* ((datestamp-buf (format-time-string "%Y-%m-%d" plan-time))
+ (datestamp (date-to-time datestamp-buf))
+ (now (current-time))
+ (hours (string-to-number (format-time-string "%H" now)))
+ (minutes (string-to-number (format-time-string "%M" now)))
+ (offset (+ (* (* hours 60) 60) (* minutes 60)))
+ (datestamp-adj (time-add datestamp offset)))
+ datestamp-adj))
+
+
+(defun casual-timezone--date-formatter (timestamp)
+ "Datestamp formatter given TIMESTAMP.
+
+This formats the output result using the customizable variables
+`casual-timezone-datestamp-format' and `casual-timezone-working-hours-range'."
+ (let ((hour (string-to-number (format-time-string "%H" timestamp)))
+ (datestamp (format-time-string casual-timezone-datestamp-format
timestamp)))
+
+ (if (and (>= hour (map-elt casual-timezone-working-hours-range :start))
+ (<= hour (map-elt casual-timezone-working-hours-range :stop)))
+ (propertize
+ (concat datestamp " " casual-timezone-working-hour-glyph)
+ 'face
+ 'casual-timezone-planner-working-highlight)
+ datestamp)))
+
+(defun casual-timezone-planner-forward-day ()
+ "Move forward one day in timezone planner.
+
+Note: This command relies on `vtable-update-object' which breaks if the
+window width has changed."
+ (interactive)
+ (unless (vtable-current-table) (error "No planner table"))
+ (casual-timezone--planner-adjust-day nil))
+
+(defun casual-timezone-planner-backward-day ()
+ "Move backward one day in timezone planner.
+
+Note: This command relies on `vtable-update-object' which breaks if the
+window width has changed."
+ (interactive)
+ (unless (vtable-current-table) (error "No planner table"))
+ (casual-timezone--planner-adjust-day t))
+
+(defun casual-timezone--planner-adjust-day (backward)
+ "If BACKWARD is non-nil, adjust timezone planner -24 hours, otherwise ahead."
+ (let* ((table (vtable-current-table))
+ (objects (oref table objects))
+ (posix-day 86400)
+ (time-adjust (if backward
+ (* -1 posix-day)
+ posix-day)))
+ (mapc (lambda (obj)
+ (let ((adjusted-obj
+ (seq-map (lambda (x) (+ x time-adjust)) obj)))
+ (vtable-update-object table adjusted-obj obj)))
+ objects)
+ (casual-timezone-jump-to-relative-now)))
+
+;; Transients
+(transient-define-prefix casual-timezone-planner-tmenu ()
+ "Main menu for Casual Timezone."
+
+ ["Casual Timezone"
+ ["Navigation"
+ ("." "Current Hour" casual-timezone-jump-to-relative-now
+ :description (lambda () (casual-timezone-unicode-get :current))
+ :transient t)
+ ("p" "Previous" previous-line
+ :description (lambda () (casual-timezone-unicode-get :previous))
+ :transient t)
+ ("n" "Next" next-line
+ :description (lambda () (casual-timezone-unicode-get :next))
+ :transient t)]
+
+ ["Day"
+ ("f" "Forward" casual-timezone-planner-forward-day
+ :description (lambda () (casual-timezone-unicode-get :forward))
+ :transient t)
+ ("b" "Backward" casual-timezone-planner-backward-day
+ :description (lambda () (casual-timezone-unicode-get :backward))
+ :transient t)]
+
+ ["Copy Time"
+ ("t" "Times" casual-timezone-planner-current-time)
+ ("l" "Local" casual-timezone-planner-current-local)
+ ("r" "Remote" casual-timezone-planner-current-remote)]
+
+ ["Misc"
+ ("z" "Planner…" casual-timezone-planner)
+ ("w" "World Clock" world-clock)]]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("," "Settings" casual-timezone-settings-tmenu)
+ ("q" "Quit" quit-window)
+ (casual-lib-quit-all)])
+
+(provide 'casual-timezone-utils)
+;;; casual-timezone-utils.el ends here
diff --git a/lisp/casual-timezone.el b/lisp/casual-timezone.el
new file mode 100644
index 0000000000..97b30982a9
--- /dev/null
+++ b/lisp/casual-timezone.el
@@ -0,0 +1,44 @@
+;;; casual-timezone.el --- Timezone Planner -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+
+;;
+
+;;; Code:
+
+(require 'casual-timezone-settings)
+
+;;;###autoload (autoload 'casual-timezone-tmenu "casual-timezone" nil t)
+(transient-define-prefix casual-timezone-tmenu ()
+ "Main menu for Casual Timezone."
+
+ ["Casual Timezone"
+ ("l" "Local to Remote…" casual-timezone-local-time-to-remote)
+ ("r" "Remote to Local…" casual-timezone-remote-time-to-local)
+ ("z" "Planner…" casual-timezone-planner)]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("," "Settings" casual-timezone-settings-tmenu)
+ (casual-lib-quit-all)])
+
+(provide 'casual-timezone)
+;;; casual-timezone.el ends here
diff --git a/lisp/casual.el b/lisp/casual.el
index 50492469da..2d3e67a58a 100644
--- a/lisp/casual.el
+++ b/lisp/casual.el
@@ -5,7 +5,7 @@
;; Author: Charles Choi <[email protected]>
;; URL: https://github.com/kickingvegas/casual
;; Keywords: tools, wp
-;; Version: 2.4.3
+;; Version: 2.4.4-rc.1
;; Package-Requires: ((emacs "29.1") (transient "0.6.0"))
;; This program is free software; you can redistribute it and/or modify
diff --git a/tests/Makefile b/tests/Makefile
index c485160e9d..5217e1d636 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -28,7 +28,8 @@ ibuffer-tests \
info-tests \
isearch-tests \
make-mode-tests \
-re-builder-tests
+re-builder-tests \
+timezone-tests
SRC_DIR=../lisp
@@ -43,7 +44,8 @@ ibuffer-tests \
info-tests \
isearch-tests \
make-mode-tests \
-re-builder-tests
+re-builder-tests \
+timezone-tests
lib-tests:
$(MAKE) -C $(SRC_DIR) $@
@@ -80,3 +82,6 @@ make-mode-tests:
re-builder-tests:
$(MAKE) -C $(SRC_DIR) $@
+
+timezone-tests:
+ $(MAKE) -C $(SRC_DIR) $@
diff --git a/tests/casual-timezone-test-utils.el
b/tests/casual-timezone-test-utils.el
new file mode 100644
index 0000000000..da28df8849
--- /dev/null
+++ b/tests/casual-timezone-test-utils.el
@@ -0,0 +1,40 @@
+;;; casual-timezone-test-utils.el --- Casual Test Utils -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'casual-lib)
+(require 'kmacro)
+
+
+(defun casualt-timezone-setup ()
+ "Casual timezone test setup."
+ )
+
+(defun casualt-timezone-breakdown ()
+ "Casual timezone test breakdown."
+ )
+
+(provide 'casual-timezone-test-utils)
+;;; casual-timezone-test-utils.el ends here
diff --git a/tests/test-casual-calc-time.el b/tests/test-casual-calc-time.el
index 5d062ff0c1..923b1918dd 100644
--- a/tests/test-casual-calc-time.el
+++ b/tests/test-casual-calc-time.el
@@ -32,10 +32,12 @@
(cl-letf
(((symbol-function #'calc-now) (lambda (x) (interactive)(print "WARNING:
override")))
((symbol-function #'calc-inc-month) (lambda (x) (interactive)(print
"WARNING: override")))
+ ((symbol-function #'casual-calc-push-timestamp) (lambda (x)
(interactive)(print "WARNING: override")))
((symbol-function #'calc-unix-time) (lambda (x) (interactive)(print
"WARNING: override")))
((symbol-function #'calc-business-days-plus) (lambda (x)
(interactive)(print "WARNING: override")))
((symbol-function #'calc-business-days-minus) (lambda (x)
(interactive)(print "WARNING: override"))))
(let* ((test-vectors '(("n" . calc-now)
+ ("t" . casual-calc-push-timestamp)
("f" . casual-calc-first-day-tmenu)
("i" . calc-inc-month)
("u" . calc-unix-time)
diff --git a/tests/test-casual-editkit-utils.el
b/tests/test-casual-editkit-utils.el
index 5268f88fce..b16efd771e 100644
--- a/tests/test-casual-editkit-utils.el
+++ b/tests/test-casual-editkit-utils.el
@@ -509,27 +509,29 @@
(casualt-mock #'sunrise-sunset)
(casualt-mock #'erc)
(casualt-mock #'eww)
+ (casualt-mock #'casual-timezone-tmenu)
(casualt-mock #'tetris)
(casualt-mock #'zone))
(let ((test-vectors
- '((:binding "s" :command shell)
+ '((:binding "sh" :command shell)
(:binding "!" :command shell-command)
(:binding "&" :command async-shell-command)
- (:binding "e" :command eshell)
- (:binding "i" :command ielm)
- (:binding "t" :command term)
- (:binding "p" :command run-python)
- (:binding "c" :command calc)
- (:binding "r" :command re-builder)
- (:binding "w" :command count-words)
- (:binding "a" :command calendar)
- (:binding "C" :command world-clock)
- (:binding "S" :command sunrise-sunset)
- (:binding "E
y" :command erc)
- (:binding "M-e" :command eww)
- (:binding "z" :command zone)
- (:binding "T" :command tetris))))
+ (:binding "es" :command eshell)
+ (:binding "ie" :command ielm)
+ (:binding "te" :command term)
+ (:binding "py" :command run-python)
+ (:binding "cc" :command calc)
+ (:binding "re" :command re-builder)
+ (:binding "wc" :command count-words)
+ (:binding "ca" :command calendar)
+ (:binding "cl" :command world-clock)
+ (:binding "su" :command sunrise-sunset)
+ (:binding "er
y" :command erc)
+ (:binding "ew" :command eww)
+ (:binding "tz" :command casual-timezone-tmenu)
+ (:binding "zo" :command zone)
+ (:binding "ts" :command tetris))))
(casualt-suffix-testcase-runner test-vectors
#'casual-editkit-tools-tmenu
diff --git a/tests/test-casual-editkit.el b/tests/test-casual-editkit.el
index d6417df292..a6818f3f5f 100644
--- a/tests/test-casual-editkit.el
+++ b/tests/test-casual-editkit.el
@@ -41,7 +41,9 @@
(casualt-mock #'recentf-open-files)
(casualt-mock #'revert-buffer)
(casualt-mock #'save-buffer)
+ (casualt-mock #'write-file)
(casualt-mock #'widen)
+ (casualt-mock #'write-region)
(casualt-mock #'insert-char)
(casualt-mock #'fill-paragraph)
@@ -49,7 +51,7 @@
(casualt-mock #'join-line)
(casualt-mock #'mark-sexp)
(casualt-mock #'kill-sexp)
- (casualt-mock #'transpose-sexps)
+ (casualt-mock #'transpose-sexp)
(casualt-mock #'org-agenda)
(casualt-mock #'compile)
@@ -69,6 +71,8 @@
(:binding "R" :command recentf-open-files)
(:binding "v" :command revert-buffer)
(:binding "s" :command save-buffer)
+ (:binding "S" :command write-file)
+ (:binding "y" :command write-region)
(:binding "e" :command casual-editkit-edit-tmenu)
(:binding "p" :command fill-paragraph)
@@ -109,6 +113,7 @@
(:binding "x" :command save-buffers-kill-emacs))))
(insert "hello")
+ (casualt-mock-active-region)
(casualt-suffix-testcase-runner test-vectors
#'casual-editkit-main-tmenu
'(lambda () (random 5000)))
diff --git a/tests/test-casual-timezone-settings.el
b/tests/test-casual-timezone-settings.el
new file mode 100644
index 0000000000..6eef0cc2e3
--- /dev/null
+++ b/tests/test-casual-timezone-settings.el
@@ -0,0 +1,61 @@
+;;; test-casual-timezone-settings.el --- Casual Make Settings Tests -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+
+;;
+
+;;; Code:
+
+(require 'ert)
+(require 'casual-timezone-test-utils)
+(require 'casual-timezone-settings)
+
+(ert-deftest test-casual-timezone-settings-tmenu ()
+ (let ()
+ (casualt-timezone-setup)
+ (cl-letf ((casualt-mock #'casual-timezone-about)
+ (casualt-mock #'casual-timezone--customize-working-hours-range)
+ (casualt-mock #'casual-timezone--customize-working-hour-glyph)
+ (casualt-mock
#'casual-timezone--customize-planner-working-highlight)
+ (casualt-mock
#'casual-timezone--customize-convert-timestamp-format)
+ (casualt-mock #'casual-timezone--customize-datestamp-format)
+ (casualt-mock #'casual-timezone--describe-format-time-string))
+
+ (let ((test-vectors
+ '((:binding "r" :command
casual-timezone--customize-working-hours-range)
+ (:binding "g" :command
casual-timezone--customize-working-hour-glyph)
+ (:binding "F" :command
casual-timezone--customize-planner-working-highlight)
+ (:binding "c" :command
casual-timezone--customize-convert-timestamp-format)
+ (:binding "p" :command
casual-timezone--customize-datestamp-format)
+ (:binding "f" :command
casual-timezone--describe-format-time-string)
+ (:binding "a" :command casual-timezone-about)
+ )))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-timezone-settings-tmenu
+ '(lambda () (random 5000)))))
+ (casualt-timezone-breakdown)))
+
+(ert-deftest test-casual-timezone-about ()
+ (should (stringp (casual-timezone-about))))
+
+(provide 'test-casual-timezone-settings)
+;;; test-casual-timezone-setttings.el ends here
diff --git a/tests/test-casual-timezone-utils.el
b/tests/test-casual-timezone-utils.el
new file mode 100644
index 0000000000..228ff76866
--- /dev/null
+++ b/tests/test-casual-timezone-utils.el
@@ -0,0 +1,144 @@
+;;; test-casual-timezone-utils.el --- Casual Make Utils Tests -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'casual-timezone-test-utils)
+(require 'casual-timezone-utils)
+
+(ert-deftest test-casual-timezone-unicode-get ()
+ (let ((casual-lib-use-unicode nil))
+ (should (string-equal (casual-timezone-unicode-get :forward) "Forward"))
+ (should (string-equal (casual-timezone-unicode-get :backward) "Backward"))
+ (should (string-equal (casual-timezone-unicode-get :current) "Current
Hour"))
+ (should (string-equal (casual-timezone-unicode-get :previous) "Previous"))
+ (should (string-equal (casual-timezone-unicode-get :next) "Next")))
+
+ (let ((casual-lib-use-unicode t))
+ (should (string-equal (casual-timezone-unicode-get :forward) "→"))
+ (should (string-equal (casual-timezone-unicode-get :backward) "←"))
+ (should (string-equal (casual-timezone-unicode-get :current) "⨀"))
+ (should (string-equal (casual-timezone-unicode-get :previous) "↑"))
+ (should (string-equal (casual-timezone-unicode-get :next) "↓"))))
+
+(ert-deftest test-casual-timezone-planner-tmenu ()
+ (let ()
+ (casualt-timezone-setup)
+ (cl-letf ((casualt-mock #'casual-timezone-jump-to-relative-now)
+ (casualt-mock #'previous-line)
+ (casualt-mock #'next-line)
+ (casualt-mock #'casual-timezone-planner-forward-day)
+ (casualt-mock #'casual-timezone-planner-backward-day)
+ (casualt-mock #'casual-timezone-planner-current-time)
+ (casualt-mock #'casual-timezone-planner-current-local)
+ (casualt-mock #'casual-timezone-planner-current-remote)
+ (casualt-mock #'casual-timezone-planner)
+ (casualt-mock #'world-clock)
+ (casualt-mock #'quit-window))
+
+ (let ((test-vectors
+ '((:binding "." :command casual-timezone-jump-to-relative-now)
+ (:binding "p" :command previous-line)
+ (:binding "n" :command next-line)
+ (:binding "f" :command casual-timezone-planner-forward-day)
+ (:binding "b" :command casual-timezone-planner-backward-day)
+ (:binding "t" :command casual-timezone-planner-current-time)
+ (:binding "l" :command casual-timezone-planner-current-local)
+ (:binding "r" :command casual-timezone-planner-current-remote)
+ (:binding "z" :command casual-timezone-planner)
+ (:binding "w" :command world-clock)
+ (:binding "q" :command quit-window))))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-timezone-planner-tmenu
+ '(lambda () (random 5000)))))
+ (casualt-timezone-breakdown)))
+
+(ert-deftest test-casual-timezone--date-formatter ()
+ (let* ((now (list 26671 48225 355484 0))
+ (casual-timezone-datestamp-format "%a %b %-e %Y, %l:%M %p")
+ (control "Thu May 22 2025, 5:08 PM ☼")
+ (result (casual-timezone--date-formatter now)))
+
+ (should (string-equal control result))))
+
+(ert-deftest test-casual-timezone-zone-info ()
+ ;; !!!: This count of zones for macOS only. Subject to change.
+ (let* ((control 598)
+ (zone-info (casual-timezone-zone-info))
+ (result (length zone-info)))
+ (should (= control result))))
+
+;; !!!: is this function being used?
+(ert-deftest test-casual-timezone-map-local-to-timezone ()
+ (let* ((ts "2025-05-23")
+ (remote-tz "Europe/Berlin")
+ (control "2025-05-23 09:00:00 CEST")
+ (result (casual-timezone-map-local-to-timezone ts remote-tz)))
+ (should (string-equal control result))))
+
+(ert-deftest test-casual-timezone-offset-8601 ()
+ ;; !!!: This test will only work in Pacific Time.
+ (let* ((control "+0200")
+ (offset 7200)
+ (result (casual-timezone-offset-8601 offset)))
+ (should (string-equal control result))))
+
+
+(ert-deftest test-casual-timezone-planner-mode-map ()
+ (let ((test-map casual-timezone-planner-mode-map))
+ (should (eq (keymap-lookup test-map "C-o")
#'casual-timezone-planner-tmenu))
+ (should (eq (keymap-lookup test-map ".")
#'casual-timezone-jump-to-relative-now))
+ (should (eq (keymap-lookup test-map "t")
#'casual-timezone-planner-current-time))
+ (should (eq (keymap-lookup test-map "l")
#'casual-timezone-planner-current-local))
+ (should (eq (keymap-lookup test-map "r")
#'casual-timezone-planner-current-remote))
+ (should (eq (keymap-lookup test-map "p") #'previous-line))
+ (should (eq (keymap-lookup test-map "n") #'next-line))
+ (should (eq (keymap-lookup test-map "q") #'quit-window))
+ (should (eq (keymap-lookup test-map "j") #'next-line))
+ (should (eq (keymap-lookup test-map "k") #'previous-line))
+ (should (eq (keymap-lookup test-map "w") #'world-clock))
+ (should (eq (keymap-lookup test-map "z") #'casual-timezone-planner))
+ (should (eq (keymap-lookup test-map "c") #'calendar))))
+
+
+(ert-deftest test-casual-timezone-local-time-to-remote ()
+ (let* ((read-date "2025-05-23 12:00")
+ (remote-tz "Europe/Berlin")
+ (control "Europe/Berlin 2025-05-23 21:00:00 CEST")
+ (result (casual-timezone-local-time-to-remote read-date remote-tz)))
+
+ (should (string-equal control result))))
+
+(ert-deftest test-casual-timezone-remote-time-to-local ()
+ (let* ((read-date "2025-05-23 12:00")
+ (remote-tz "Europe/Berlin")
+ (control "2025-05-23 03:00:00 PDT")
+ (result (casual-timezone-remote-time-to-local read-date remote-tz)))
+
+ (should (string-equal control result))))
+
+
+(provide 'test-casual-timezone-utils)
+;;; test-casual-timezone-utils.el ends here
diff --git a/tests/test-casual-timezone.el b/tests/test-casual-timezone.el
new file mode 100644
index 0000000000..0d9592d6c8
--- /dev/null
+++ b/tests/test-casual-timezone.el
@@ -0,0 +1,50 @@
+;;; test-casual-timezone.el --- Casual Make Tests -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; 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:
+
+;;
+
+;;; Code:
+
+(require 'ert)
+(require 'casual-timezone-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-timezone)
+
+(ert-deftest test-casual-timezone-tmenu ()
+ (let ()
+ (casualt-timezone-setup)
+ (cl-letf ((casualt-mock #'casual-timezone-local-time-to-remote)
+ (casualt-mock #'casual-timezone-remote-time-to-local)
+ (casualt-mock #'casual-timezone-planner))
+
+ (let ((test-vectors
+ '((:binding "l" :command casual-timezone-local-time-to-remote)
+ (:binding "r" :command casual-timezone-remote-time-to-local)
+ (:binding "z" :command casual-timezone-planner))))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-timezone-tmenu
+ '(lambda () (random 5000)))))
+ (casualt-timezone-breakdown)))
+
+(provide 'test-casual-timezone)
+;;; test-casual-timezone.el ends here