branch: elpa/casual
commit 02de27d7cb9451c85fe77ff35927543e8ffa45eb
Author: Charles Choi <[email protected]>
Commit: Charles Choi <[email protected]>
Add CSV mode support
This change adds support for Emacs `csv-mode`.
Includes workaround fix for timezone calculation for test regression.
---
docs/casual.info | Bin 139918 -> 143571 bytes
docs/casual.org | 4 +-
docs/casual.texi | 108 ++++++++++++++-
docs/csv.org | 80 +++++++++++
docs/images/casual-csv-align-screenshot.png | Bin 0 -> 38846 bytes
docs/images/casual-csv-edit-screenshot.png | Bin 0 -> 203137 bytes
docs/images/casual-csv-settings-screenshot.png | Bin 0 -> 71135 bytes
docs/images/casual-csv-view-screenshot.png | Bin 0 -> 68512 bytes
docs/images/casual-csv-view-unicode-screenshot.png | Bin 0 -> 63214 bytes
lisp/Makefile | 5 +
lisp/Makefile-csv.make | 32 +++++
lisp/casual-csv-settings.el | 146 +++++++++++++++++++++
lisp/casual-csv-utils.el | 105 +++++++++++++++
lisp/casual-csv.el | 135 +++++++++++++++++++
lisp/casual-lib.el | 26 ++++
templates/lisp/casual-MODULE.el | 2 +-
templates/tests/test-casual-MODULE-settings.el | 8 +-
tests/Makefile | 5 +
tests/casual-csv-test-utils.el | 39 ++++++
tests/test-casual-csv-settings.el | 68 ++++++++++
tests/test-casual-csv-utils.el | 74 +++++++++++
tests/test-casual-csv.el | 91 +++++++++++++
tests/test-casual-timezone-utils.el | 6 +-
23 files changed, 923 insertions(+), 11 deletions(-)
diff --git a/docs/casual.info b/docs/casual.info
index 4b1f8889ea..4797f91e00 100644
Binary files a/docs/casual.info and b/docs/casual.info differ
diff --git a/docs/casual.org b/docs/casual.org
index 1ed82fcee6..7a2bccd7f2 100644
--- a/docs/casual.org
+++ b/docs/casual.org
@@ -5,7 +5,7 @@
#+EMAIL: [email protected]
#+OPTIONS: ':t toc:t author:t email:t H:4 f:t
#+LANGUAGE: en
-#+MACRO: version 2.10.2
+#+MACRO: version 2.11.0
#+MACRO: kbd (eval (org-texinfo-kbd-macro $1))
#+TEXINFO_FILENAME: casual.info
#+TEXINFO_CLASS: casual
@@ -147,6 +147,7 @@ Configuration of a particular Casual user interface is
performed per mode. Go to
- [[#calc-install][Calc]]
- [[#calendar-install][Calendar]]
- [[#compile-install][Compile (Grep)]]
+- [[#csv-install][CSV]]
- [[#dired-install][Dired]]
- [[#ediff-install][Ediff]]
- [[#editkit-install][EditKit]]
@@ -260,6 +261,7 @@ The following modes are supported by Casual:
#+INCLUDE: "./calc.org" :minlevel 2
#+INCLUDE: "./calendar.org" :minlevel 2
#+INCLUDE: "./compile.org" :minlevel 2
+#+INCLUDE: "./csv.org" :minlevel 2
#+INCLUDE: "./dired.org" :minlevel 2
#+INCLUDE: "./ediff.org" :minlevel 2
#+INCLUDE: "./editkit.org" :minlevel 2
diff --git a/docs/casual.texi b/docs/casual.texi
index 928a0c44ea..edfa7b0b90 100644
--- a/docs/casual.texi
+++ b/docs/casual.texi
@@ -20,7 +20,7 @@ Copyright © 2024-2025 Charles Y@. Choi
@finalout
@titlepage
@title Casual User Guide
-@subtitle for version 2.10.2
+@subtitle for version 2.11.0
@author Charles Y@. Choi (@email{kickingvegas@@gmail.com})
@page
@vskip 0pt plus 1filll
@@ -33,7 +33,7 @@ Copyright © 2024-2025 Charles Y@. Choi
@node Top
@top Casual User Guide
-Version: 2.10.2
+Version: 2.11.0
Casual is a project to re-imagine the primary user interface for Emacs using
keyboard-driven menus.
@@ -105,6 +105,7 @@ Casual Modes
* Calc::
* Calendar::
* Compile::
+* CSV::
* Dired::
* Ediff::
* EditKit::
@@ -163,6 +164,11 @@ Compile
* Compile Install::
* Compile Usage::
+CSV
+
+* CSV Install::
+* CSV Usage::
+
Dired
* Dired Requirements::
@@ -446,6 +452,8 @@ Configuration of a particular Casual user interface is
performed per mode. Go to
@item
@ref{Compile Install, , Compile (Grep)}
@item
+@ref{CSV Install, , CSV}
+@item
@ref{Dired Install, , Dired}
@item
@ref{Ediff Install, , Ediff}
@@ -608,6 +616,7 @@ The following modes are supported by Casual:
* Calc::
* Calendar::
* Compile::
+* CSV::
* Dired::
* Ediff::
* EditKit::
@@ -1418,6 +1427,101 @@ If the output window is from a Grep command,
@code{casual-compile-tmenu} will ad
By enabling “@kbd{u} Use Unicode Symbols” from the Settings menu, Casual
Compile will use Unicode symbols as appropriate in its menus.
+@node CSV
+@section CSV
+
+@cindex CSV
+@vindex casual-csv-tmenu
+
+Casual CSV is a user interface for @code{csv-mode}, a mode for working with
CSV files.
+
+@image{images/casual-csv-edit-screenshot,,,,png}
+
+@menu
+* CSV Install::
+* CSV Usage::
+@end menu
+
+@node CSV Install
+@subsection CSV Install
+
+@cindex CSV Install
+
+In your initialization file, bind the Transient @code{casual-csv-tmenu} to
your key binding of preference.
+
+@lisp
+(keymap-set csv-mode-map "M-m" #'casual-csv-tmenu)
+@end lisp
+
+While not required, the following configuration is recommended for working
with CSV files.
+
+@lisp
+;; disable line wrap
+(add-hook 'csv-mode-hook
+ (lambda ()
+ (visual-line-mode -1)
+ (toggle-truncate-lines 1)))
+
+;; auto detect separator
+(add-hook 'csv-mode-hook #'csv-guess-set-separator)
+;; turn on field alignment
+(add-hook 'csv-mode-hook #'csv-align-mode)
+@end lisp
+
+@node CSV Usage
+@subsection CSV Usage
+
+@cindex CSV Usage
+
+@image{images/casual-csv-edit-screenshot,,,,png}
+
+The following sections are offered in the menu:
+
+@table @asis
+@item Navigation, Line, Buffer
+Commands for moving the point, mostly with respect to a field.
+@item Page
+Move up or down a page.
+@item Buffer/File
+Commands associated the current buffer or file, such changing the buffer state
from viewable (read-only) to editable (writeable), display alignment, or
duplicating the file for subsequent editing.
+@item Field
+Commands to mark or copy a field.
+@item Sort
+Sorting commands. This section is displayed only if the buffer is editable.
+@item Fields
+Kill and yank commands dedicated for CSV mode. Note that these commands do
@emph{not} use the default @code{kill-ring} and are marked with a bullet (•).
This section is displayed only if the buffer is editable.
+@item Misc
+Miscellaneous commands. Note if a region is selected containing multiple
complete rows, the “@kbd{C} Copy as Table” command will reformat the selected
rows as an Org table and copy them in the @code{kill-ring} for subsequent
pasting.
+@end table
+
+@subheading CVS View/Edit, Duplicate
+
+If the buffer is in view (read-only) mode, then only relevant commands are
displayed.
+
+@image{images/casual-csv-view-screenshot,,,,png}
+
+If the buffer is editable, a common to desire to instead work on a copy of the
CSV file to avoid making unwanted changes. This can be done using the “@kbd{d}
Duplicate” command.
+
+@subheading CSV Align
+@vindex casual-csv-align-tmenu
+
+The display of the CSV buffer can be controlled with this menu.
+
+@image{images/casual-csv-align-screenshot,,,,png}
+
+
+@subheading CSV Settings
+@vindex casual-csv-settings-tmenu
+
+@image{images/casual-csv-settings-screenshot,,,,png}
+
+
+@subheading CSV Unicode Symbol Support
+
+By enabling “@kbd{u} Use Unicode Symbols” from the Settings menu, Casual CSV
will use Unicode symbols as appropriate in its menus.
+
+@image{images/casual-csv-view-unicode-screenshot,,,,png}
+
@node Dired
@section Dired
diff --git a/docs/csv.org b/docs/csv.org
new file mode 100644
index 0000000000..26dcc84688
--- /dev/null
+++ b/docs/csv.org
@@ -0,0 +1,80 @@
+* CSV
+#+CINDEX: CSV
+#+VINDEX: casual-csv-tmenu
+
+Casual CSV is a user interface for ~csv-mode~, a mode for working with CSV
files.
+
+[[file:images/casual-csv-edit-screenshot.png]]
+
+** CSV Install
+:PROPERTIES:
+:CUSTOM_ID: csv-install
+:END:
+
+#+CINDEX: CSV Install
+
+In your initialization file, bind the Transient ~casual-csv-tmenu~ to your key
binding of preference.
+
+#+begin_src elisp :lexical no
+ (keymap-set csv-mode-map "M-m" #'casual-csv-tmenu)
+#+end_src
+
+While not required, the following configuration is recommended for working
with CSV files.
+
+#+BEGIN_SRC elisp :lexical no
+ ;; disable line wrap
+ (add-hook 'csv-mode-hook
+ (lambda ()
+ (visual-line-mode -1)
+ (toggle-truncate-lines 1)))
+
+ ;; auto detect separator
+ (add-hook 'csv-mode-hook #'csv-guess-set-separator)
+ ;; turn on field alignment
+ (add-hook 'csv-mode-hook #'csv-align-mode)
+#+END_SRC
+
+** CSV Usage
+#+CINDEX: CSV Usage
+
+[[file:images/casual-csv-edit-screenshot.png]]
+
+The following sections are offered in the menu:
+
+- Navigation, Line, Buffer :: Commands for moving the point, mostly with
respect to a field.
+- Page :: Move up or down a page.
+- Buffer/File :: Commands associated the current buffer or file, such changing
the buffer state from viewable (read-only) to editable (writeable), display
alignment, or duplicating the file for subsequent editing.
+- Field :: Commands to mark or copy a field.
+- Sort :: Sorting commands. This section is displayed only if the buffer is
editable.
+- Fields :: Kill and yank commands dedicated for CSV mode. Note that these
commands do /not/ use the default ~kill-ring~ and are marked with a bullet (•).
This section is displayed only if the buffer is editable.
+- Misc :: Miscellaneous commands. Note if a region is selected containing
multiple complete rows, the “{{{kbd(C)}}} Copy as Table” command will reformat
the selected rows as an Org table and copy them in the ~kill-ring~ for
subsequent pasting.
+
+#+TEXINFO: @subheading CVS View/Edit, Duplicate
+
+If the buffer is in view (read-only) mode, then only relevant commands are
displayed.
+
+[[file:images/casual-csv-view-screenshot.png]]
+
+If the buffer is editable, a common to desire to instead work on a copy of the
CSV file to avoid making unwanted changes. This can be done using the
“{{{kbd(d)}}} Duplicate” command.
+
+#+TEXINFO: @subheading CSV Align
+#+VINDEX: casual-csv-align-tmenu
+
+The display of the CSV buffer can be controlled with this menu.
+
+[[file:images/casual-csv-align-screenshot.png]]
+
+
+#+TEXINFO: @subheading CSV Settings
+#+VINDEX: casual-csv-settings-tmenu
+
+[[file:images/casual-csv-settings-screenshot.png]]
+
+
+#+TEXINFO: @subheading CSV Unicode Symbol Support
+
+By enabling “{{{kbd(u)}}} Use Unicode Symbols” from the Settings menu, Casual
CSV will use Unicode symbols as appropriate in its menus.
+
+[[file:images/casual-csv-view-unicode-screenshot.png]]
+
+
diff --git a/docs/images/casual-csv-align-screenshot.png
b/docs/images/casual-csv-align-screenshot.png
new file mode 100644
index 0000000000..56f95f3443
Binary files /dev/null and b/docs/images/casual-csv-align-screenshot.png differ
diff --git a/docs/images/casual-csv-edit-screenshot.png
b/docs/images/casual-csv-edit-screenshot.png
new file mode 100644
index 0000000000..006e8cfc88
Binary files /dev/null and b/docs/images/casual-csv-edit-screenshot.png differ
diff --git a/docs/images/casual-csv-settings-screenshot.png
b/docs/images/casual-csv-settings-screenshot.png
new file mode 100644
index 0000000000..7b02ef0209
Binary files /dev/null and b/docs/images/casual-csv-settings-screenshot.png
differ
diff --git a/docs/images/casual-csv-view-screenshot.png
b/docs/images/casual-csv-view-screenshot.png
new file mode 100644
index 0000000000..64bcee2700
Binary files /dev/null and b/docs/images/casual-csv-view-screenshot.png differ
diff --git a/docs/images/casual-csv-view-unicode-screenshot.png
b/docs/images/casual-csv-view-unicode-screenshot.png
new file mode 100644
index 0000000000..b406054c7d
Binary files /dev/null and b/docs/images/casual-csv-view-unicode-screenshot.png
differ
diff --git a/lisp/Makefile b/lisp/Makefile
index b65da829cc..ab5c1fe54d 100644
--- a/lisp/Makefile
+++ b/lisp/Makefile
@@ -25,6 +25,7 @@ bookmarks-tests \
calc-tests \
calendar-tests \
compile-tests \
+csv-tests \
dired-tests \
ediff-tests \
editkit-tests \
@@ -67,6 +68,10 @@ calendar-tests:
compile-tests:
$(MAKE) -C $(SRC_DIR) -f Makefile-compile.make tests
+.PHONY: csv-tests
+csv-tests:
+ $(MAKE) -C $(SRC_DIR) -f Makefile-csv.make tests
+
.PHONY: dired-tests
dired-tests:
$(MAKE) -C $(SRC_DIR) -f Makefile-dired.make tests
diff --git a/lisp/Makefile-csv.make b/lisp/Makefile-csv.make
new file mode 100644
index 0000000000..8709566e06
--- /dev/null
+++ b/lisp/Makefile-csv.make
@@ -0,0 +1,32 @@
+##
+# 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-csv
+ELISP_INCLUDES=casual-csv-utils.el \
+casual-csv-settings.el
+ELISP_PACKAGES=
+ELISP_TEST_INCLUDES=casual-csv-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)/csv-mode-current \
+-L $(CASUAL_LIB_LISP_DIR)
+
+include Makefile--rules.make
diff --git a/lisp/casual-csv-settings.el b/lisp/casual-csv-settings.el
new file mode 100644
index 0000000000..35564fea11
--- /dev/null
+++ b/lisp/casual-csv-settings.el
@@ -0,0 +1,146 @@
+;;; casual-csv-settings.el --- Casual CSV 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 'csv-mode)
+(require 'casual-lib)
+
+(transient-define-prefix casual-csv-settings-tmenu ()
+ "Casual csv settings menu."
+ ["Casual csv: Settings"
+ ["Customize"
+ ("A" "Align Style" casual-csv--customize-align-style
+ :description (lambda () (format "Align Style (%s)"
+ (capitalize (symbol-name csv-align-style)))))
+ ("s" "Separators" casual-csv--customize-separators)
+ ("i" "Invisibility Default" casual-csv--customize-invisibility-default
+ :description (lambda () (casual-lib-checkbox-label
csv-invisibility-default
+ "Invisibility Default")))]
+
+ [""
+ ("G" "CSV Group" casual-csv--customize-group)
+ ("h" "Header Lines" casual-csv--customize-header-lines
+ :description (lambda () (format "Header Lines (%d)" csv-header-lines)))
+ ("c" "Comment Start Default" casual-csv--customize-comment-start-default
+ :description (lambda () (format
+ "Comment Start Default (%s)"
+ csv-comment-start-default)))
+ ("f" "Field Quotes" casual-csv--customize-field-quotes
+ :description (lambda () (format
+ "Field Quotes (%s)"
+ (string-join csv-field-quotes))))]
+
+ ["Width"
+ ("w" "Min" casual-csv--customize-align-min-width
+ :description (lambda () (format "Min (%d)" csv-align-min-width)))
+ ("W" "Max" casual-csv--customize-align-max-width
+ :description (lambda () (format "Max (%d)" csv-align-max-width)))]]
+
+ [:class transient-row
+ (casual-lib-customize-unicode)
+ (casual-lib-customize-hide-navigation)]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("a" "About" casual-csv-about)
+ (casual-lib-quit-all)])
+
+
+;; -------------------------------------------------------------------
+;; Functions
+
+(defun casual-csv--customize-group ()
+ "Customize csv group."
+ (interactive)
+ (customize-group "CSV"))
+
+(defun casual-csv--customize-align-style ()
+ "Customize `csv-align-style'."
+ (interactive)
+ (customize-variable 'csv-align-style))
+
+(defun casual-csv--customize-separators ()
+ "Customize `csv-separators'."
+ (interactive)
+ (customize-variable 'csv-separators))
+
+(defun casual-csv--customize-field-quotes ()
+ "Customize `csv-field-quotes'."
+ (interactive)
+ (customize-variable 'csv-field-quotes))
+
+(defun casual-csv--customize-align-max-width ()
+ "Customize `csv-align-max-width'."
+ (interactive)
+ (customize-variable 'csv-align-max-width))
+
+(defun casual-csv--customize-align-min-width ()
+ "Customize `csv-align-min-width'."
+ (interactive)
+ (customize-variable 'csv-align-min-width))
+
+(defun casual-csv--customize-invisibility-default ()
+ "Customize `csv-invisibility-default'."
+ (interactive)
+ (customize-variable 'csv-invisibility-default))
+
+(defun casual-csv--customize-comment-start-default ()
+ "Customize `csv-comment-start-default'."
+ (interactive)
+ (customize-variable 'csv-comment-start-default))
+
+(defun casual-csv--customize-header-lines ()
+ "Customize `csv-comment-header-lines'."
+ (interactive)
+ (customize-variable 'csv-header-lines))
+
+(defun casual-csv-about-csv ()
+ "Casual csv is a Transient menu for csv pages.
+
+Learn more about using Casual csv 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 csv, consider making a modest financial
+contribution to help support its development and maintenance.
+URL `https://www.buymeacoffee.com/kickingvegas'
+
+Casual csv was conceived and crafted by Charles Choi in San Francisco,
+California.
+
+Thank you for using Casual csv.
+
+Always choose love."
+ (ignore))
+
+(defun casual-csv-about ()
+ "About information for Casual csv."
+ (interactive)
+ (describe-function #'casual-csv-about-csv))
+
+(provide 'casual-csv-settings)
+;;; casual-csv-settings.el ends here
diff --git a/lisp/casual-csv-utils.el b/lisp/casual-csv-utils.el
new file mode 100644
index 0000000000..4569351d3e
--- /dev/null
+++ b/lisp/casual-csv-utils.el
@@ -0,0 +1,105 @@
+;;; casual-csv-utils.el --- Casual CSV 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 'csv-mode)
+(require 'casual-lib)
+(require 'org-table)
+
+(defconst casual-csv-unicode-db
+ '((:up . '("↑" "Up"))
+ (:down . '("↓" "Down"))
+ (:right . '("→" "Right"))
+ (:left . '("←" "Left"))
+ (:bol . '("⇤" "Begin"))
+ (:eol . '("⇥" "End"))
+ (:beginning-of-buffer . '("⇱" "Begin"))
+ (:end-of-buffer . '("⇲" "End")))
+ "Unicode symbol DB to use for CSV Transient menus.")
+
+(defun casual-csv-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-csv-unicode-db))
+
+(defun casual-csv-kill-region-as-org-table (start end)
+ "Copy CSV region at START, END as Org table in the `kill-ring'."
+ (interactive "r")
+ (let ((buf (buffer-substring start end)))
+ (with-temp-buffer
+ (insert buf)
+ (org-table-convert-region (point-min) (point-max))
+ (kill-region (point-min) (point-max)))))
+
+(defun casual-csv-align-auto ()
+ "Auto align CSV fields."
+ (interactive)
+ (setopt csv-align-style 'auto)
+ (call-interactively #'csv-align-fields))
+
+(defun casual-csv-align-left ()
+ "Left align CSV fields."
+ (interactive)
+ (setopt csv-align-style 'left)
+ (call-interactively #'csv-align-fields))
+
+(defun casual-csv-align-centre ()
+ "Centre align CSV fields."
+ (interactive)
+ (setopt csv-align-style 'centre)
+ (call-interactively #'csv-align-fields))
+
+(defun casual-csv-align-right ()
+ "Right align CSV fields."
+ (interactive)
+ (setopt csv-align-style 'right)
+ (call-interactively #'csv-align-fields))
+
+
+;; -------------------------------------------------------------------
+;; Transients
+(transient-define-prefix casual-csv-align-tmenu ()
+ ["Align"
+ :description (lambda () (format
+ "Casual CSV Align: %s %s"
+ (buffer-name)
+ (capitalize (symbol-name csv-align-style))))
+ :class transient-row
+ ("a" "Auto" casual-csv-align-auto :transient t)
+ ("l" "Left" casual-csv-align-left :transient t)
+ ("c" "Centre" casual-csv-align-centre :transient t)
+ ("r" "Right" casual-csv-align-right :transient t)
+ ("t" "Toggle" csv-align-mode :transient t)]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("RET" "Dismiss" casual-lib-quit-all)
+ (casual-lib-quit-all)])
+
+(provide 'casual-csv-utils)
+;;; casual-csv-utils.el ends here
diff --git a/lisp/casual-csv.el b/lisp/casual-csv.el
new file mode 100644
index 0000000000..2b259e69bb
--- /dev/null
+++ b/lisp/casual-csv.el
@@ -0,0 +1,135 @@
+;;; casual-csv.el --- Transient UI for CSV mode -*- 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 `csv-mode'.
+
+;; INSTALLATION
+
+;; In your initialization file, bind the Transient `casual-csv-tmenu' to your
+;; key binding of preference.
+
+;; (require 'casual-csv) ; optional if using autoloaded menu
+;; (keymap-set csv-mode-map "M-m" #'casual-csv-tmenu)
+
+;; While not required, the following configuration is recommended for working
+;; with CSV files.
+
+;; (add-hook 'csv-mode-hook
+;; (lambda ()
+;; (visual-line-mode -1)
+;; (toggle-truncate-lines 1)))
+
+;; (add-hook 'csv-mode-hook #'csv-guess-set-separator)
+;; (add-hook 'csv-mode-hook #'csv-align-mode)
+
+;;; Code:
+(require 'casual-editkit-utils)
+(require 'casual-csv-settings)
+(require 'casual-csv-utils)
+
+;;;###autoload (autoload 'casual-csv-tmenu "casual-csv" nil t)
+(transient-define-prefix casual-csv-tmenu ()
+ :refresh-suffixes t
+ ["Casual CSV"
+ :description (lambda () (format
+ "Casual CSV: %s [%d,%d] %s"
+ (buffer-name)
+ (line-number-at-pos)
+ (csv--field-index)
+ (capitalize (symbol-name csv-align-style))))
+ :pad-keys t
+
+ ["Navigation"
+ ("S-TAB" "←" csv-backtab-command
+ :description (lambda () (format "%s" (casual-csv-unicode-get :left)))
+ :transient t)
+ ("TAB" "→" csv-tab-command
+ :description (lambda () (format "%s" (casual-csv-unicode-get :right)))
+ :transient t)]
+ [""
+ ("p" "↑" previous-line
+ :description (lambda () (format "%s" (casual-csv-unicode-get :up)))
+ :transient t)
+ ("n" "↓" next-line
+ :description (lambda () (format "%s" (casual-csv-unicode-get :down)))
+ :transient t)]
+ ["Line"
+ ("C-a" "⇤" move-beginning-of-line
+ :description (lambda () (format "%s" (casual-csv-unicode-get :bol)))
+ :transient t)
+ ("C-e" "⇥" move-end-of-line
+ :description (lambda () (format "%s" (casual-csv-unicode-get :eol)))
+ :transient t)]
+ ["Buffer"
+ ("<" "⇱" beginning-of-buffer
+ :description (lambda () (format "%s" (casual-csv-unicode-get
:beginning-of-buffer)))
+ :transient t)
+ (">" "⇲" end-of-buffer
+ :description (lambda () (format "%s" (casual-csv-unicode-get
:end-of-buffer)))
+ :transient t)]
+
+ ["Page"
+ ("M-v" "Up" scroll-down-command :transient t)
+ ("C-v" "Down" scroll-up-command :transient t)]
+
+ ["Buffer/File"
+ ("a" "Align›" casual-csv-align-tmenu)
+ ("v" "View" view-mode
+ :if (lambda () (not buffer-read-only))
+ :transient t)
+ ("e" "Edit" View-exit
+ :if (lambda () buffer-read-only)
+ :transient t)
+ ("d" "Duplicate" casual-lib-duplicate-file)]]
+
+ [["Field"
+ :pad-keys t
+ ("m" "Mark" mark-sexp)
+ ("c" "Copy" casual-editkit-copy-sexp)]
+
+ ["Sort"
+ :if (lambda () (not buffer-read-only))
+ ("s" "Fields" csv-sort-fields)
+ ("N" "Numeric" csv-sort-numeric-fields)
+ ("r" "Reverse" csv-reverse-region)]
+
+ ["Fields"
+ :if (lambda () (not buffer-read-only))
+ ("k" "Kill∙" csv-kill-fields)
+ ("y" "Yank∙" csv-yank-fields)]
+
+ ["Misc"
+ ("t" "Transpose" csv-transpose
+ :if (lambda () (not buffer-read-only)))
+ ("S" "Separator…" csv-set-separator)
+ ("o" "Occur…" occur)
+ ("C" "Copy as Table" casual-csv-kill-region-as-org-table
+ :inapt-if-not use-region-p)]]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("," "Settings" casual-csv-settings-tmenu)
+ ("q" "Quit" quit-window)
+ (casual-lib-quit-all)])
+
+(provide 'casual-csv)
+;;; casual-csv.el ends here
diff --git a/lisp/casual-lib.el b/lisp/casual-lib.el
index 7efb4d8017..fe56ea5abb 100644
--- a/lisp/casual-lib.el
+++ b/lisp/casual-lib.el
@@ -144,6 +144,32 @@ V is either nil or non-nil."
(forward-paragraph)
(forward-line))
+(defun casual-lib-duplicate-file (&optional arg)
+ "Duplicate the current file with prefix option ARG.
+
+This command will duplicate and open the current file in the buffer to a
+filename of the form “<filename> copy.<extension>”. If the current
+buffer is modified, a prompt will be raised to save it before making the
+duplicate copy.
+
+By default this command will immediate open the duplicate file into a
+new buffer. This can be avoided if a prefix ARG is provided."
+ (interactive "P")
+ (if (and (buffer-modified-p) (y-or-n-p "Save buffer? "))
+ (save-buffer))
+
+ (let ((filename (buffer-file-name)))
+ (unless filename
+ (error "This command only works on a file."))
+
+ (let* ((extension (file-name-extension filename t))
+ (target (format "%s copy%s"
+ (file-name-sans-extension filename)
+ extension)))
+ (copy-file filename target)
+ (if (not arg)
+ (find-file target)))))
+
;; Transients
(transient-define-suffix casual-lib-quit-all ()
"Casual suffix to call `transient-quit-all'."
diff --git a/templates/lisp/casual-MODULE.el b/templates/lisp/casual-MODULE.el
index d232b3300f..0bc40c3344 100644
--- a/templates/lisp/casual-MODULE.el
+++ b/templates/lisp/casual-MODULE.el
@@ -43,7 +43,7 @@
(casual-lib-quit-one)
("," "Settings" casual-$MODULE-settings-tmenu)
;; ("I" "ⓘ" casual-$MODULE-info)
- ;; ("q" "Quit" quit-window)
+ ("q" "Quit" quit-window)
(casual-lib-quit-all)])
(provide 'casual-$MODULE)
diff --git a/templates/tests/test-casual-MODULE-settings.el
b/templates/tests/test-casual-MODULE-settings.el
index d50632f5b4..a5b55b8ba4 100644
--- a/templates/tests/test-casual-MODULE-settings.el
+++ b/templates/tests/test-casual-MODULE-settings.el
@@ -34,10 +34,10 @@
(casualt-mock #'casual-$MODULE-about))
(let ((test-vectors
- '((:binding "G" :com$MODULEd casual-$MODULE--customize-group)
- (:binding "u" :com$MODULEd
casual-lib-customize-casual-lib-use-unicode)
- (:binding "n" :com$MODULEd
casual-lib-customize-casual-lib-hide-navigation)
- (:binding "a" :com$MODULEd casual-$MODULE-about))))
+ '((:binding "G" :command casual-$MODULE--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-$MODULE-about))))
(casualt-suffix-testcase-runner test-vectors
#'casual-$MODULE-settings-tmenu
diff --git a/tests/Makefile b/tests/Makefile
index 82b98bed4e..b803100565 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -24,6 +24,7 @@ bookmarks-tests \
calc-tests \
calendar-tests \
compile-tests \
+csv-tests \
dired-tests \
ediff-tests \
editkit-tests \
@@ -47,6 +48,7 @@ bookmarks-tests \
calc-tests \
calendar-tests \
compile-tests \
+csv-tests \
dired-tests \
ediff-tests \
editkit-tests \
@@ -82,6 +84,9 @@ calendar-tests:
compile-tests:
$(MAKE) -C $(SRC_DIR) $@
+csv-tests:
+ $(MAKE) -C $(SRC_DIR) $@
+
dired-tests:
$(MAKE) -C $(SRC_DIR) $@
diff --git a/tests/casual-csv-test-utils.el b/tests/casual-csv-test-utils.el
new file mode 100644
index 0000000000..c738b7281f
--- /dev/null
+++ b/tests/casual-csv-test-utils.el
@@ -0,0 +1,39 @@
+;;; casual-csv-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-csv-setup ()
+ "Casual csv setup."
+ )
+
+(defun casualt-csv-breakdown ()
+ "Casual csv breakdown."
+ )
+
+(provide 'casual-csv-test-utils)
+;;; casual-csv-test-utils.el ends here
diff --git a/tests/test-casual-csv-settings.el
b/tests/test-casual-csv-settings.el
new file mode 100644
index 0000000000..8adfd89fc3
--- /dev/null
+++ b/tests/test-casual-csv-settings.el
@@ -0,0 +1,68 @@
+;;; test-casual-csv-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-csv-test-utils)
+(require 'casual-csv-settings)
+
+(ert-deftest test-casual-csv-settings-tmenu ()
+ (let ()
+ (cl-letf ((casualt-mock #'casual-csv--customize-align-style)
+ (casualt-mock #'casual-csv--customize-separators)
+ (casualt-mock #'casual-csv--customize-invisibility-default)
+ (casualt-mock #'casual-csv--customize-group)
+ (casualt-mock #'casual-csv--customize-header-lines)
+ (casualt-mock #'casual-csv--customize-comment-start-default)
+ (casualt-mock #'casual-csv--customize-field-quotes)
+ (casualt-mock #'casual-csv--customize-align-min-width)
+ (casualt-mock #'casual-csv--customize-align-max-width)
+ (casualt-mock #'casual-csv-about))
+
+ (let ((test-vectors
+ '(
+ (:binding "A" :command casual-csv--customize-align-style)
+ (:binding "s" :command casual-csv--customize-separators)
+ (:binding "i" :command
casual-csv--customize-invisibility-default)
+ (:binding "G" :command casual-csv--customize-group)
+ (:binding "h" :command casual-csv--customize-header-lines)
+ (:binding "c" :command
casual-csv--customize-comment-start-default)
+ (:binding "f" :command casual-csv--customize-field-quotes)
+ (:binding "w" :command casual-csv--customize-align-min-width)
+ (:binding "W" :command casual-csv--customize-align-max-width)
+
+ (:binding "u" :command
casual-lib-customize-casual-lib-use-unicode)
+ (:binding "n" :command
casual-lib-customize-casual-lib-hide-navigation)
+ (:binding "a" :command casual-csv-about))))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-csv-settings-tmenu
+ '(lambda () (random 5000)))))))
+
+(ert-deftest test-casual-csv-about ()
+ (should (stringp (casual-csv-about))))
+
+(provide 'test-casual-csv-settings)
+;;; test-casual-csv-setttings.el ends here
diff --git a/tests/test-casual-csv-utils.el b/tests/test-casual-csv-utils.el
new file mode 100644
index 0000000000..4a87ef1c49
--- /dev/null
+++ b/tests/test-casual-csv-utils.el
@@ -0,0 +1,74 @@
+;;; test-casual-csv-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-csv-test-utils)
+(require 'casual-csv-utils)
+
+(ert-deftest test-casual-csv-unicode-get ()
+ (let ((casual-lib-use-unicode nil))
+ (should (string-equal (casual-csv-unicode-get :up) "Up"))
+ (should (string-equal (casual-csv-unicode-get :down) "Down"))
+ (should (string-equal (casual-csv-unicode-get :right) "Right"))
+ (should (string-equal (casual-csv-unicode-get :left) "Left"))
+ (should (string-equal (casual-csv-unicode-get :bol) "Begin"))
+ (should (string-equal (casual-csv-unicode-get :eol) "End"))
+ (should (string-equal (casual-csv-unicode-get :beginning-of-buffer)
"Begin"))
+ (should (string-equal (casual-csv-unicode-get :end-of-buffer) "End")))
+
+ (let ((casual-lib-use-unicode t))
+ (should (string-equal (casual-csv-unicode-get :up) "↑"))
+ (should (string-equal (casual-csv-unicode-get :down) "↓"))
+ (should (string-equal (casual-csv-unicode-get :right) "→"))
+ (should (string-equal (casual-csv-unicode-get :left) "←"))
+ (should (string-equal (casual-csv-unicode-get :bol) "⇤"))
+ (should (string-equal (casual-csv-unicode-get :eol) "⇥"))
+ (should (string-equal (casual-csv-unicode-get :beginning-of-buffer) "⇱"))
+ (should (string-equal (casual-csv-unicode-get :end-of-buffer) "⇲"))))
+
+
+(ert-deftest test-casual-csv-align-tmenu ()
+ (let ((tmpfile "casual-csv-align-tmenu.txt"))
+ (casualt-csv-setup)
+ (cl-letf ((casualt-mock #'csv-align-mode)
+ (casualt-mock #'casual-csv-align-auto)
+ (casualt-mock #'casual-csv-align-left)
+ (casualt-mock #'casual-csv-align-right)
+ (casualt-mock #'casual-csv-align-centre))
+
+ (let ((test-vectors
+ '((:binding "t" :command csv-align-mode)
+ (:binding "a" :command casual-csv-align-auto)
+ (:binding "l" :command casual-csv-align-left)
+ (:binding "r" :command casual-csv-align-right)
+ (:binding "c" :command casual-csv-align-centre))))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-csv-align-tmenu
+ '(lambda () (random 5000)))))
+ (casualt-csv-breakdown)))
+
+(provide 'test-casual-csv-utils)
+;;; test-casual-csv-utils.el ends here
diff --git a/tests/test-casual-csv.el b/tests/test-casual-csv.el
new file mode 100644
index 0000000000..5d826a33e5
--- /dev/null
+++ b/tests/test-casual-csv.el
@@ -0,0 +1,91 @@
+;;; test-casual-csv.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-csv-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-csv)
+
+(ert-deftest test-casual-csv-tmenu ()
+ (let ()
+ (casualt-csv-setup)
+
+ (cl-letf (
+ (casualt-mock #'csv-backtab-command)
+ (casualt-mock #'csv-tab-command)
+ (casualt-mock #'previous-line)
+ (casualt-mock #'next-line)
+ (casualt-mock #'move-beginning-of-line)
+ (casualt-mock #'move-end-of-line)
+ (casualt-mock #'beginning-of-buffer)
+ (casualt-mock #'end-of-buffer)
+ (casualt-mock #'scroll-down-command)
+ (casualt-mock #'scroll-up-command)
+ (casualt-mock #'casual-csv-align-tmenu)
+ (casualt-mock #'view-mode)
+ (casualt-mock #'View-exit)
+ (casualt-mock #'casual-lib-duplicate-file)
+ (casualt-mock #'mark-sexp)
+ (casualt-mock #'casual-editkit-copy-sexp)
+
+ (casualt-mock #'occur)
+ (casualt-mock #'casual-csv-kill-region-as-org-table)
+ (casualt-mock #'casual-csv-settings-tmenu)
+
+ (casualt-mock #'quit-window))
+
+ (let ((test-vectors
+ '(
+ (:binding "S-TAB" :command csv-backtab-command)
+ (:binding "TAB" :command csv-tab-command)
+ (:binding "n" :command next-line)
+ (:binding "p" :command previous-line)
+ (:binding "C-e" :command move-end-of-line)
+ (:binding "C-a" :command move-beginning-of-line)
+ (:binding ">" :command end-of-buffer)
+ (:binding "<" :command beginning-of-buffer)
+ (:binding "C-v" :command scroll-up-command)
+ (:binding "M-v" :command scroll-down-command)
+ (:binding "a" :command casual-csv-align-tmenu)
+ ;; (:binding "v" :command view-mode)
+ ;; (:binding "e" :command View-exit)
+ ;; (:binding "s" :command csv-sort-fields)
+
+ (:binding "m" :command mark-sexp)
+ (:binding "c" :command casual-editkit-copy-sexp)
+
+ (:binding "o" :command occur)
+ (:binding "," :command casual-csv-settings-tmenu)
+ (:binding "q" :command quit-window)
+ )))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-csv-tmenu
+ '(lambda () (random 5000)))))
+ (casualt-csv-breakdown)))
+
+(provide 'test-casual-csv)
+;;; test-casual-csv.el ends here
diff --git a/tests/test-casual-timezone-utils.el
b/tests/test-casual-timezone-utils.el
index 5420381fb3..82f53039e0 100644
--- a/tests/test-casual-timezone-utils.el
+++ b/tests/test-casual-timezone-utils.el
@@ -102,7 +102,7 @@
(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")
+ (control "2025-05-23 10:00:00 CEST")
(result (casual-timezone-map-local-to-timezone ts remote-tz)))
(should (string-equal control result))))
@@ -133,7 +133,7 @@
(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")
+ (control "Europe/Berlin 2025-05-23 22:00:00 CEST")
(result (casual-timezone-local-time-to-remote read-date remote-tz)))
(should (string-equal control result))))
@@ -141,7 +141,7 @@
(ert-deftest test-casual-timezone-local-time-to-remote-victoria ()
(let* ((read-date "2025-05-23 12:00")
(remote-tz "Australia/Victoria")
- (control "Australia/Victoria 2025-05-24 05:00:00 AEST")
+ (control "Australia/Victoria 2025-05-24 06:00:00 AEST")
(result (casual-timezone-local-time-to-remote read-date remote-tz)))
(should (string-equal control result))))