branch: elpa/casual
commit e507877fef1f85c0906ebbcd97fd414c66106baf
Merge: f33c344d0f 7a16655df2
Author: Charles Choi <[email protected]>
Commit: GitHub <[email protected]>

    Merge pull request #314 from kickingvegas/173-build-casual-ediff
    
    Add Casual Ediff
---
 docs/ediff.org                                     | 113 ++++++++
 docs/images/casual-ediff-basic-screenshot.png      | Bin 0 -> 283120 bytes
 docs/images/casual-ediff-merge-conflict.png        | Bin 0 -> 313757 bytes
 docs/images/casual-ediff-screenshot.png            | Bin 0 -> 362160 bytes
 lisp/Makefile                                      |   5 +
 lisp/Makefile-agenda.make                          |   1 +
 lisp/Makefile-bibtex.make                          |   1 +
 lisp/Makefile-bookmarks.make                       |   1 +
 lisp/Makefile-calc.make                            |   1 +
 lisp/Makefile-calendar.make                        |   1 +
 lisp/Makefile-compile.make                         |   1 +
 lisp/{Makefile-eshell.make => Makefile-ediff.make} |   9 +-
 lisp/Makefile-editkit.make                         |   1 +
 lisp/Makefile-elisp.make                           |   1 +
 lisp/Makefile-eshell.make                          |   1 +
 lisp/Makefile-help.make                            |   1 +
 lisp/Makefile-ibuffer.make                         |   1 +
 lisp/Makefile-image.make                           |   1 +
 lisp/Makefile-isearch.make                         |   1 +
 lisp/Makefile-make-mode.make                       |   1 +
 lisp/Makefile-man.make                             |   1 +
 lisp/Makefile-re-builder.make                      |   1 +
 lisp/Makefile-timezone.make                        |   1 +
 lisp/casual-ediff-settings.el                      | 100 +++++++
 lisp/casual-ediff-utils.el                         | 242 +++++++++++++++++
 lisp/casual-ediff.el                               | 295 +++++++++++++++++++++
 tests/Makefile                                     |   5 +
 tests/casual-ediff-test-utils.el                   |  39 +++
 tests/test-casual-ediff-settings.el                |  60 +++++
 tests/test-casual-ediff-utils.el                   |  98 +++++++
 tests/test-casual-ediff.el                         |  56 ++++
 tests/test-casual-eshell-settings.el               |   1 +
 tests/test-casual-eshell-utils.el                  |   4 -
 tests/test-casual-timezone-utils.el                |   9 +-
 34 files changed, 1044 insertions(+), 9 deletions(-)

diff --git a/docs/ediff.org b/docs/ediff.org
new file mode 100644
index 0000000000..19b9ae9c24
--- /dev/null
+++ b/docs/ediff.org
@@ -0,0 +1,113 @@
+* Ediff
+#+CINDEX: Ediff
+#+VINDEX: casual-ediff-tmenu
+
+Casual Ediff is a user interface for Ediff ([[info:ediff#Top][Ediff]]), a 
visual interface for the Unix diff and patch utilities. Casual Ediff strives to 
improve the usability of Ediff by simplifying the following workflows:
+
+- Comparing a modified and uncommitted version-controlled file with its most 
recent commit.
+- Resolving a merge conflicted file.
+
+Notable features of Casual Ediff include:
+
+- Context aware menu items.
+- Menu design tuned for side-by-side comparison.
+- For a merge conflict, the ability to resolve using both the conflicting diff 
versions.
+
+Shown below is screenshot of the Casual Ediff in action for a 
version-controlled file.
+
+[[file:images/casual-ediff-screenshot.png]]
+
+
+** Ediff Install
+:PROPERTIES:
+:CUSTOM_ID: ediff-install
+:END:
+
+#+CINDEX: Ediff Install
+
+In your initialization file, bind the Transient ~casual-ediff-tmenu~ to your 
key binding of preference.
+
+#+begin_src elisp :lexical no
+  (casual-ediff-install) ; run this to enable Casual Ediff
+  (add-hook 'ediff-keymap-setup-hook
+            (lambda ()
+              (keymap-set ediff-mode-map "C-o" #'casual-ediff-tmenu)))
+#+end_src
+
+If the current buffer is loaded with a version-controlled file, then the 
difference between that buffer's content with its most recent commit can be 
seen with the command ~casual-ediff-revision~. It is often convenient to bind 
this command. Shown below is an example of such a binding.
+
+#+begin_src elisp :lexical no
+  (keymap-global-set "<f15>" #'casual-ediff-revision)
+#+end_src
+
+Users who wish to call ~casual-ediff-revision~ via mouse can use the command 
~casual-ediff-revision-from-menu~. An example of its use is shown in the source 
example below (note this is a code fragment). 
+
+#+begin_src elisp :lexical no
+  (easy-menu-add-item
+       menu nil
+       ["Ediff revision…"
+        casual-ediff-revision-from-menu
+        :visible (and (bound-and-true-p buffer-file-name)
+                      (vc-registered (buffer-file-name)))
+        :help "Ediff this file with revision"])
+#+end_src
+
+
+#+TEXINFO: @subsubheading Ediff Variables
+
+Casual Ediff recommends the following variables be set as follows:
+
+| Variable                    | Value                     |
+|-----------------------------+---------------------------|
+| ediff-keep-variants         | nil                       |
+| ediff-window-setup-function | ediff-setup-windows-plain |
+| ediff-split-window-function | split-window-horizontally |
+
+** Ediff Usage
+#+CINDEX: Ediff Usage
+
+[[file:images/casual-ediff-basic-screenshot.png]]
+
+Casual Ediff (~casual-ediff-tmenu~) is invoked from the Ediff control window, 
typically using the binding {{{kbd(C-o)}}} (or your binding of preference).
+
+The menu comprises of sections laid out horizontally that correspond to source 
of the diffs. The number of sections and their contents are context-dependent 
on the type of content to be compared.
+
+The following sections are offered in the menu:
+
+- A :: Context-dependent commands for Ediff buffer A.
+- B :: Context-dependent commands for Ediff buffer B.
+- C :: Context-dependent commands for Ediff buffer C, if available.
+- Diff :: Ediff commands not related to a specific buffer.
+  
+Navigation between diffs are bound to the {{{kbd(p)}}} and {{{kbd(n)}}} keys 
for previous and next diff respectively. 
+
+*** Ediff Basic Operation
+
+[[file:images/casual-ediff-basic-screenshot.png]]
+
+If two writeable files are compared, Casual Ediff provides the ability to 
write diffs from either side A or B to each other. Unwanted changes to a side 
can be restored. 
+
+*** Comparing a Version-Controlled File
+
+[[file:images/casual-ediff-screenshot.png]]
+
+Casual Ediff provides a dedicated command to compare a version-controlled file 
with its latest commit: ~casual-ediff-revision~. This command short-circuits 
the prompts asked for by the command  ~ediff-revision~ which asks for the 
source file and the revision to compare it to. Call ~casual-ediff-revision~ 
from the window of a version-controlled file, typically by key binding of 
preference.
+
+The A side will hold the last committed revision of a file. The B side will 
hold current source file.
+
+Users desiring a mouse-driven approach to calling ~casual-ediff-revision~ 
should use the command ~casual-ediff-revision-from-menu~ invoked from either 
the top-level or context menu.
+
+Note that both ~casual-ediff-revision~ and ~casual-ediff-revision-from-menu~ 
will checkpoint the window configuration before invoking the Ediff session and 
restore it after the session is complete.
+
+*** Resolving a Merge Conflict
+
+[[file:images/casual-ediff-merge-conflict.png]]
+
+Magit offers a means of invoking Ediff to resolve a merge conflict via the 
{{{kbd(e)}}} binding from the ~magit-status~ ([[info:magit#Status 
Buffer][(magit) Status Buffer]]) window.
+
+Calling ~casual-ediff-tmenu~ will show the menu shown above. Note that in the 
C section, there are commands to merge both the variant A and variant B diffs 
using the bindings {{{kbd(mab)}}} or {{{kbd(mba)}}}, depending on the desired 
order the diffs are to be merged.
+
+
+*** Ediff Unicode Symbol Support
+By enabling “{{{kbd(u)}}} Use Unicode Symbols” from the Settings menu, Casual 
Ediff will use Unicode symbols as appropriate in its menus.
+
diff --git a/docs/images/casual-ediff-basic-screenshot.png 
b/docs/images/casual-ediff-basic-screenshot.png
new file mode 100644
index 0000000000..1d70b4834d
Binary files /dev/null and b/docs/images/casual-ediff-basic-screenshot.png 
differ
diff --git a/docs/images/casual-ediff-merge-conflict.png 
b/docs/images/casual-ediff-merge-conflict.png
new file mode 100644
index 0000000000..d0220f71ed
Binary files /dev/null and b/docs/images/casual-ediff-merge-conflict.png differ
diff --git a/docs/images/casual-ediff-screenshot.png 
b/docs/images/casual-ediff-screenshot.png
new file mode 100644
index 0000000000..45902acc31
Binary files /dev/null and b/docs/images/casual-ediff-screenshot.png differ
diff --git a/lisp/Makefile b/lisp/Makefile
index b26f8b2433..b65da829cc 100644
--- a/lisp/Makefile
+++ b/lisp/Makefile
@@ -26,6 +26,7 @@ calc-tests                                    \
 calendar-tests                                 \
 compile-tests                                  \
 dired-tests                                    \
+ediff-tests                                    \
 editkit-tests                                  \
 elisp-tests                                    \
 eshell-tests                                   \
@@ -70,6 +71,10 @@ compile-tests:
 dired-tests:
        $(MAKE) -C $(SRC_DIR) -f Makefile-dired.make tests
 
+.PHONY: ediff-tests
+ediff-tests:
+       $(MAKE) -C $(SRC_DIR) -f Makefile-ediff.make tests
+
 .PHONY: editkit-tests
 editkit-tests:
        $(MAKE) -C $(SRC_DIR) -f Makefile-editkit.make tests
diff --git a/lisp/Makefile-agenda.make b/lisp/Makefile-agenda.make
index ad3896dd6f..8f990993d0 100644
--- a/lisp/Makefile-agenda.make
+++ b/lisp/Makefile-agenda.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-bibtex.make b/lisp/Makefile-bibtex.make
index ffc38630ca..4960bfb80a 100644
--- a/lisp/Makefile-bibtex.make
+++ b/lisp/Makefile-bibtex.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-bookmarks.make b/lisp/Makefile-bookmarks.make
index 5783f1d17c..e1716f617f 100644
--- a/lisp/Makefile-bookmarks.make
+++ b/lisp/Makefile-bookmarks.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-calc.make b/lisp/Makefile-calc.make
index 2021e916e4..8b9bb41b8e 100644
--- a/lisp/Makefile-calc.make
+++ b/lisp/Makefile-calc.make
@@ -53,6 +53,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transpose-frame-current   \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(EMACS_ELPA_DIR)/magit-current             \
 -L $(EMACS_ELPA_DIR)/magit-section-current     \
 -L $(EMACS_ELPA_DIR)/dash-current              \
diff --git a/lisp/Makefile-calendar.make b/lisp/Makefile-calendar.make
index 01caf35f33..1d80048aa2 100644
--- a/lisp/Makefile-calendar.make
+++ b/lisp/Makefile-calendar.make
@@ -27,6 +27,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transpose-frame-current   \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(EMACS_ELPA_DIR)/magit-current             \
 -L $(EMACS_ELPA_DIR)/magit-section-current     \
 -L $(EMACS_ELPA_DIR)/dash-current              \
diff --git a/lisp/Makefile-compile.make b/lisp/Makefile-compile.make
index ab09c7f592..6cf74554a0 100644
--- a/lisp/Makefile-compile.make
+++ b/lisp/Makefile-compile.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-eshell.make b/lisp/Makefile-ediff.make
similarity index 86%
copy from lisp/Makefile-eshell.make
copy to lisp/Makefile-ediff.make
index 349f653096..faa6772349 100644
--- a/lisp/Makefile-eshell.make
+++ b/lisp/Makefile-ediff.make
@@ -16,15 +16,16 @@
 
 include Makefile--defines.make
 
-PACKAGE_NAME=casual-eshell
-ELISP_INCLUDES=casual-eshell-utils.el  \
-casual-eshell-settings.el
+PACKAGE_NAME=casual-ediff
+ELISP_INCLUDES=casual-ediff-utils.el   \
+casual-ediff-settings.el
 ELISP_PACKAGES=
-ELISP_TEST_INCLUDES=casual-eshell-test-utils.el
+ELISP_TEST_INCLUDES=casual-ediff-test-utils.el
 PACKAGE_PATHS=                                 \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(EMACS_ELPA_DIR)/magit-current             \
 -L $(EMACS_ELPA_DIR)/magit-section-current     \
 -L $(EMACS_ELPA_DIR)/dash-current              \
diff --git a/lisp/Makefile-editkit.make b/lisp/Makefile-editkit.make
index d75e1e9805..ab3350e96a 100644
--- a/lisp/Makefile-editkit.make
+++ b/lisp/Makefile-editkit.make
@@ -26,6 +26,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transpose-frame-current   \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(EMACS_ELPA_DIR)/magit-current             \
 -L $(EMACS_ELPA_DIR)/magit-section-current     \
 -L $(EMACS_ELPA_DIR)/dash-current              \
diff --git a/lisp/Makefile-elisp.make b/lisp/Makefile-elisp.make
index f9d2fb8466..45052935a4 100644
--- a/lisp/Makefile-elisp.make
+++ b/lisp/Makefile-elisp.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-eshell.make b/lisp/Makefile-eshell.make
index 349f653096..d3e178ea2f 100644
--- a/lisp/Makefile-eshell.make
+++ b/lisp/Makefile-eshell.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(EMACS_ELPA_DIR)/magit-current             \
 -L $(EMACS_ELPA_DIR)/magit-section-current     \
 -L $(EMACS_ELPA_DIR)/dash-current              \
diff --git a/lisp/Makefile-help.make b/lisp/Makefile-help.make
index ae3368698c..8de0cca5ea 100644
--- a/lisp/Makefile-help.make
+++ b/lisp/Makefile-help.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-ibuffer.make b/lisp/Makefile-ibuffer.make
index 2521701ea9..d524770e6f 100644
--- a/lisp/Makefile-ibuffer.make
+++ b/lisp/Makefile-ibuffer.make
@@ -24,6 +24,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-image.make b/lisp/Makefile-image.make
index 7cb0c1f018..1f000821e9 100644
--- a/lisp/Makefile-image.make
+++ b/lisp/Makefile-image.make
@@ -24,6 +24,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-isearch.make b/lisp/Makefile-isearch.make
index bc761cbeb9..01ece3c994 100644
--- a/lisp/Makefile-isearch.make
+++ b/lisp/Makefile-isearch.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 .PHONY: tests compile regression
diff --git a/lisp/Makefile-make-mode.make b/lisp/Makefile-make-mode.make
index f567e868f0..26d3697949 100644
--- a/lisp/Makefile-make-mode.make
+++ b/lisp/Makefile-make-mode.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-man.make b/lisp/Makefile-man.make
index 76dd12ead4..5bb4f4a1d6 100644
--- a/lisp/Makefile-man.make
+++ b/lisp/Makefile-man.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-re-builder.make b/lisp/Makefile-re-builder.make
index 52ddc528df..ff2000bcee 100644
--- a/lisp/Makefile-re-builder.make
+++ b/lisp/Makefile-re-builder.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/Makefile-timezone.make b/lisp/Makefile-timezone.make
index b293c6824e..d8b2fb1c04 100644
--- a/lisp/Makefile-timezone.make
+++ b/lisp/Makefile-timezone.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS=                                        \
 -L $(EMACS_ELPA_DIR)/compat-current            \
 -L $(EMACS_ELPA_DIR)/seq-current               \
 -L $(EMACS_ELPA_DIR)/transient-current         \
+-L $(EMACS_ELPA_DIR)/cond-let-current          \
 -L $(CASUAL_LIB_LISP_DIR)
 
 include Makefile--rules.make
diff --git a/lisp/casual-ediff-settings.el b/lisp/casual-ediff-settings.el
new file mode 100644
index 0000000000..f017da6633
--- /dev/null
+++ b/lisp/casual-ediff-settings.el
@@ -0,0 +1,100 @@
+;;; casual-ediff-settings.el --- Casual Ediff 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 'ediff)
+(require 'casual-lib)
+
+(transient-define-prefix casual-ediff-settings-tmenu ()
+  "Casual Ediff settings menu."
+  ["Casual Ediff: Settings"
+   [("k" "Keep Variants"
+     casual-ediff-customize-ediff-keep-variants
+     :description (lambda () (casual-lib-checkbox-label ediff-keep-variants
+                                                   "Keep Variants")))
+
+    ("w" "Window Setup Function"
+     casual-ediff-customize-ediff-window-setup-function)
+
+    ("s" "Split Window Function"
+     casual-ediff-customize-ediff-split-window-function)]
+
+   [("G" "Ediff Group" casual-ediff-customize-group)
+    (casual-lib-customize-unicode)
+    (casual-lib-customize-hide-navigation)]]
+
+  [:class transient-row
+   (casual-lib-quit-one)
+   ("a" "About" casual-ediff-about :transient nil)
+   (casual-lib-quit-all)])
+
+(defun casual-ediff-customize-group ()
+  "Customize Ediff group."
+  (interactive)
+  (customize-group "ediff"))
+
+(defun casual-ediff-customize-ediff-keep-variants ()
+  "Customize `ediff-keep-variants'."
+  (interactive)
+  (customize-variable 'ediff-keep-variants))
+
+(defun casual-ediff-customize-ediff-window-setup-function ()
+  "Customize `ediff-window-setup-function'."
+  (interactive)
+  (customize-variable 'ediff-window-setup-function))
+
+(defun casual-ediff-customize-ediff-split-window-function ()
+  "Customize `ediff-split-window-function'."
+  (interactive)
+  (customize-variable 'ediff-split-window-function))
+
+(defun casual-ediff-about-ediff ()
+  "Casual Ediff is a Transient menu for Ediff.
+
+Learn more about using Casual Ediff 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 Ediff, consider making a modest financial
+contribution to help support its development and maintenance.
+URL `https://www.buymeacoffee.com/kickingvegas'
+
+Casual Ediff was conceived and crafted by Charles Choi in San Francisco,
+California.
+
+Thank you for using Casual Ediff.
+
+Always choose love."
+  (ignore))
+
+(defun casual-ediff-about ()
+  "About information for Casual Ediff."
+  (interactive)
+  (describe-function #'casual-ediff-about-ediff))
+
+(provide 'casual-ediff-settings)
+;;; casual-ediff-settings.el ends here
diff --git a/lisp/casual-ediff-utils.el b/lisp/casual-ediff-utils.el
new file mode 100644
index 0000000000..543a3712e4
--- /dev/null
+++ b/lisp/casual-ediff-utils.el
@@ -0,0 +1,242 @@
+;;; casual-ediff-utils.el --- Casual Eshell 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 'ediff)
+(require 'casual-lib)
+
+;; these defvars are here to let cc-ediff-mode.el compile clean
+(defvar ediff-buffer-A)
+(defvar ediff-buffer-B)
+(defvar ediff-buffer-C)
+(defvar ediff-merge-job)
+(defvar ediff-ancestor-buffer)
+
+;; CC: I set my Ediff variables in `custom-set-variables'
+;; Use your own preference.
+;; '(ediff-keep-variants nil)
+;; '(ediff-split-window-function 'split-window-horizontally)
+;; '(ediff-window-setup-function 'ediff-setup-windows-plain)
+
+(defconst casual-ediff-unicode-db
+  '((:previous . '("↑" "Previous"))
+    (:next . '("↓" "Next"))
+    (:scroll-to-right . '("↤" "Scroll to right"))
+    (:scroll-to-left . '("↦" "Scroll to left"))
+    (:refresh . '("⟲" "Refresh")))
+
+  "Unicode symbol DB to use for Ediff Transient menus.")
+
+(defun casual-ediff-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-ediff-unicode-db))
+
+
+;; -------------------------------------------------------------------
+;;; Format
+
+(defun casual-ediff--buffer-description (ebuf slot &optional extend)
+  "Format name of buffer EBUF in SLOT with option to EXTEND."
+  (let ((bufname (buffer-name ebuf))
+        (fwidth (frame-width)))
+    (casual-ediff--display-filename bufname slot extend fwidth)))
+
+(defun casual-ediff--display-filename (filename slot extend fwidth)
+  "Reformat FILENAME for Ediff display using SLOT, EXTEND, and FWIDTH."
+  (if (and (string-match "~\\([[:xdigit:]]*\\)~$" filename) (< fwidth 95))
+      (let* ((commit-hash (match-string 1 filename))
+             (short-hash (truncate-string-to-width commit-hash 7 0 nil t))
+             (filename (string-replace commit-hash short-hash filename)))
+        (format "%s: %s" slot filename))
+    (if extend
+        (let* ((bwidth (- (/ fwidth 2) 6))
+               (ext-fmt (concat "%s: " (format "%%-%ss" bwidth))))
+          (format ext-fmt slot filename))
+      (format "%s: %s" slot filename))))
+
+
+;; -------------------------------------------------------------------
+;;; Ediff Functions
+(defun casual-ediff-info ()
+  "Open Info for Ediff."
+  (interactive) (info "(ediff) Top"))
+
+(defvar casual-ediff--revision-session-p nil
+  "If t then `casual-ediff--internal-last-revision' has been called.
+This state variable is used to insert added behavior to the advised
+function `ediff-janitor'.")
+
+(defvar casual-ediff--installed-p nil
+  "If t then Casual Ediff is initialized.")
+
+(defun casual-ediff-revision-from-menu (e)
+  "Invoke `casual-ediff-revision' on E with variable `buffer-file-name'."
+  (interactive "e")
+  (ignore e)
+  (casual-ediff-revision))
+
+(defun casual-ediff-revision ()
+  "Run Ediff comparing current file with last committed version.
+
+This function handles the interactive concerns found in
+`ediff-revision'. This function will also test if a diff should apply to
+the current buffer."
+  (interactive)
+  (when (and (bound-and-true-p buffer-file-name)
+             (vc-registered (buffer-file-name)))
+    (if (and (buffer-modified-p)
+            (y-or-n-p (format "Buffer %s is modified.  Save buffer? "
+                               (buffer-name))))
+      (save-buffer (current-buffer)))
+    (message buffer-file-name)
+    (casual-ediff--internal-last-revision))
+
+  (cond ((not (bound-and-true-p buffer-file-name))
+         (message (concat (buffer-name) " is not a file that can be diffed.")))
+        ((not (vc-registered buffer-file-name))
+         (message (concat buffer-file-name " is not under version 
control.")))))
+
+(defun casual-ediff--internal-last-revision ()
+  "Implementation of Ediff comparing current file with last committed version.
+
+This function handles the actual diff behavior called by `ediff-revision'."
+  (let ((rev1 "")
+        (rev2 ""))
+    (setq casual-ediff--revision-session-p t)
+    (ediff-load-version-control)
+    (funcall
+     (intern (format "ediff-%S-internal" ediff-version-control-package))
+     rev1 rev2 nil)))
+
+(defun casual-ediff-janitor (ask keep-variants)
+  "Kill buffers A, B, and, possibly, C, if these buffers aren't modified.
+In merge jobs, buffer C is not deleted here, but rather according to
+`ediff-quit-merge-hook'.
+ASK non-nil means ask the user whether to keep each unmodified buffer, unless
+KEEP-VARIANTS is non-nil, in which case buffers are never killed.
+A side effect of cleaning up may be that you should be careful when comparing
+the same buffer in two separate Ediff sessions: quitting one of them might
+delete this buffer in another session as well.
+
+!!!: This method overrides the original Ediff function."
+  (let ((ask (if (and (boundp 'casual-ediff--revision-session-p)
+                      casual-ediff--revision-session-p)
+                 nil
+               ask)))
+    (ediff-dispose-of-variant-according-to-user
+     ediff-buffer-A 'A ask keep-variants)
+    ;; !!!: Test global state variable `casual-ediff--revision-session-p' to
+    ;; determine if the modified repo file should be kept.
+    ;; Guarding in place to hopefully avoid side-effects when `ediff-janitor' 
is
+    ;; called from other Ediff functions. Informal testing has not revealed any
+    ;; side-effects but YOLO.
+    (if (and (boundp 'casual-ediff--revision-session-p)
+             casual-ediff--revision-session-p)
+        (ediff-dispose-of-variant-according-to-user
+         ;; CC Note: keep-variants argument is hard-coded to t to keep
+         ;; buffer holding modified repo file around.
+         ediff-buffer-B 'B t t)
+      (ediff-dispose-of-variant-according-to-user
+       ediff-buffer-B 'B ask keep-variants))
+    (if ediff-merge-job  ; don't del buf C if merging--del ancestor buf instead
+        (ediff-dispose-of-variant-according-to-user
+         ediff-ancestor-buffer 'Ancestor ask keep-variants)
+      (ediff-dispose-of-variant-according-to-user
+       ediff-buffer-C 'C ask keep-variants))
+    ;; CC Note: Reset global state variable `casual-ediff--revision-session-p'.
+    (if (and (boundp 'casual-ediff--revision-session-p)
+             casual-ediff--revision-session-p)
+        (setq casual-ediff--revision-session-p nil))))
+
+
+(defun casual-ediff--stash-window-configuration-for-ediff ()
+  "Store window configuration to register 🧊.
+
+Use of emoji is to avoid potential use of keyboard character to reference
+the register."
+  (window-configuration-to-register ?🧊))
+
+(defun casual-ediff--restore-window-configuration-for-ediff ()
+  "Restore window configuration from register 🧊.
+
+Use of emoji is to avoid potential use of keyboard character to reference
+the register."
+  (jump-to-register ?🧊))
+
+(defun casual-ediff--restore-and-save-diff (key)
+  "Restore diff and save Ediff buffer referenced by KEY."
+  (ediff-restore-diff nil key)
+  (casual-ediff--save-buffer key))
+
+(defun casual-ediff--save-buffer (key)
+  "Save Ediff buffer referenced by KEY."
+  (setq last-command-event key)
+  (ediff-save-buffer nil))
+
+(defun casual-ediff--buffer-read-only-p (buf)
+  "Predicate to test if BUF is read-only."
+  (with-current-buffer buf
+    (if buffer-read-only t nil)))
+
+(defun casual-ediff--split-window-vertically-description ()
+  "Provide string label for state of `split-window-vertically'."
+  (if (eq ediff-split-window-function 'split-window-vertically)
+      "Side by side"
+    "On top"))
+
+;; The implementations of `casual-ediff-copy-AB-to-C' and
+;; `casual-ediff-copy-BA-to-C' are adapted from code written by killdash9 from
+;; the following Stack Overflow post.
+;; 
https://stackoverflow.com/questions/9656311/conflict-resolution-with-emacs-ediff-how-can-i-take-the-changes-of-both-version/29757750#29757750
+
+(defun casual-ediff-copy-AB-to-C ()
+  "Resolve merge conflict by inserting difference from buffer A then buffer B."
+  (interactive)
+  (ediff-copy-diff
+   ediff-current-difference nil 'C nil
+   (concat
+    (ediff-get-region-contents
+     ediff-current-difference 'A ediff-control-buffer)
+    (ediff-get-region-contents
+     ediff-current-difference 'B ediff-control-buffer))))
+
+(defun casual-ediff-copy-BA-to-C ()
+  "Resolve merge conflict by inserting difference from buffer B then buffer A."
+  (interactive)
+  (ediff-copy-diff
+   ediff-current-difference nil 'C nil
+   (concat
+    (ediff-get-region-contents
+     ediff-current-difference 'B ediff-control-buffer)
+    (ediff-get-region-contents
+     ediff-current-difference 'A ediff-control-buffer))))
+
+(provide 'casual-ediff-utils)
+;;; casual-ediff-utils.el ends here
diff --git a/lisp/casual-ediff.el b/lisp/casual-ediff.el
new file mode 100644
index 0000000000..2dd6ff965d
--- /dev/null
+++ b/lisp/casual-ediff.el
@@ -0,0 +1,295 @@
+;;; casual-ediff.el --- Transient UI for Eshell -*- 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:
+
+;; This library provides a Transient-based user interface for Ediff.
+
+;; INSTALLATION
+
+;; To install Casual Ediff, the function `casual-ediff-install' should be 
called
+;; from your Emacs initialization file. You will also need to bind
+;; `casual-ediff-tmenu' to your key binding of preference.
+
+;; (require 'casual-ediff) ; optional if using autoloaded menu
+;; (casual-ediff-install) ; run this to enable Casual Ediff
+;; (add-hook 'ediff-keymap-setup-hook
+;;           (lambda ()
+;;             (keymap-set ediff-mode-map "C-o" #'casual-ediff-tmenu)))
+
+;; Notes
+;; - `casual-ediff-install' will:
+;;   * override advise the function `ediff-janitor'.
+;;   * in-memory set the variable `ediff-window-setup-function' to plain.
+;; - The way Ediff handles keymaps necessitates the configuration of
+;;   `ediff-keymap-setup-hook' as shown above.
+
+;;; Code:
+(require 'casual-ediff-settings)
+(require 'casual-ediff-utils)
+
+;;;###autoload (autoload 'casual-ediff-tmenu "casual-ediff" nil t)
+(transient-define-prefix casual-ediff-tmenu ()
+  :refresh-suffixes t
+  [["A"
+    :pad-keys t
+    :description (lambda () (casual-ediff--buffer-description
+                        ediff-buffer-A
+                        "A"
+                        (not ediff-buffer-C)))
+    ("ab" "A→B" ediff-copy-A-to-B
+     :transient t
+     :if (lambda () (and
+                (not ediff-buffer-C)
+                ediff-buffer-B
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-B)))))
+
+    ("ac" "A→C" ediff-copy-A-to-C
+     :transient t
+     :if (lambda () (and
+                ediff-buffer-C
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-C)))))
+
+    ("ra" "Restore A"
+     (lambda ()
+       "Restore and save prior state of buffer A."
+       (interactive)
+       (casual-ediff--restore-and-save-diff ?a))
+     :transient t
+     :if (lambda () (and
+                (or ediff-buffer-B ediff-buffer-C)
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+    ("wa" "Save A"
+     (lambda ()
+       "Save buffer A."
+       (interactive)
+       (casual-ediff--save-buffer ?a))
+     :transient t
+     :if (lambda () (and
+                (or ediff-buffer-B ediff-buffer-C)
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-A))))
+     :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-A)))]
+
+   ;; !!!: A diff B
+   ["Diff"
+    :pad-keys t
+    :if (lambda () (not ediff-buffer-C))
+    ("p" "↑" ediff-previous-difference
+     :description (lambda () (casual-ediff-unicode-get :previous))
+     :transient t)
+    ("n" "↓" ediff-next-difference
+     :description (lambda () (casual-ediff-unicode-get :next))
+     :transient t)
+
+    ("<" "↤" ediff-scroll-horizontally
+     :description (lambda () (casual-ediff-unicode-get :scroll-to-right))
+     :transient t)
+    (">" "↦" ediff-scroll-horizontally
+     :description (lambda () (casual-ediff-unicode-get :scroll-to-left))
+     :transient t)
+
+    ("!" "⟲" ediff-update-diffs
+     :description (lambda () (casual-ediff-unicode-get :refresh))
+     :transient t)
+
+    ("|" "H/V" ediff-toggle-split
+     :transient t
+     :description casual-ediff--split-window-vertically-description)
+    ("#" "Skip 𝑤𝑠" ediff-toggle-skip-similar
+     :transient t
+     :description (lambda ()
+                    (casual-lib-checkbox-label ediff-ignore-similar-regions
+                                               "Skip Space")))]
+
+   ["B"
+    :pad-keys t
+    :description (lambda () (casual-ediff--buffer-description ediff-buffer-B 
"B"))
+    ("ba" "A←B" ediff-copy-B-to-A
+     :transient t
+     :if (lambda () (and
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+    ("bc" "B→C" ediff-copy-B-to-C
+     :transient t
+     :if (lambda () (and
+                ediff-buffer-C
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-C)))))
+
+    ("rb" "Restore B"
+     (lambda ()
+       "Restore and save prior state of buffer B."
+       (interactive)
+       (casual-ediff--restore-and-save-diff ?b))
+     :transient t
+     :if (lambda () (and
+                ediff-buffer-B
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+                (not (string= (file-name-extension (buffer-name 
ediff-buffer-B))
+                              "~{index}")))))
+
+    ("wb" "Save B"
+     (lambda ()
+       "Save buffer B."
+       (interactive)
+       (casual-ediff--save-buffer ?b))
+     :transient t
+     :if (lambda () (and
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+                (not (string= (file-name-extension (buffer-name 
ediff-buffer-B))
+                              "~{index}"))))
+     :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-B)))]
+
+   ;; !!!: A B diff C
+   ["Diff"
+    :pad-keys t
+    :if (lambda () (and ediff-buffer-C t))
+    ("p" "↑" ediff-previous-difference
+     :description (lambda () (casual-ediff-unicode-get :previous))
+     :transient t)
+    ("n" "↓" ediff-next-difference
+     :description (lambda () (casual-ediff-unicode-get :next))
+     :transient t)
+
+    (">" "↤" ediff-scroll-horizontally
+     :description (lambda () (casual-ediff-unicode-get :scroll-to-right))
+     :transient t)
+    ("<" "↦" ediff-scroll-horizontally
+     :description (lambda () (casual-ediff-unicode-get :scroll-to-left))
+     :transient t)
+
+    ("!" "⟲" ediff-update-diffs
+     :description (lambda () (casual-ediff-unicode-get :refresh))
+     :transient t)
+    ("|" "H/V" ediff-toggle-split
+     :description casual-ediff--split-window-vertically-description
+     :transient t)
+    ("#" "Skip 𝑤𝑠" ediff-toggle-skip-similar
+     :transient t
+     :description (lambda ()
+                    (casual-lib-checkbox-label ediff-ignore-similar-regions
+                                               "Skip Space")))]
+
+   ["C"
+    :pad-keys t
+    :if (lambda () (if ediff-buffer-C t nil))
+    :description (lambda () (casual-ediff--buffer-description ediff-buffer-C 
"C"))
+    ("cb" "B←C" ediff-copy-C-to-B
+     :transient t
+     :if (lambda () (and
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+                (not (string= (file-name-extension (buffer-name 
ediff-buffer-B))
+                              "~{index}")))))
+
+    ("ca" "A←C" ediff-copy-C-to-A
+     :transient t
+     :if (lambda () (and
+                (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+    ("mab" "Merge A,B to C" casual-ediff-copy-AB-to-C
+     :transient t
+     :if (lambda () (string-equal (buffer-name ediff-buffer-C) 
"*ediff-merge*")))
+
+    ("mba" "Merge B,A to C" casual-ediff-copy-BA-to-C
+     :transient t
+     :if (lambda () (string-equal (buffer-name ediff-buffer-C) 
"*ediff-merge*")))
+
+    ("rc" "Restore C"
+     (lambda ()
+       "Restore and save prior state of buffer C."
+       (interactive)
+       (casual-ediff--restore-and-save-diff ?c))
+     :transient t
+     :if (lambda ()
+           (if (not (casual-ediff--buffer-read-only-p ediff-buffer-C))
+               (if (not (string= (buffer-name ediff-buffer-C) "*ediff-merge*"))
+                   t
+                 nil)
+             nil))
+     :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-C)))
+
+    ("rm" "Restore Prior to Last Merge"
+     (lambda ()
+       "Call `ediff-restore-diff-in-merge-buffer' using current diff.
+
+Note that this command will restore only the state of the *ediff-merge*
+buffer prior to the previous merge. To avoid any changes to the
+conflicted file, exit Ediff and when prompted to save the merge file,
+reply with no."
+       (interactive)
+       (ediff-restore-diff-in-merge-buffer nil))
+     :transient t
+     :if (lambda ()
+           (if (not (casual-ediff--buffer-read-only-p ediff-buffer-C))
+               (if (string= (buffer-name ediff-buffer-C) "*ediff-merge*")
+                   t
+                 nil)
+             nil)))]]
+
+  [:class transient-row
+   (casual-lib-quit-one)
+   ("i" "Status" ediff-status-info)
+   ("I" "ⓘ" ediff-documentation)
+   ("," "Settings" casual-ediff-settings-tmenu)
+   ("q" "Quit Ediff" ediff-quit)])
+
+;;;###autoload (autoload 'casual-ediff-install "casual-ediff" nil t)
+(defun casual-ediff-install ()
+  "Install Casual Ediff."
+  (interactive)
+  (setq casual-ediff--installed-p t)
+
+  ;; CC: I set my Ediff variables in `custom-set-variables'
+  ;; Use your own preference.
+  ;; '(ediff-keep-variants nil)
+  ;; '(ediff-split-window-function 'split-window-horizontally)
+  ;; '(ediff-window-setup-function 'ediff-setup-windows-plain)
+
+  (unless (eq ediff-window-setup-function #'ediff-setup-windows-plain)
+    (message
+     "Overriding ediff-window-setup-function to ediff-setup-windows-plain. \
+Consider customizing to always set this variable to plain.")
+    (setq ediff-window-setup-function #'ediff-setup-windows-plain))
+
+  (add-hook
+   'ediff-before-setup-hook
+   #'casual-ediff--stash-window-configuration-for-ediff)
+  (add-hook
+   'ediff-after-quit-hook-internal
+   #'casual-ediff--restore-window-configuration-for-ediff)
+  (advice-add 'ediff-janitor :override #'casual-ediff-janitor))
+
+;;;###autoload (autoload 'casual-ediff-uninstall "casual-ediff" nil t)
+(defun casual-ediff-uninstall ()
+  "Uninstall Casual Ediff."
+  (interactive)
+  (advice-remove 'ediff-janitor #'casual-ediff-janitor)
+
+  (remove-hook
+   'ediff-before-setup-hook
+   #'casual-ediff--stash-window-configuration-for-ediff)
+  (remove-hook
+   'ediff-after-quit-hook-internal
+   #'casual-ediff--restore-window-configuration-for-ediff)
+
+  (setq casual-ediff--installed-p nil))
+
+(provide 'casual-ediff)
+;;; casual-ediff.el ends here
diff --git a/tests/Makefile b/tests/Makefile
index fa1ef9c71a..82b98bed4e 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -25,6 +25,7 @@ calc-tests                                    \
 calendar-tests                                 \
 compile-tests                                  \
 dired-tests                                    \
+ediff-tests                                    \
 editkit-tests                                  \
 elisp-tests                                    \
 eshell-tests                                   \
@@ -47,6 +48,7 @@ calc-tests                                    \
 calendar-tests                                 \
 compile-tests                                  \
 dired-tests                                    \
+ediff-tests                                    \
 editkit-tests                                  \
 elisp-tests                                    \
 eshell-tests                                   \
@@ -83,6 +85,9 @@ compile-tests:
 dired-tests:
        $(MAKE) -C $(SRC_DIR) $@
 
+ediff-tests:
+       $(MAKE) -C $(SRC_DIR) $@
+
 editkit-tests:
        $(MAKE) -C $(SRC_DIR) $@
 
diff --git a/tests/casual-ediff-test-utils.el b/tests/casual-ediff-test-utils.el
new file mode 100644
index 0000000000..e7ec8e9a9f
--- /dev/null
+++ b/tests/casual-ediff-test-utils.el
@@ -0,0 +1,39 @@
+;;; casual-ediff-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-ediff-setup ()
+  "Casual Ediff setup."
+    )
+
+(defun casualt-ediff-breakdown ()
+  "Casual man breakdown."
+  )
+
+(provide 'casual-ediff-test-utils)
+;;; casual-ediff-test-utils.el ends here
diff --git a/tests/test-casual-ediff-settings.el 
b/tests/test-casual-ediff-settings.el
new file mode 100644
index 0000000000..6c9f3dee6f
--- /dev/null
+++ b/tests/test-casual-ediff-settings.el
@@ -0,0 +1,60 @@
+;;; test-casual-ediff-settings.el --- Casual Elisp 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-ediff-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-ediff-settings)
+
+(ert-deftest test-casual-ediff-settings-tmenu ()
+  (let ()
+    (cl-letf ((casualt-mock #'casual-ediff-customize-group)
+              (casualt-mock #'casual-ediff-customize-ediff-keep-variants)
+              (casualt-mock 
#'casual-ediff-customize-ediff-split-window-function)
+              (casualt-mock 
#'casual-ediff-customize-ediff-window-setup-function)
+              (casualt-mock #'casual-lib-customize-casual-lib-hide-navigation)
+              (casualt-mock #'casual-lib-customize-casual-lib-use-unicode)
+              (casualt-mock #'casual-ediff-about))
+
+      (let ((test-vectors
+             '(
+               (:binding "k" :command 
casual-ediff-customize-ediff-keep-variants)
+               (:binding "w" :command 
casual-ediff-customize-ediff-window-setup-function)
+               (:binding "s" :command 
casual-ediff-customize-ediff-split-window-function)
+               (:binding "G" :command casual-ediff-customize-group)
+               (:binding "u" :command 
casual-lib-customize-casual-lib-use-unicode)
+               (:binding "n" :command 
casual-lib-customize-casual-lib-hide-navigation)
+               (:binding "a" :command casual-ediff-about))))
+
+        (casualt-suffix-testcase-runner test-vectors
+                                        #'casual-ediff-settings-tmenu
+                                        '(lambda () (random 5000)))))))
+
+(ert-deftest test-casual-ediff-about ()
+  (should (stringp (casual-ediff-about))))
+
+(provide 'test-casual-ediff-settings)
+;;; test-casual-ediff-setttings.el ends here
diff --git a/tests/test-casual-ediff-utils.el b/tests/test-casual-ediff-utils.el
new file mode 100644
index 0000000000..32570958d9
--- /dev/null
+++ b/tests/test-casual-ediff-utils.el
@@ -0,0 +1,98 @@
+;;; test-casual-ediff-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-ediff-test-utils)
+(require 'casual-ediff-utils)
+
+(ert-deftest test-casual-ediff-unicode-get ()
+  (let ((casual-lib-use-unicode nil))
+    (should (string-equal (casual-ediff-unicode-get :previous) "Previous"))
+    (should (string-equal (casual-ediff-unicode-get :next) "Next"))
+    (should (string-equal (casual-ediff-unicode-get :scroll-to-right) "Scroll 
to right"))
+    (should (string-equal (casual-ediff-unicode-get :scroll-to-left) "Scroll 
to left"))
+    (should (string-equal (casual-ediff-unicode-get :refresh) "Refresh")))
+
+  (let ((casual-lib-use-unicode t))
+    (should (string-equal (casual-ediff-unicode-get :previous) "↑"))
+    (should (string-equal (casual-ediff-unicode-get :next) "↓"))
+    (should (string-equal (casual-ediff-unicode-get :scroll-to-right) "↤"))
+    (should (string-equal (casual-ediff-unicode-get :scroll-to-left) "↦"))
+    (should (string-equal (casual-ediff-unicode-get :refresh) "⟲"))))
+
+
+(ert-deftest test-casual-ediff--display-filename ()
+  (let* ((control "A: fred.foo")
+         (filename "fred.foo")
+         (slot "A")
+         (extend nil)
+         (fwidth 120)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result)))
+
+  (let* ((control "B: fred.foo")
+         (filename "fred.foo")
+         (slot "B")
+         (extend nil)
+         (fwidth 120)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result)))
+
+  (let* ((control "B: fred.foo                                              ")
+         (filename "fred.foo")
+         (slot "B")
+         (extend t)
+         (fwidth 120)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result)))
+
+  (let* ((control "B: fred.foo.~ABCDEF…~")
+         (filename "fred.foo.~ABCDEF0123456~")
+         (slot "B")
+         (extend t)
+         (fwidth 80)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result)))
+
+  (let* ((control
+          "B: fred.foo.~ABCDEF0123456~                                         
                   ")
+         (filename "fred.foo.~ABCDEF0123456~")
+         (slot "B")
+         (extend t)
+         (fwidth 180)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result)))
+
+  (let* ((control
+          "B: fred.foo.~ABCDEF0123456~")
+         (filename "fred.foo.~ABCDEF0123456~")
+         (slot "B")
+         (extend nil)
+         (fwidth 180)
+         (result (casual-ediff--display-filename filename slot extend fwidth)))
+    (should (string-equal control result))))
+
+(provide 'test-casual-ediff-utils)
+;;; test-casual-ediff-utils.el ends here
diff --git a/tests/test-casual-ediff.el b/tests/test-casual-ediff.el
new file mode 100644
index 0000000000..dd5c1e1af8
--- /dev/null
+++ b/tests/test-casual-ediff.el
@@ -0,0 +1,56 @@
+;;; test-casual-ediff.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-ediff-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-ediff)
+
+;; Gah, testing casual-ediff-tmenu might be too hard.
+;; Resorting to manual testing.
+
+;; (ert-deftest test-casual-ediff-tmenu ()
+;;   (let ((tmpfile "casual-ediff-tmenu.txt"))
+;;     (casualt-ediff-setup)
+;;     (cl-letf (;;((symbol-function #') (lambda () t))
+;;               (casualt-mock #'ediff-previous-difference)
+;;               (casualt-mock #'ediff-next-difference)
+;;               ;;(casualt-mock #')
+;;               )
+
+;;       (let ((test-vectors
+;;              '((:binding "p" :command ediff-previous-difference)
+;;                (:binding "n" :command ediff-next-difference)
+;;                ;;(:binding "" :command ediff-main-)
+;;                )))
+
+;;         (casualt-suffix-testcase-runner test-vectors
+;;                                         #'casual-ediff-tmenu
+;;                                         '(lambda () (random 5000)))))
+;;     (casualt-ediff-breakdown)))
+
+(provide 'test-casual-ediff)
+;;; test-casual-ediff.el ends here
diff --git a/tests/test-casual-eshell-settings.el 
b/tests/test-casual-eshell-settings.el
index 267873ee41..33e9240e78 100644
--- a/tests/test-casual-eshell-settings.el
+++ b/tests/test-casual-eshell-settings.el
@@ -26,6 +26,7 @@
 
 (require 'ert)
 (require 'casual-eshell-test-utils)
+(require 'casual-lib-test-utils)
 (require 'casual-eshell-settings)
 
 (ert-deftest test-casual-eshell-settings-tmenu ()
diff --git a/tests/test-casual-eshell-utils.el 
b/tests/test-casual-eshell-utils.el
index f54d492bdb..b1b2e5a75d 100644
--- a/tests/test-casual-eshell-utils.el
+++ b/tests/test-casual-eshell-utils.el
@@ -46,9 +46,5 @@
     (should (string-equal (casual-eshell-unicode-get :clear) "⌫"))
     ))
 
-
-
-
-
 (provide 'test-casual-eshell-utils)
 ;;; test-casual-eshell-utils.el ends here
diff --git a/tests/test-casual-timezone-utils.el 
b/tests/test-casual-timezone-utils.el
index ee06be4998..5420381fb3 100644
--- a/tests/test-casual-timezone-utils.el
+++ b/tests/test-casual-timezone-utils.el
@@ -146,8 +146,15 @@
 
     (should (string-equal control result))))
 
+(defun in-daylight-saving-time-p ()
+  "Return t if the current local time is in Daylight Saving Time (DST), nil 
otherwise."
+  (let ((dst (nth 8 (decode-time (current-time)))))
+    (and dst (not (eq dst 0)))))
+
 (ert-deftest test-casual-timezone-remote-time-to-local ()
-  (let* ((read-date "2025-05-23 12:00")
+  (let* ((read-date (if (in-daylight-saving-time-p)
+                        "2025-05-23 11:00"
+                      "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)))

Reply via email to