unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#70208: [PATCH] Add command `list-keyboard-macros`
@ 2024-04-05  3:34 Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-04-05  6:16 ` Eli Zaretskii
  0 siblings, 1 reply; 6+ messages in thread
From: Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-04-05  3:34 UTC (permalink / raw)
  To: 70208

[-- Attachment #1: Type: text/plain, Size: 475 bytes --]

Hello,

The attached patch adds the command `list-keyboard-macros`, which works 
like `list-buffers` using `tabulated-list-mode`.  It allows for 
re-arranging the macros in the ring; editing their counters, counter 
formats, and macro keys; deleting macros in the ring; and duplicating 
macros for further editing.

Please let me know what you think. I followed the naming scheme used by 
Dired for a few of the commands, but don't know if that's best.

Thank you.

[-- Attachment #2: 0001-Add-command-list-keyboard-macros-that-works-like-lis.patch --]
[-- Type: text/x-patch, Size: 24055 bytes --]

From 3dc72b6a41c0ea61602db43823d5cefaed5b54e9 Mon Sep 17 00:00:00 2001
From: Earl Hyatt <okamsn@protonmail.com>
Date: Sun, 24 Mar 2024 11:49:21 -0400
Subject: [PATCH] Add command 'list-keyboard-macros' that works like
 'list-buffers'.

The command 'list-keyboard-macros' allows editing and re-arranging
macros using 'tabulated-list-mode'.  Existing keyboard macros can be
duplicated or deleted.  Macro counters and counter formats can take new
values read from the minibuffer.  Macro keys can be edited using
'edit-kbd-macro'.

* doc/emacs/kmacro.texi (Keyboard Macros): Mention the new command
in the section introduction, since it relates to multiple subsections.
* etc/NEWS (Kmacro Menu Mode): Mention the new mode and command.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-marked)
(kmacro-menu-flagged): Add faces for marks and flags.
* lisp/kmacro.el (kmacro-menu-mode-map, kmacro-menu-mode): Add mode
and map.
* lisp/kmacro.el (list-keyboard-macros): Add command.
* lisp/kmacro.el (kmacro-menu--deletion-flags, kmacro-menu--marks)
(kmacro-menu--id-kmacro, kmacro-menu--id-position, kmacro-menu--kmacros)
(kmacro-menu--refresh, kmacro-menu--map-ids, kmacro-menu--update)
(kmacro-menu--update-at, kmacro-menu--query-revert, kmacro-menu--assert-row)
(kmacro-menu--propertize-keys, kmacro-menu--do-region)
(kmacro-menu--marks-exist-p): Add utility functions of mode
and commands.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-flag-for-deletion)
(kmacro-menu-unmark, kmacro-menu-unmark-backward)
(kmacro-menu-unmark-all): Add commands for marks and flags.
* lisp/kmacro.el (kmacro-menu-do-flagged-delete, kmacro-menu-do-copy)
(kmacro-menu-do-delete): Add commands that modify the ring.
* lisp/kmacro.el (kmacro-menu-edit-position, kmacro-menu-transpose)
(kmacro-menu-edit-format, kmacro-menu-edit-counter)
(kmacro-menu-edit-keys, kmacro-menu-edit-column): Add commands that
modify a keyboard macro.
---
 doc/emacs/kmacro.texi |   6 +
 etc/NEWS              |   8 +
 lisp/kmacro.el        | 508 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 522 insertions(+)

diff --git a/doc/emacs/kmacro.texi b/doc/emacs/kmacro.texi
index e30def34475..d0106fce5d1 100644
--- a/doc/emacs/kmacro.texi
+++ b/doc/emacs/kmacro.texi
@@ -24,6 +24,12 @@ Keyboard Macros
 keyboard macro is defined and also has been, in effect, executed once.
 You can then do the whole thing over again by invoking the macro.
 
+  The list of defined keyboard macros can be seen via @kbd{M-x
+list-keyboard-macros @key{RET}}.  This command can be used to re-order
+the list of defined macros (the @dfn{keyboard macro ring}) and to edit
+the properties of those keyboard macros, which are described in the
+following subsections.
+
   Keyboard macros differ from ordinary Emacs commands in that they are
 written in the Emacs command language rather than in Lisp.  This makes it
 easier for the novice to write them, and makes them more convenient as
diff --git a/etc/NEWS b/etc/NEWS
index eda84d588a8..e56122cd00d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1388,6 +1388,14 @@ When this is non-nil, the lines of key sequences are displayed with
 the most recent line first.  This is can be useful when working with
 macros with many lines, such as from 'kmacro-edit-lossage'.
 
+** Kmacro Menu Mode
+
+*** New mode and new command 'list-keyboard-macros'.
+This is the macro version of commands like 'list-buffers' and
+'list-processes'.  It allows rearranging the macros in the ring,
+duplicating them, deleting them, and editing their counters, formats,
+and keys.
+
 ** Miscellaneous
 
 ---
diff --git a/lisp/kmacro.el b/lisp/kmacro.el
index 897ebf14330..45594160e6d 100644
--- a/lisp/kmacro.el
+++ b/lisp/kmacro.el
@@ -1388,6 +1388,514 @@ kmacro-redisplay
     (let ((executing-kbd-macro nil))
       (redisplay))))
 
+;;; Mode and commands for working with the ring in a table
+
+(defvar tabulated-list-format)
+(defvar tabulated-list-entries)
+(defvar tabulated-list-sort-key)
+(declare-function tabulated-list-init-header  "tabulated-list" ())
+(declare-function tabulated-list-print "tabulated-list"
+                  (&optional remember-pos update))
+
+(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
+  "Face used for the Keyboard Macro Menu marks."
+  :group 'kmacro)
+
+(defface kmacro-menu-flagged '((t (:inherit error)))
+  "Face used for keyboard macros flagged for deletion."
+  :group 'kmacro)
+
+(defface kmacro-menu-marked '((t (:inherit warning)))
+  "Face used for keyboard macros marked for duplication."
+  :group 'kmacro)
+
+(defvar-keymap kmacro-menu-mode-map
+  :doc "Keymap for `kmacro-menu-mode'."
+  :parent tabulated-list-mode-map
+  "#" #'kmacro-menu-edit-position
+  "c" #'kmacro-menu-edit-counter
+  "e" #'kmacro-menu-edit-keys
+  "f" #'kmacro-menu-edit-format
+  "RET" #'kmacro-menu-edit-column
+
+  "C" #'kmacro-menu-do-copy
+  "D" #'kmacro-menu-do-delete
+  "m" #'kmacro-menu-mark
+
+  "d" #'kmacro-menu-flag-for-deletion
+  "x" #'kmacro-menu-do-flagged-delete
+
+  "u" #'kmacro-menu-unmark
+  "U" #'kmacro-menu-unmark-all
+  "DEL"#'kmacro-menu-unmark-backward
+
+  "<remap> <transpose-lines>" #'kmacro-menu-transpose)
+
+(define-derived-mode kmacro-menu-mode tabulated-list-mode
+  "Keyboard Macro Menu"
+  "Major mode for listing keyboard macros."
+  (make-local-variable 'kmacro-menu--marks)
+  (make-local-variable 'kmacro-menu--deletion-flags)
+  (setq-local tabulated-list-format
+              [("Position" 8 t)
+               ("Counter"  8 nil :right-align t :pad-right 2)
+               ("Format"  8 nil)
+               ("Formatted" 10 nil)
+               ("Keys" 1 nil)])
+  (setq-local tabulated-list-padding 2)
+  (add-hook 'tabulated-list-revert-hook #'kmacro-menu--refresh nil t)
+  (tabulated-list-init-header)
+  (unless (kmacro-ring-empty-p)
+    (kmacro-menu--refresh)
+    (tabulated-list-print)))
+
+(defun list-keyboard-macros ()
+  "List the keyboard macros."
+  (interactive)
+  (let ((buf (get-buffer-create "*List Keyboard Macros*")))
+    (with-current-buffer buf
+      (kmacro-menu-mode))
+    (pop-to-buffer buf)))
+
+;;;; Utility functions and mode data
+
+(defvar kmacro-menu--deletion-flags nil
+  "Alist of entries flagged for deletion.")
+
+(defvar kmacro-menu--marks nil
+  "Alist of entries marked for copying and duplication.")
+
+(defun kmacro-menu--id-kmacro (entry-id)
+  "Return keyboard macro that is part of the ENTRY-ID."
+  (car entry-id))
+
+(defun kmacro-menu--id-position (entry-id)
+  "Return ordinal position that is part of the ENTRY-ID."
+  (cdr entry-id))
+
+(defun kmacro-menu--kmacros ()
+  "Return a list of the existing keyboard macros."
+  (when last-kbd-macro
+    (cons (kmacro-ring-head)
+          kmacro-ring)))
+
+(defun kmacro-menu--refresh ()
+  "Reset the list of keyboard macros."
+  (setq-local tabulated-list-entries
+              (seq-map-indexed (lambda (km idx)
+                                 (let ((cnt (kmacro--counter km))
+                                       (fmt (kmacro--format km)))
+                                   `((,km . ,idx)
+                                     [,(format "%d" idx)
+                                      ,(format "%d" cnt)
+                                      ,fmt
+                                      ,(format fmt cnt)
+                                      ,(format-kbd-macro (kmacro--keys km))])))
+                               (kmacro-menu--kmacros))
+              kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (tabulated-list-clear-all-tags))
+
+(defun kmacro-menu--map-ids (function)
+  "Map a FUNCTION to the current table entry IDs in order.
+
+If FILTER is non-nil, then IDs for which FILTER returns nil are
+excluded.
+
+Returns a list of the output of FUNCTION."
+  (mapcar function
+          (mapcar #'car
+                  (seq-sort-by #'cdar #'< tabulated-list-entries))))
+
+(defun kmacro-menu--update (kmacros)
+  "Update the variables for the current and previous keyboard macros.
+
+KMACROS is a list of `kmacro' objects."
+  (if (null kmacros)
+      (setq last-kbd-macro nil
+            kmacro-counter-format kmacro-default-counter-format
+            kmacro-counter 0
+            kmacro-ring nil)
+    (kmacro-split-ring-element (car kmacros))
+    (setq kmacro-ring (cdr kmacros))))
+
+(defun kmacro-menu--update-at (kmacro n)
+  "Update to KMACRO at position N."
+  (kmacro-menu--update
+   (kmacro-menu--map-ids (lambda (id)
+                           (if (= n (kmacro-menu--id-position id))
+                               kmacro
+                             (kmacro-menu--id-kmacro id))))))
+
+(defun kmacro-menu--query-revert ()
+  "When the table differs from the existing macros, ask whether to revert table."
+  (interactive)
+  (when (and (not (equal (kmacro-menu--kmacros)
+                         (kmacro-menu--map-ids #'kmacro-menu--id-kmacro)))
+             (yes-or-no-p "Table does not match existing keyboard macros.  Stop and revert table?"))
+    (tabulated-list-revert)
+    (signal 'quit nil)))
+
+(defun kmacro-menu--assert-row (&optional id)
+  "Signal an error if point is not on a table row.
+
+ID is the tabulated list id of the supposed entry at point."
+  (unless (or id (tabulated-list-get-id))
+    (user-error "Not on a table row")))
+
+(defun kmacro-menu--propertize-keys (face)
+  "Redisplay the macro at point with FACE."
+  (let ((data (tabulated-list-delete-entry)))
+    (setf (aref (cadr data) 4) (propertize (aref (cadr data) 4) 'face face))
+    (tabulated-list-print-entry (car data) (cadr data)))
+  (forward-line -1))
+
+(defun kmacro-menu--do-region (function &optional no-region-halt)
+  "Run FUNCTION on macros in the region or on the current line.
+
+If NO-REGION-HALT is non-nil, then if there is no region, do not
+advance."
+  (let ((advance nil))
+    (save-excursion
+      (let* ((line-beg)
+             (line-end))
+        (if (and (use-region-p)
+                 (progn
+                   (let ((reg-beg (region-beginning))
+                         (reg-end (region-end)))
+                     (setq line-beg (progn
+                                      (goto-char reg-beg)
+                                      (pos-bol))
+                           line-end (progn
+                                      (goto-char reg-end)
+                                      (if (bolp)
+                                          reg-end
+                                        (forward-line 1)
+                                        (point)))))
+                   (/= line-beg line-end)))
+            (progn
+              (goto-char line-beg)
+              (let ((id))
+                (while (and (< (point) line-end)
+                            (setq id (tabulated-list-get-id)))
+                  (kmacro-menu--assert-row id)
+                  (funcall function id)
+                  (forward-line 1))))
+          (let ((id (tabulated-list-get-id)))
+            (kmacro-menu--assert-row id)
+            (funcall function id))
+          (setq advance (not no-region-halt)))))
+    (when advance
+      (forward-line 1))))
+
+(defun kmacro-menu--marks-exist-p ()
+  "Return non-nil if markers exist for any table entries."
+  (let ((tag (gensym)))
+    (catch tag
+      (kmacro-menu--map-ids (lambda (id)
+                              (when (alist-get (kmacro-menu--id-position id)
+                                               kmacro-menu--marks)
+                                (throw tag t))))
+      nil)))
+
+;;;; Commands for Marks and Flags
+
+(defun kmacro-menu-mark ()
+  "Mark the keyboard macro at point for copying via `kmacro-menu-do-copy'.
+
+If the region is active, mark all macros in the region."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--marks)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-marked)
+     (tabulated-list-put-tag #("*" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-flag-for-deletion ()
+  "Flag the keyboard macro for deletion by `kmacro-menu-do-flagged-delete'.
+
+If the region is active, then flag all macros in the region."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--deletion-flags)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-flagged)
+     (tabulated-list-put-tag #("D" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-unmark ()
+  "Unmark and unflag the keyboard macro at point.
+
+If the region is active, then unmark all macros in the region."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--deletion-flags)
+           nil)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--marks)
+           nil)
+     (kmacro-menu--propertize-keys 'default)
+     (tabulated-list-put-tag " "))))
+
+(defun kmacro-menu-unmark-backward ()
+  "Like `kmacro-menu-unmark', but move backwards instead of forwards."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (let ((go-back (not (use-region-p))))
+    (kmacro-menu-unmark)
+    (when go-back
+      (forward-line -2))))
+
+(defun kmacro-menu-unmark-all ()
+  "Unmark and unflag all listed keyboard macros."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (setq-local kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (save-excursion
+    (goto-char (point-min))
+    (while (tabulated-list-get-id)
+      (kmacro-menu--propertize-keys 'default)
+      (forward-line 1))
+    (tabulated-list-clear-all-tags)))
+
+;;;; Commands that Modify the Ring
+
+(defun kmacro-menu-do-flagged-delete ()
+  "Delete keyboard macros flagged via `kmacro-menu-flag-for-deletion'."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (let ((res)
+        (num-deletes 0))
+    (kmacro-menu--map-ids (lambda (id)
+                            (if (alist-get (kmacro-menu--id-position id)
+                                           kmacro-menu--deletion-flags)
+                                (setq num-deletes (1+ num-deletes))
+                              (push (kmacro-menu--id-kmacro id) res))))
+    (when (yes-or-no-p (if (= 1 num-deletes)
+                           "Delete 1 keyboard macro?"
+                         (format "Delete %d keyboard macros?"
+                                 num-deletes)))
+      (kmacro-menu--update
+       (nreverse res))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-do-copy ()
+  "Duplicate the macros in the region, or the marked macros, or the one at point.
+
+Macros are duplicated at their current position in the macro ring.
+
+If the region is active, duplicate the macros in the region, regardless
+of whether there are marked macros.  Otherwise, if there are marked
+macros, delete those.  Otherwise, duplicate the one macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (let* ((region-exists (use-region-p))
+         (mark-exists (kmacro-menu--marks-exist-p))
+         (id-alist (if (or region-exists
+                           (not mark-exists))
+                       (let ((region-alist))
+                         (kmacro-menu--do-region
+                          (lambda (id)
+                            (push (cons (kmacro-menu--id-position id)
+                                        t)
+                                  region-alist))
+                          t)
+                         region-alist)
+                     kmacro-menu--marks))
+         (num-duplicates 0))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (push (kmacro-menu--id-kmacro id) res)
+                              (when (alist-get (kmacro-menu--id-position id)
+                                               id-alist)
+                                (push (kmacro-menu--id-kmacro id) res)
+                                (setq num-duplicates (1+ num-duplicates)))))
+      ;; Confirm the action if we operated on marks or the region, but
+      ;; don't confirm if operating on a single line without a region.
+      (when (if (or mark-exists region-exists)
+                (yes-or-no-p (if (= 1 num-duplicates)
+                                 "Copy (duplicate) 1 keyboard macro?"
+                               (format "Copy (duplicate) %d keyboard macros?"
+                                       num-duplicates)))
+              t)
+        (kmacro-menu--update (nreverse res))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-do-delete ()
+  "Delete the macros in the region, the marked macros, or the one at point.
+
+If the region is active, delete the macros in the region, regardless
+of whether there are marked macros.  Otherwise, if there are marked
+macros, delete those.  Otherwise, delete the one macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (let ((num-deletes 0)
+        (id-alist (if (or (use-region-p)
+                          (not (kmacro-menu--marks-exist-p)))
+                      (let ((region-alist))
+                        (kmacro-menu--do-region
+                         (lambda (id)
+                           (push (cons (kmacro-menu--id-position id)
+                                       t)
+                                 region-alist))
+                         t)
+                        region-alist)
+                    kmacro-menu--marks)))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (if (alist-get (kmacro-menu--id-position id)
+                                             id-alist)
+                                  (setq num-deletes (1+ num-deletes))
+                                (push (kmacro-menu--id-kmacro id) res))))
+      (when (yes-or-no-p (if (= 1 num-deletes)
+                             "Delete 1 keyboard macro?"
+                           (format "Delete %d keyboard macros?"
+                                   num-deletes)))
+        (kmacro-menu--update (nreverse res))
+        (tabulated-list-revert)))))
+
+;;;; Commands that Modify a Keyboard Macro
+
+(defun kmacro-menu-edit-position ()
+  "Move the keyboard macro at point to a new position."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((new-position (read-number "New position: " 0))
+           (old-km (kmacro-menu--id-kmacro id))
+           (old-pos (kmacro-menu--id-position id)))
+      (unless (= old-pos new-position)
+        (kmacro-menu--update (let ((res nil)
+                                   (true-new-pos (if (> new-position old-pos)
+                                                     (1+ new-position)
+                                                   new-position)))
+                               (kmacro-menu--map-ids (lambda (this-id)
+                                                       (let ((this-km (kmacro-menu--id-kmacro this-id))
+                                                             (this-pos (kmacro-menu--id-position this-id)))
+                                                         (unless (= old-pos this-pos)
+                                                           (when (= this-pos true-new-pos)
+                                                             (push old-km res))
+                                                           (push this-km res)))))
+                               (when (>= true-new-pos
+                                         (length tabulated-list-entries))
+                                 (push old-km res))
+                               (nreverse res)))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-transpose ()
+  "Move the keyboard macro at point to the next earlier position.
+
+Note that this is the earlier position in the ring, not the sorted
+table."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((old-pos (kmacro-menu--id-position id)))
+      (unless (= old-pos 0)
+        (let ((new-pos (1- old-pos)))
+          (kmacro-menu--update
+           (let ((res))
+             (kmacro-menu--map-ids
+              (lambda (this-id)
+                (let ((this-pos (kmacro-menu--id-position this-id)))
+                  (unless (= old-pos this-pos)
+                    (when (= new-pos this-pos)
+                      (push (kmacro-menu--id-kmacro id) res))
+                    (push (kmacro-menu--id-kmacro this-id) res)))))
+             (nreverse res))))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-edit-format ()
+  "Edit the counter format of the keyboard macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--update-at
+       (kmacro (kmacro--keys km)
+               (kmacro--counter km)
+               (read-string "New format: " nil nil
+                            (list kmacro-default-counter-format
+                                  (kmacro--format km))))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-counter ()
+  "Edit the counter of the keyboard macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--update-at
+       (kmacro (kmacro--keys km)
+               (read-number "New counter: "
+                            (list 0
+                                  (kmacro--counter
+                                   (kmacro-menu--id-kmacro id))))
+               (kmacro--format km))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-keys ()
+  "Edit the keys of the keyboard macro at point via `edmacro-mode'."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((old-km (kmacro-menu--id-kmacro id)))
+      (edit-kbd-macro (kmacro--keys old-km)
+                      nil
+                      nil
+                      (lambda (mac)
+                        (kmacro-menu--update-at
+                         (kmacro mac
+                                 (kmacro--counter old-km)
+                                 (kmacro--format old-km))
+                         (kmacro-menu--id-position id))
+                        (tabulated-list-revert))))))
+
+(defun kmacro-menu-edit-column ()
+  "Edit the value in the current column of the keyboard macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive)
+  (kmacro-menu--query-revert)
+  (let ((col (get-text-property (point) 'tabulated-list-column-name)))
+    (if (null col)
+        (user-error "No column at point")
+      (pcase col
+        ("Position"  (call-interactively #'kmacro-menu-edit-position))
+        ("Counter"  (call-interactively #'kmacro-menu-edit-counter))
+        ("Format"  (call-interactively #'kmacro-menu-edit-format))
+        ("Formatted" (user-error "Formatted counter is not editable"))
+        ("Keys" (call-interactively #'kmacro-menu-edit-keys))))))
+
+
 (provide 'kmacro)
 
 ;;; kmacro.el ends here
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#70208: [PATCH] Add command `list-keyboard-macros`
  2024-04-05  3:34 bug#70208: [PATCH] Add command `list-keyboard-macros` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-04-05  6:16 ` Eli Zaretskii
  2024-04-06 23:26   ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 6+ messages in thread
From: Eli Zaretskii @ 2024-04-05  6:16 UTC (permalink / raw)
  To: Okamsn; +Cc: 70208

> Date: Fri, 05 Apr 2024 03:34:59 +0000
> From:  Okamsn via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs@gnu.org>
> 
> The attached patch adds the command `list-keyboard-macros`, which works 
> like `list-buffers` using `tabulated-list-mode`.  It allows for 
> re-arranging the macros in the ring; editing their counters, counter 
> formats, and macro keys; deleting macros in the ring; and duplicating 
> macros for further editing.

Thanks, please see some comments below.

> --- a/doc/emacs/kmacro.texi
> +++ b/doc/emacs/kmacro.texi
> @@ -24,6 +24,12 @@ Keyboard Macros
>  keyboard macro is defined and also has been, in effect, executed once.
>  You can then do the whole thing over again by invoking the macro.
>  
> +  The list of defined keyboard macros can be seen via @kbd{M-x
> +list-keyboard-macros @key{RET}}.  This command can be used to re-order
> +the list of defined macros (the @dfn{keyboard macro ring}) and to edit
> +the properties of those keyboard macros, which are described in the
> +following subsections.

Please rewrite this not to use passive tense so much ("can be seen",
"can be used").

Also, I think this command should be documented in more detail,
including the commands in kmacro-menu-mode-map, later in the manual.
In any case, each documented command should be indexed, with an
explicit @findex.

> +*** New mode and new command 'list-keyboard-macros'.

You say "new mode", but don't mention the mode or its name.

Also, since the manuals have been updated by the patch, this entry
should be marked with "+++".

> +(defvar tabulated-list-format)
> +(defvar tabulated-list-entries)
> +(defvar tabulated-list-sort-key)
> +(declare-function tabulated-list-init-header  "tabulated-list" ())
> +(declare-function tabulated-list-print "tabulated-list"
> +                  (&optional remember-pos update))

tabulated-list is preloaded, so I don't think these are needed.

> +(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
> +  "Face used for the Keyboard Macro Menu marks."
> +  :group 'kmacro)
> +
> +(defface kmacro-menu-flagged '((t (:inherit error)))
> +  "Face used for keyboard macros flagged for deletion."
> +  :group 'kmacro)
> +
> +(defface kmacro-menu-marked '((t (:inherit warning)))
> +  "Face used for keyboard macros marked for duplication."
> +  :group 'kmacro)

Please add a :version tag to new faces.

> +(define-derived-mode kmacro-menu-mode tabulated-list-mode
> +  "Keyboard Macro Menu"
> +  "Major mode for listing keyboard macros."
                     ^^^^^^^
"listing and editing", I think?

> +(defun kmacro-menu--kmacros ()
> +  "Return a list of the existing keyboard macros."
             ^^^^^^
"the list"

Also, I think this should say "or nil, if none are defined".

> +(defun kmacro-menu--map-ids (function)
> +  "Map a FUNCTION to the current table entry IDs in order.

Our style is to say "Map FUNCTION", without "a".

Better yet, say "Apply FUNCTION to current table's entries in order."

> +Returns a list of the output of FUNCTION."

"Return", to be consistent with "Map".

> +(defun kmacro-menu--update (kmacros)
> +  "Update the variables for the current and previous keyboard macros.

This doc string doesn't say what are the "variables" to which it
alludes.

> +(defun kmacro-menu--update-at (kmacro n)
> +  "Update to KMACRO at position N."

Not sure I understand what you mean by "Update to" here.  Update what?

> +  (kmacro-menu--update
> +   (kmacro-menu--map-ids (lambda (id)
> +                           (if (= n (kmacro-menu--id-position id))
> +                               kmacro
> +                             (kmacro-menu--id-kmacro id))))))
> +
> +(defun kmacro-menu--query-revert ()
> +  "When the table differs from the existing macros, ask whether to revert table."
      ^^^^
Not "When", but "If", right?

> +  (interactive)

Interactive functions (i.e. commands) should never be internal, so the
double-dash in the name is inappropriate.

> +  (when (and (not (equal (kmacro-menu--kmacros)
> +                         (kmacro-menu--map-ids #'kmacro-menu--id-kmacro)))
> +             (yes-or-no-p "Table does not match existing keyboard macros.  Stop and revert table?"))
> +    (tabulated-list-revert)
> +    (signal 'quit nil)))
> +
> +(defun kmacro-menu--assert-row (&optional id)
> +  "Signal an error if point is not on a table row.
> +
> +ID is the tabulated list id of the supposed entry at point."
> +  (unless (or id (tabulated-list-get-id))
> +    (user-error "Not on a table row")))
> +
> +(defun kmacro-menu--propertize-keys (face)
> +  "Redisplay the macro at point with FACE."
> +  (let ((data (tabulated-list-delete-entry)))
> +    (setf (aref (cadr data) 4) (propertize (aref (cadr data) 4) 'face face))
> +    (tabulated-list-print-entry (car data) (cadr data)))
> +  (forward-line -1))
> +
> +(defun kmacro-menu--do-region (function &optional no-region-halt)
> +  "Run FUNCTION on macros in the region or on the current line.

This doesn't explain when it operates on region and when on the
current line.

> +(defun kmacro-menu-flag-for-deletion ()
> +  "Flag the keyboard macro for deletion by `kmacro-menu-do-flagged-delete'.
> +
> +If the region is active, then flag all macros in the region."
   ^^^^^^^^^^^^^^^^^^^^^^^
And if not?

> +(defun kmacro-menu-unmark ()
> +  "Unmark and unflag the keyboard macro at point.
> +
> +If the region is active, then unmark all macros in the region."

The last sentence should say "instead" at the end, to make it clear
that this is alternative to the previous sentence.

> +(defun kmacro-menu-edit-format ()
> +  "Edit the counter format of the keyboard macro at point."

Should the doc string say more about what is a valid format that the
user can type.

> +(defun kmacro-menu-edit-counter ()
> +  "Edit the counter of the keyboard macro at point."

Any motivation? why would a user want to edit the counter?

Last, but not least: please consider making at least some of the
commands in this patch specific to kmacro-menu-mode.





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#70208: [PATCH] Add command `list-keyboard-macros`
  2024-04-05  6:16 ` Eli Zaretskii
@ 2024-04-06 23:26   ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-04-07  7:55     ` Eli Zaretskii
  0 siblings, 1 reply; 6+ messages in thread
From: Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-04-06 23:26 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 70208

[-- Attachment #1: Type: text/plain, Size: 5310 bytes --]

Eli Zaretskii wrote:
>> --- a/doc/emacs/kmacro.texi
>> +++ b/doc/emacs/kmacro.texi
>> @@ -24,6 +24,12 @@ Keyboard Macros
>>   keyboard macro is defined and also has been, in effect, executed once.
>>   You can then do the whole thing over again by invoking the macro.
>>
>> +  The list of defined keyboard macros can be seen via @kbd{M-x
>> +list-keyboard-macros @key{RET}}.  This command can be used to re-order
>> +the list of defined macros (the @dfn{keyboard macro ring}) and to edit
>> +the properties of those keyboard macros, which are described in the
>> +following subsections.
> 
> Please rewrite this not to use passive tense so much ("can be seen",
> "can be used").
> 
> Also, I think this command should be documented in more detail,
> including the commands in kmacro-menu-mode-map, later in the manual.
> In any case, each documented command should be indexed, with an
> explicit @findex.

I moved the description to its own section. How is it now? I copied part 
of the Texinfo documentation of `list-buffers` and the Buffer Menu node.

>> +*** New mode and new command 'list-keyboard-macros'.
> 
> You say "new mode", but don't mention the mode or its name.
> 
> Also, since the manuals have been updated by the patch, this entry
> should be marked with "+++".

Done.

>> +(defvar tabulated-list-format)
>> +(defvar tabulated-list-entries)
>> +(defvar tabulated-list-sort-key)
>> +(declare-function tabulated-list-init-header  "tabulated-list" ())
>> +(declare-function tabulated-list-print "tabulated-list"
>> +                  (&optional remember-pos update))
> 
> tabulated-list is preloaded, so I don't think these are needed.

Removed.

>> +(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
>> +  "Face used for the Keyboard Macro Menu marks."
>> +  :group 'kmacro)
>> +
>> +(defface kmacro-menu-flagged '((t (:inherit error)))
>> +  "Face used for keyboard macros flagged for deletion."
>> +  :group 'kmacro)
>> +
>> +(defface kmacro-menu-marked '((t (:inherit warning)))
>> +  "Face used for keyboard macros marked for duplication."
>> +  :group 'kmacro)
> 
> Please add a :version tag to new faces.

Done.

>> +(define-derived-mode kmacro-menu-mode tabulated-list-mode
>> +  "Keyboard Macro Menu"
>> +  "Major mode for listing keyboard macros."
>                       ^^^^^^^
> "listing and editing", I think?

Done.

>> +(defun kmacro-menu--kmacros ()
>> +  "Return a list of the existing keyboard macros."
>               ^^^^^^
> "the list"
> 
> Also, I think this should say "or nil, if none are defined".

Changed.

>> +(defun kmacro-menu--map-ids (function)
>> +  "Map a FUNCTION to the current table entry IDs in order.
> 
> Our style is to say "Map FUNCTION", without "a".
> 
> Better yet, say "Apply FUNCTION to current table's entries in order."
> 
>> +Returns a list of the output of FUNCTION."
> 
> "Return", to be consistent with "Map".

Changed.

>> +(defun kmacro-menu--update (kmacros)
>> +  "Update the variables for the current and previous keyboard macros.
> 
> This doc string doesn't say what are the "variables" to which it
> alludes.

Changed.

>> +(defun kmacro-menu--update-at (kmacro n)
>> +  "Update to KMACRO at position N."
> 
> Not sure I understand what you mean by "Update to" here.  Update what?

I changed the functions to use "replace" instead of "update".  What I 
meant was that only existing keyboard macro at that position would be 
replaced. The others would be re-used.

>> +  (kmacro-menu--update
>> +   (kmacro-menu--map-ids (lambda (id)
>> +                           (if (= n (kmacro-menu--id-position id))
>> +                               kmacro
>> +                             (kmacro-menu--id-kmacro id))))))
>> +
>> +(defun kmacro-menu--query-revert ()
>> +  "When the table differs from the existing macros, ask whether to revert table."
>        ^^^^
> Not "When", but "If", right?

Yes. Changed.

>> +  (interactive)
> 
> Interactive functions (i.e. commands) should never be internal, so the
> double-dash in the name is inappropriate.

That function wasn't meant to be a command. I removed the `interactive` use.

> ... 

I changed the wording of the commands that act on the region when there 
is one.  Please check them again.

> 
>> +(defun kmacro-menu-edit-format ()
>> +  "Edit the counter format of the keyboard macro at point."
> 
> Should the doc string say more about what is a valid format that the
> user can type.

I added that.

>> +(defun kmacro-menu-edit-counter ()
>> +  "Edit the counter of the keyboard macro at point."
> 
> Any motivation? why would a user want to edit the counter?

Sometimes, I want to fix a mistake in a keyboard macro and then re-run 
it with a previous counter value.  Another possibility is duplicating a 
macro, changing the definition somewhat for a different context, and 
then setting the counter back to 0 or another value.

> Last, but not least: please consider making at least some of the
> commands in this patch specific to kmacro-menu-mode.

That is what I meant to do by giving the mode in the `declare` form. I 
added the mode for the `interactive` form too. Is that what you mean?

Thank you.

[-- Attachment #2: v2-0001-Add-command-list-keyboard-macros-that-works-like-.patch --]
[-- Type: text/x-patch, Size: 30549 bytes --]

From 7c9589fdbff228a400a1af5169e95321ff49ee7a Mon Sep 17 00:00:00 2001
From: Earl Hyatt <okamsn@protonmail.com>
Date: Sun, 24 Mar 2024 11:49:21 -0400
Subject: [PATCH v2] Add command 'list-keyboard-macros' that works like
 'list-buffers'.

The command 'list-keyboard-macros' allows editing and re-arranging
macros using 'tabulated-list-mode'.  Existing keyboard macros can be
duplicated or deleted.  Macro counters and counter formats can take new
values read from the minibuffer.  Macro keys can be edited using
'edit-kbd-macro'.

* doc/emacs/kmacro.texi (Kmacro Menu): Document the new command
and the menu's commands.
* etc/NEWS (Kmacro Menu Mode): Mention the new mode and command.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-marked)
(kmacro-menu-flagged): Add faces for marks and flags.
* lisp/kmacro.el (kmacro-menu-mode-map, kmacro-menu-mode): Add mode
and map.
* lisp/kmacro.el (list-keyboard-macros, kmacro-menu): Add command.
* lisp/kmacro.el (kmacro-menu--deletion-flags, kmacro-menu--marks)
(kmacro-menu--id-kmacro, kmacro-menu--id-position, kmacro-menu--kmacros)
(kmacro-menu--refresh, kmacro-menu--map-ids, kmacro-menu--replace-all)
(kmacro-menu--replace-at, kmacro-menu--query-revert, kmacro-menu--assert-row)
(kmacro-menu--propertize-keys, kmacro-menu--do-region)
(kmacro-menu--marks-exist-p): Add utility functions of mode
and commands.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-flag-for-deletion)
(kmacro-menu-unmark, kmacro-menu-unmark-backward)
(kmacro-menu-unmark-all): Add commands for marks and flags.
* lisp/kmacro.el (kmacro-menu-do-flagged-delete, kmacro-menu-do-copy)
(kmacro-menu-do-delete): Add commands that modify the ring.
* lisp/kmacro.el (kmacro-menu-edit-position, kmacro-menu-transpose)
(kmacro-menu-edit-format, kmacro-menu-edit-counter)
(kmacro-menu-edit-keys, kmacro-menu-edit-column): Add commands that
modify a keyboard macro.
---
 doc/emacs/kmacro.texi | 158 +++++++++++++
 etc/NEWS              |  10 +
 lisp/kmacro.el        | 523 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 691 insertions(+)

diff --git a/doc/emacs/kmacro.texi b/doc/emacs/kmacro.texi
index e30def34475..0e2236355f2 100644
--- a/doc/emacs/kmacro.texi
+++ b/doc/emacs/kmacro.texi
@@ -42,6 +42,8 @@ Keyboard Macros
 * Edit Keyboard Macro::      Editing keyboard macros.
 * Keyboard Macro Step-Edit:: Interactively executing and editing a keyboard
                                macro.
+* Kmacro Menu::              An interface for viewing and editing
+                               keyboard macros and the keyboard macro ring.
 @end menu
 
 @node Basic Keyboard Macro
@@ -616,3 +618,159 @@ Keyboard Macro Step-Edit
 keyboard macro; it then terminates the step-editing and replaces the
 original keyboard macro with the edited macro.
 @end itemize
+
+@node Kmacro Menu
+@section Listing Keyboard Macros
+@cindex Kmacro Menu
+
+@cindex listing current keyboard macros
+@kindex M-x list-keyboard-macros @key{RET}
+@findex kmacro-menu
+@findex list-keyboard-macros
+  To display a list of existing keyboard macros, type @kbd{M-x
+list-keyboard-macros @key{RET}}.  This pops up the @dfn{Kmacro Menu} in
+a buffer named @file{*Keyboard Macro List*}.  Each line in the list
+shows one macro's position, counter value, counter format, that counter
+value using that format, and macro keys.  Here is an example of a macro
+list:
+
+@smallexample
+Position  Counter  Format   Formatted  Keys
+0               1  %02d     01         N : SPC <F3> RET
+1               0  %d       0          l o n g SPC p h r a s e
+@end smallexample
+
+@noindent
+The macros are listed with the current macro at the top in position zero
+and the older macros in the order in which they are found in the macro
+ring (@pxref{Keyboard Macro Ring}).  Using the Kmacro Menu, you can
+change the order of the macros and change their counters, counter
+formats, and keys.  The Kmacro Menu is a read-only buffer, and can be
+changed only through the special commands described in this section.
+After a command is run, the Kmacro Menu resets to show the new values of
+the macro properties and the macro ring.  The usual cursor motion
+commands can be used in this buffer.
+
+  You can use the following commands to change a macro's properties:
+
+@table @kbd
+@item #
+@findex kmacro-menu-edit-position
+@kindex # @r{(Kmacro Menu)}
+Change the position of the macro on the current line
+(@pxref{Keyboard Macro Ring}).
+
+@item C-x C-t
+@findex kmacro-menu-transpose
+@kindex C-x C-t @r{(Kmacro Menu)}
+Move the macro on the current line to the line above, like in
+@code{transpose-lines}.
+
+@item c
+@findex kmacro-menu-edit-counter
+@kindex c @r{(Kmacro Menu)}
+Change the counter value of the macro on the current line
+(@pxref{Keyboard Macro Counter}).
+
+@item f
+@findex kmacro-menu-edit-format
+@kindex f @r{(Kmacro Menu)}
+Change the counter format of the macro on the current line.
+
+@item e
+@findex kmacro-menu-edit-keys
+@kindex e @r{(Kmacro Menu)}
+Change the keys of the macro on the current line using
+@code{edit-kbd-macro} (@pxref{Edit Keyboard Macro}).
+
+@item @key{RET}
+@findex kmacro-menu-edit-column
+@kindex @key{RET} @r{(Kmacro Menu)}
+Change the value in the current column of the macro on the current line
+using commands above.
+@end table
+
+  The following commands delete or duplicate macros in the list:
+
+@table @kbd
+@item d
+@findex kmacro-menu-flag-for-deletion
+@item d @r{(Kmacro Menu)}
+Flag the macro on the current line for deletion, then move point to the
+next line (@code{kmacro-menu-flag-for-deletion}).  The deletion flag is
+indicated by the character @samp{D} at the start of line.  The deletion
+occurs only when you type the @kbd{x} command (see below).
+
+  If the region is active, this command flags all of the macros in the
+region.
+
+@item x
+@findex kmacro-menu-do-flagged-delete
+@item x @r{(Kmacro Menu)}
+Delete the macros in the list that have been flagged for deletion
+(@code{kmacro-menu-do-flagged-delete}).
+
+@item m
+@findex kmacro-menu-mark
+@item m @r{(Kmacro Menu)}
+Mark the macro on the current line, then move point to the next line
+(@code{kmacro-menu-mark}).  Marked macros are indicated by the character
+@samp{*} at the start of line.  Marked macros can be operated on by the
+@kbd{C} and @kbd{D} commands (see below).
+
+  If the region is active, this command marks all of the macros in the
+region.
+
+@item C
+@findex kmacro-menu-do-copy
+@item C @r{(Kmacro Menu)}
+This command copies macros by duplicating them at their current
+positions in the list (@code{kmacro-menu-do-copy}).  For example,
+running this command on the macro at position zero will insert a copy of
+that macro into position 1 and move the remaining macros down.
+
+  If the region is active, this command duplicates the macros in the
+region.  Otherwise, if there are marked macros, this command duplicates
+the marked macros.  If there is no region nor are there marked macros,
+this command duplicates the macro on the current line.  In the first two
+cases, the command prompts for confirmation before duplication.
+
+@item D
+@findex kmacro-menu-do-delete
+@item D @r{(Kmacro Menu)}
+This command deletes macros, removing them from the ring
+(@code{kmacro-menu-do-delete}).  For example, running this command on
+the macro at position zero will delete the current macro and then make
+the first macro in the macro ring (previously at position one) the new
+current macro, popping it from the ring.
+
+  If the region is active, this command deletes the macros in the
+region.  Otherwise, if there are marked macros, this command deletes the
+marked macros.  If there is no region nor are there marked macros, this
+command deletes the macro on the current line.  In all cases, the
+command prompts for confirmation before duplication.
+
+  This command is an alternative to the @kbd{d} and @kbd{x} commands
+(see above).
+
+@item u
+@findex kmacro-menu-unmark
+@item u @r{(Kmacro Menu)}
+Unmark and unflag the macro on the current line, then move point down
+to the next line (@code{kmacro-menu-unmark}).  If there is an active
+region, this command unmarks and unflags all of the macros in the
+region.
+
+@item @key{DEL}
+@findex kmacro-menu-unmark-backward
+@item @key{DEL} @r{(Kmacro Menu)}
+Like the @kbd{u} command (see above), but move point up to the previous
+line when there is no active region
+(@code{kmacro-menu-unmark-backward}).
+
+@item U
+@findex kmacro-menu-unmark-all
+@item U @r{(Kmacro Menu)}
+Unmark and unflag all macros in the list
+(@code{kmacro-menu-unmark-all}).
+@end table
diff --git a/etc/NEWS b/etc/NEWS
index eda84d588a8..7c7c26626a6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1388,6 +1388,16 @@ When this is non-nil, the lines of key sequences are displayed with
 the most recent line first.  This is can be useful when working with
 macros with many lines, such as from 'kmacro-edit-lossage'.
 
+** Kmacro Menu Mode
+
++++
+*** New mode 'kmacro-menu-mode' and new command 'list-keyboard-macros'.
+The new command 'list-keyboard-macros' the macro version of commands
+like 'list-buffers' and 'list-processes', creating a listing of the
+currently existing keyboards macros using the new 'kmacro-menu-mode'.
+It allows rearranging the macros in the ring, duplicating them, deleting
+them, and editing their counters, formats, and keys.
+
 ** Miscellaneous
 
 ---
diff --git a/lisp/kmacro.el b/lisp/kmacro.el
index 897ebf14330..5e985546c37 100644
--- a/lisp/kmacro.el
+++ b/lisp/kmacro.el
@@ -1388,6 +1388,529 @@ kmacro-redisplay
     (let ((executing-kbd-macro nil))
       (redisplay))))
 
+;;; Mode and commands for working with the ring in a table
+
+(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
+  "Face used for the Keyboard Macro Menu marks."
+  :group 'kmacro
+  :version "30.0.50")
+
+(defface kmacro-menu-flagged '((t (:inherit error)))
+  "Face used for keyboard macros flagged for deletion."
+  :group 'kmacro
+  :version "30.0.50")
+
+(defface kmacro-menu-marked '((t (:inherit warning)))
+  "Face used for keyboard macros marked for duplication."
+  :group 'kmacro
+  :version "30.0.50")
+
+(defvar-keymap kmacro-menu-mode-map
+  :doc "Keymap for `kmacro-menu-mode'."
+  :parent tabulated-list-mode-map
+  "#" #'kmacro-menu-edit-position
+  "c" #'kmacro-menu-edit-counter
+  "e" #'kmacro-menu-edit-keys
+  "f" #'kmacro-menu-edit-format
+  "RET" #'kmacro-menu-edit-column
+
+  "C" #'kmacro-menu-do-copy
+  "D" #'kmacro-menu-do-delete
+  "m" #'kmacro-menu-mark
+
+  "d" #'kmacro-menu-flag-for-deletion
+  "x" #'kmacro-menu-do-flagged-delete
+
+  "u" #'kmacro-menu-unmark
+  "U" #'kmacro-menu-unmark-all
+  "DEL"#'kmacro-menu-unmark-backward
+
+  "<remap> <transpose-lines>" #'kmacro-menu-transpose)
+
+(define-derived-mode kmacro-menu-mode tabulated-list-mode
+  "Keyboard Macro Menu"
+  "Major mode for listing and editing keyboard macros."
+  (make-local-variable 'kmacro-menu--marks)
+  (make-local-variable 'kmacro-menu--deletion-flags)
+  (setq-local tabulated-list-format
+              [("Position" 8 nil)
+               ("Counter"  8 nil :right-align t :pad-right 2)
+               ("Format"  8 nil)
+               ("Formatted" 10 nil)
+               ("Keys" 1 nil)])
+  (setq-local tabulated-list-padding 2)
+  (add-hook 'tabulated-list-revert-hook #'kmacro-menu--refresh nil t)
+  (tabulated-list-init-header)
+  (unless (kmacro-ring-empty-p)
+    (kmacro-menu--refresh)
+    (tabulated-list-print)))
+
+(defalias 'kmacro-menu #'list-keyboard-macros)
+(defun list-keyboard-macros ()
+  "List the keyboard macros."
+  (interactive)
+  (let ((buf (get-buffer-create "*Keyboard Macro List*")))
+    (with-current-buffer buf
+      (kmacro-menu-mode))
+    (pop-to-buffer buf)))
+
+;;;; Utility functions and mode data
+
+(defvar kmacro-menu--deletion-flags nil
+  "Alist of entries flagged for deletion.")
+
+(defvar kmacro-menu--marks nil
+  "Alist of entries marked for copying and duplication.")
+
+(defun kmacro-menu--id-kmacro (entry-id)
+  "Return keyboard macro that is part of the ENTRY-ID."
+  (car entry-id))
+
+(defun kmacro-menu--id-position (entry-id)
+  "Return ordinal position that is part of the ENTRY-ID."
+  (cdr entry-id))
+
+(defun kmacro-menu--kmacros ()
+  "Return the list of the existing keyboard macros or nil, if none are defined."
+  (when last-kbd-macro
+    (cons (kmacro-ring-head)
+          kmacro-ring)))
+
+(defun kmacro-menu--refresh ()
+  "Reset the list of keyboard macros."
+  (setq-local tabulated-list-entries
+              (seq-map-indexed (lambda (km idx)
+                                 (let ((cnt (kmacro--counter km))
+                                       (fmt (kmacro--format km)))
+                                   `((,km . ,idx)
+                                     [,(format "%d" idx)
+                                      ,(format "%d" cnt)
+                                      ,fmt
+                                      ,(format fmt cnt)
+                                      ,(format-kbd-macro (kmacro--keys km))])))
+                               (kmacro-menu--kmacros))
+              kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (tabulated-list-clear-all-tags))
+
+(defun kmacro-menu--map-ids (function)
+  "Apply FUNCTION to the current table's entry IDs in order.
+
+If FILTER is non-nil, then IDs for which FILTER returns nil are
+excluded.
+
+Return a list of the output of FUNCTION."
+  (mapcar function
+          (mapcar #'car
+                  (seq-sort-by #'cdar #'< tabulated-list-entries))))
+
+(defun kmacro-menu--replace-all (kmacros)
+  "Replace the existing keyboard macros with those in KMACROS.
+
+The first element in the list overwrites the values of `last-kbd-macro',
+`kmacro-counter', and `kmacro-counter-format'.  The remaining elements
+become the value of `kmacro-ring'.
+
+KMACROS is a list of `kmacro' objects."
+  (if (null kmacros)
+      (setq last-kbd-macro nil
+            kmacro-counter-format kmacro-default-counter-format
+            kmacro-counter 0
+            kmacro-ring nil)
+    (kmacro-split-ring-element (car kmacros))
+    (setq kmacro-ring (cdr kmacros))))
+
+(defun kmacro-menu--replace-at (kmacro n)
+  "Replace the keyboard macro at position N with KMACRO.
+
+This function replaces all of the existing keyboard macros via
+`kmacro-menu--replace-all'.  Except for the macro at position N, which will
+be KMACRO, the replacement macros are the existing macros identified in
+the table."
+  (kmacro-menu--replace-all
+   (kmacro-menu--map-ids (lambda (id)
+                           (if (= n (kmacro-menu--id-position id))
+                               kmacro
+                             (kmacro-menu--id-kmacro id))))))
+
+(defun kmacro-menu--query-revert ()
+  "If the table differs from the existing macros, ask whether to revert table."
+  (when (and (not (equal (kmacro-menu--kmacros)
+                         (kmacro-menu--map-ids #'kmacro-menu--id-kmacro)))
+             (yes-or-no-p "Table does not match existing keyboard macros.  Stop and revert table?"))
+    (tabulated-list-revert)
+    (signal 'quit nil)))
+
+(defun kmacro-menu--assert-row (&optional id)
+  "Signal an error if point is not on a table row.
+
+ID is the tabulated list id of the supposed entry at point."
+  (unless (or id (tabulated-list-get-id))
+    (user-error "Not on a table row")))
+
+(defun kmacro-menu--propertize-keys (face)
+  "Redisplay the macro keys at point with FACE."
+  (tabulated-list-set-col 4 (propertize (aref (tabulated-list-get-entry) 4)
+                                        'face face)))
+
+(defun kmacro-menu--do-region (function)
+  "Run FUNCTION on macros in the region or on the current line at the line start.
+
+If there is an active region, for each line in the region, move to the
+beginning of the line and apply FUNCTION to the table entry ID of the
+line.  If there is no region, apply FUNCTION only to the table entry ID
+of the current line.
+
+When there is no active region, advance to the beginning of the next
+line after applying FUNCTION."
+  (if (use-region-p)
+      (save-excursion
+        (let* ((reg-beg (region-beginning))
+               (reg-end (region-end))
+               (line-beg (progn
+                           (goto-char reg-beg)
+                           (pos-bol)))
+               (line-end (let ((reg-end reg-end))
+                           (goto-char reg-end)
+                           (if (bolp)
+                               reg-end
+                             (pos-bol 2)))))
+          (goto-char line-beg)
+          (let ((id))
+            (while (and (< (point) line-end)
+                        (setq id (tabulated-list-get-id)))
+              (kmacro-menu--assert-row id)
+              (funcall function id)
+              (forward-line 1)))))
+    (let ((id (tabulated-list-get-id)))
+      (kmacro-menu--assert-row id)
+      (goto-char (pos-bol))
+      (funcall function id)
+      (forward-line 1))))
+
+(defun kmacro-menu--marks-exist-p ()
+  "Return non-nil if markers exist for any table entries."
+  (let ((tag (gensym)))
+    (catch tag
+      (kmacro-menu--map-ids (lambda (id)
+                              (when (alist-get (kmacro-menu--id-position id)
+                                               kmacro-menu--marks)
+                                (throw tag t))))
+      nil)))
+
+;;;; Commands for Marks and Flags
+
+(defun kmacro-menu-mark ()
+  "Mark macros in the region or, otherwise, on the current line.
+
+If marking the current line, move point to the next line when done.
+
+Marked macros can be operated on by `kmacro-menu-do-copy' and
+`kmacro-menu-do-delete'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--marks)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-marked)
+     (tabulated-list-put-tag #("*" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-flag-for-deletion ()
+  "Flag macros in the region or, otherwise, on the current line.
+
+If marking the current line, move point to the next line when done.
+
+Flagged macros can be deleted via `kmacro-menu-do-flagged-delete'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--deletion-flags)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-flagged)
+     (tabulated-list-put-tag #("D" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-unmark ()
+  "Unmark and unflag macros in the region or, otherwise, on the current line.
+
+If unmarking or unflagging the current line, move point to the next line
+when done."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (let ((pos (kmacro-menu--id-position id)))
+       (setf (alist-get pos kmacro-menu--deletion-flags) nil
+             (alist-get pos kmacro-menu--marks) nil))
+     (kmacro-menu--propertize-keys 'default)
+     (tabulated-list-put-tag " "))))
+
+(defun kmacro-menu-unmark-backward ()
+  "Like `kmacro-menu-unmark', but move backwards instead of forwards."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((go-back (not (use-region-p))))
+    (kmacro-menu-unmark)
+    (when go-back
+      (forward-line -2))))
+
+(defun kmacro-menu-unmark-all ()
+  "Unmark and unflag all listed keyboard macros."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (setq-local kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (save-excursion
+    (goto-char (point-min))
+    (while (tabulated-list-get-id)
+      (kmacro-menu--propertize-keys 'default)
+      (forward-line 1))
+    (tabulated-list-clear-all-tags)))
+
+;;;; Commands that Modify the Ring
+
+(defun kmacro-menu-do-flagged-delete ()
+  "Delete keyboard macros flagged via `kmacro-menu-flag-for-deletion'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((res)
+        (num-deletes 0))
+    (kmacro-menu--map-ids (lambda (id)
+                            (if (alist-get (kmacro-menu--id-position id)
+                                           kmacro-menu--deletion-flags)
+                                (setq num-deletes (1+ num-deletes))
+                              (push (kmacro-menu--id-kmacro id) res))))
+    (when (yes-or-no-p (if (= 1 num-deletes)
+                           "Delete 1 keyboard macro?"
+                         (format "Delete %d keyboard macros?"
+                                 num-deletes)))
+      (kmacro-menu--replace-all
+       (nreverse res))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-do-copy ()
+  "Duplicate the macros in the region, or the marked macros, or the one at point.
+
+Macros are duplicated at their current position in the macro ring.
+
+If the region is active, duplicate the macros in the region, regardless
+of whether there are marked macros.  Otherwise, if there are marked
+macros, delete those.  Otherwise, duplicate the one macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let* ((region-exists (use-region-p))
+         (mark-exists (kmacro-menu--marks-exist-p))
+         (id-alist (if (or region-exists
+                           (not mark-exists))
+                       (let ((region-alist))
+                         (kmacro-menu--do-region
+                          (lambda (id)
+                            (push (cons (kmacro-menu--id-position id)
+                                        t)
+                                  region-alist)))
+                         region-alist)
+                     kmacro-menu--marks))
+         (num-duplicates 0))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (let ((pos (kmacro-menu--id-position id))
+                                    (km (kmacro-menu--id-kmacro id)))
+                                (push km res)
+                                (when (alist-get pos id-alist)
+                                  (push km res)
+                                  (setq num-duplicates (1+ num-duplicates))))))
+      ;; Confirm the action if we operated on marks or the region, but
+      ;; don't confirm if operating on a single line without a region.
+      (when (if (or mark-exists region-exists)
+                (yes-or-no-p (if (= 1 num-duplicates)
+                                 "Copy (duplicate) 1 keyboard macro?"
+                               (format "Copy (duplicate) %d keyboard macros?"
+                                       num-duplicates)))
+              t)
+        (kmacro-menu--replace-all (nreverse res))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-do-delete ()
+  "Delete the macros in the region, the marked macros, or the one at point.
+
+If the region is active, delete the macros in the region, regardless
+of whether there are marked macros.  Otherwise, if there are marked
+macros, delete those.  Otherwise, delete the one macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((num-deletes 0)
+        (id-alist (if (or (use-region-p)
+                          (not (kmacro-menu--marks-exist-p)))
+                      (let ((region-alist))
+                        (kmacro-menu--do-region
+                         (lambda (id)
+                           (push (cons (kmacro-menu--id-position id)
+                                       t)
+                                 region-alist)))
+                        region-alist)
+                    kmacro-menu--marks)))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (if (alist-get (kmacro-menu--id-position id)
+                                             id-alist)
+                                  (setq num-deletes (1+ num-deletes))
+                                (push (kmacro-menu--id-kmacro id) res))))
+      (when (yes-or-no-p (if (= 1 num-deletes)
+                             "Delete 1 keyboard macro?"
+                           (format "Delete %d keyboard macros?"
+                                   num-deletes)))
+        (kmacro-menu--replace-all (nreverse res))
+        (tabulated-list-revert)))))
+
+;;;; Commands that Modify a Keyboard Macro
+
+(defun kmacro-menu-edit-position ()
+  "Move the keyboard macro at point to a new position.
+
+See the Info node `(emacs) Keyboard Macro Ring' for more information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((new-position (min (length tabulated-list-entries)
+                              (max 0
+                                   (read-number "New position: " 0))))
+           (old-km (kmacro-menu--id-kmacro id))
+           (old-pos (kmacro-menu--id-position id)))
+      (unless (= old-pos new-position)
+        (kmacro-menu--replace-all
+         (let ((res)
+               (true-new-pos (if (> new-position old-pos)
+                                 (1+ new-position)
+                               new-position)))
+           (kmacro-menu--map-ids (lambda (this-id)
+                                   (let ((this-km (kmacro-menu--id-kmacro this-id))
+                                         (this-pos (kmacro-menu--id-position this-id)))
+                                     (unless (= old-pos this-pos)
+                                       (when (= this-pos true-new-pos)
+                                         (push old-km res))
+                                       (push this-km res)))))
+           (when (>= true-new-pos
+                     (length tabulated-list-entries))
+             (push old-km res))
+           (nreverse res)))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-transpose ()
+  "Move the keyboard macro at point to the next earlier position.
+
+Note that this is the earlier position in the ring, not the sorted
+table."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((old-pos (kmacro-menu--id-position id)))
+      (unless (= old-pos 0)
+        (let ((new-pos (1- old-pos)))
+          (kmacro-menu--replace-all
+           (let ((res))
+             (kmacro-menu--map-ids
+              (lambda (this-id)
+                (let ((this-pos (kmacro-menu--id-position this-id)))
+                  (unless (= old-pos this-pos)
+                    (when (= new-pos this-pos)
+                      (push (kmacro-menu--id-kmacro id) res))
+                    (push (kmacro-menu--id-kmacro this-id) res)))))
+             (nreverse res))))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-edit-format ()
+  "Edit the counter format of the keyboard macro at point.
+
+Valid counter formats are those for integers accepted by `format'.
+
+See the command `kmacro-set-format' and the Info node `(emacs) Keyboard
+Macro Counter' for more information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--replace-at
+       (kmacro (kmacro--keys km)
+               (kmacro--counter km)
+               (read-string "New format: " nil nil
+                            (list kmacro-default-counter-format
+                                  (kmacro--format km))))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-counter ()
+  "Edit the counter of the keyboard macro at point.
+
+See Info node `(emacs) Keyboard Macro Counter' for more
+information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--replace-at
+       (kmacro (kmacro--keys km)
+               (read-number "New counter: "
+                            (list 0
+                                  (kmacro--counter
+                                   (kmacro-menu--id-kmacro id))))
+               (kmacro--format km))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-keys ()
+  "Edit the keys of the keyboard macro at point via `edmacro-mode'.
+
+See Info node `(emacs) Edit Keyboard Macro' for more
+information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((old-km (kmacro-menu--id-kmacro id)))
+      (edit-kbd-macro (kmacro--keys old-km)
+                      nil
+                      nil
+                      (lambda (mac)
+                        (kmacro-menu--replace-at
+                         (kmacro mac
+                                 (kmacro--counter old-km)
+                                 (kmacro--format old-km))
+                         (kmacro-menu--id-position id))
+                        (tabulated-list-revert))))))
+
+(defun kmacro-menu-edit-column ()
+  "Edit the value in the current column of the keyboard macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (pcase (get-text-property (point) 'tabulated-list-column-name)
+    ('nil        (user-error "No column at point"))
+    ("Position"  (call-interactively #'kmacro-menu-edit-position))
+    ("Counter"   (call-interactively #'kmacro-menu-edit-counter))
+    ("Format"    (call-interactively #'kmacro-menu-edit-format))
+    ("Formatted" (user-error "Formatted counter is not editable"))
+    ("Keys"      (call-interactively #'kmacro-menu-edit-keys))))
+
 (provide 'kmacro)
 
 ;;; kmacro.el ends here
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#70208: [PATCH] Add command `list-keyboard-macros`
  2024-04-06 23:26   ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-04-07  7:55     ` Eli Zaretskii
  2024-04-13 19:24       ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 6+ messages in thread
From: Eli Zaretskii @ 2024-04-07  7:55 UTC (permalink / raw)
  To: Okamsn; +Cc: 70208

> Date: Sat, 06 Apr 2024 23:26:15 +0000
> From: Okamsn <okamsn@protonmail.com>
> Cc: 70208@debbugs.gnu.org
> 
> +@node Kmacro Menu
> +@section Listing Keyboard Macros

"Listing and Editing", I guess?

> +After a command is run, the Kmacro Menu resets to show the new values of
                                           ^^^^^^
"Resets" is not the best word here.  I suggest to rephrase:

  After a command is run, the Kmacro Menu display changes to reflect
  the new values of ...

> +the macro properties and the macro ring.  The usual cursor motion
> +commands can be used in this buffer.

"You can use the usual cursor motion commands in this buffer."  This
avoids passive tense.

> +@item D @r{(Kmacro Menu)}
> +This command deletes macros, removing them from the ring
> +(@code{kmacro-menu-do-delete}).  For example, running this command on
> +the macro at position zero will delete the current macro and then make
> +the first macro in the macro ring (previously at position one) the new
> +current macro, popping it from the ring.
> +
> +  If the region is active, this command deletes the macros in the
> +region.  Otherwise, if there are marked macros, this command deletes the
> +marked macros.  If there is no region nor are there marked macros, this
> +command deletes the macro on the current line.  In all cases, the
> +command prompts for confirmation before duplication.
                                    ^^^^^^^^^^^^^^^^^^
"before deletion", right?

> ++++
> +*** New mode 'kmacro-menu-mode' and new command 'list-keyboard-macros'.
> +The new command 'list-keyboard-macros' the macro version of commands
                                         ^
I think "is" is missing there.

> +(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
> +  "Face used for the Keyboard Macro Menu marks."
> +  :group 'kmacro
> +  :version "30.0.50")

The version should be "30.1", the next released version (here and
elsewhere in the patch).  We never tag options with development
versions.

> +(defun kmacro-menu-mark ()
> +  "Mark macros in the region or, otherwise, on the current line.

I'd remove the "otherwise" part, and explain that in the next lines:

    Mark macros in the region or on the current line.

  If there's an active region, mark macros in the region; otherwise
  mark the macro on the current line.

> +(defun kmacro-menu-flag-for-deletion ()
> +  "Flag macros in the region or, otherwise, on the current line.

Likewise here and in all other similar commands (some of them already
have the "if there's an active region" part).

Thanks.





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#70208: [PATCH] Add command `list-keyboard-macros`
  2024-04-07  7:55     ` Eli Zaretskii
@ 2024-04-13 19:24       ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-04-14  9:41         ` Eli Zaretskii
  0 siblings, 1 reply; 6+ messages in thread
From: Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-04-13 19:24 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 70208

[-- Attachment #1: Type: text/plain, Size: 2820 bytes --]

Eli Zaretskii wrote:
>> Date: Sat, 06 Apr 2024 23:26:15 +0000
>> From: Okamsn <okamsn@protonmail.com>
>> Cc: 70208@debbugs.gnu.org
>>
>> +@node Kmacro Menu
>> +@section Listing Keyboard Macros
> 
> "Listing and Editing", I guess?
> 
>> +After a command is run, the Kmacro Menu resets to show the new values of
>                                             ^^^^^^
> "Resets" is not the best word here.  I suggest to rephrase:
> 
>    After a command is run, the Kmacro Menu display changes to reflect
>    the new values of ...
> 
>> +the macro properties and the macro ring.  The usual cursor motion
>> +commands can be used in this buffer.
> 
> "You can use the usual cursor motion commands in this buffer."  This
> avoids passive tense.
> 
>> +@item D @r{(Kmacro Menu)}
>> +This command deletes macros, removing them from the ring
>> +(@code{kmacro-menu-do-delete}).  For example, running this command on
>> +the macro at position zero will delete the current macro and then make
>> +the first macro in the macro ring (previously at position one) the new
>> +current macro, popping it from the ring.
>> +
>> +  If the region is active, this command deletes the macros in the
>> +region.  Otherwise, if there are marked macros, this command deletes the
>> +marked macros.  If there is no region nor are there marked macros, this
>> +command deletes the macro on the current line.  In all cases, the
>> +command prompts for confirmation before duplication.
>                                      ^^^^^^^^^^^^^^^^^^
> "before deletion", right?
> 
>> ++++
>> +*** New mode 'kmacro-menu-mode' and new command 'list-keyboard-macros'.
>> +The new command 'list-keyboard-macros' the macro version of commands
>                                           ^
> I think "is" is missing there.
> 
>> +(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
>> +  "Face used for the Keyboard Macro Menu marks."
>> +  :group 'kmacro
>> +  :version "30.0.50")
> 
> The version should be "30.1", the next released version (here and
> elsewhere in the patch).  We never tag options with development
> versions.
> 
>> +(defun kmacro-menu-mark ()
>> +  "Mark macros in the region or, otherwise, on the current line.
> 
> I'd remove the "otherwise" part, and explain that in the next lines:
> 
>      Mark macros in the region or on the current line.
> 
>    If there's an active region, mark macros in the region; otherwise
>    mark the macro on the current line.
> 
>> +(defun kmacro-menu-flag-for-deletion ()
>> +  "Flag macros in the region or, otherwise, on the current line.
> 
> Likewise here and in all other similar commands (some of them already
> have the "if there's an active region" part).
> 
> Thanks.

Please see the attached.

Thank you.

[-- Attachment #2: v4-0001-Add-command-list-keyboard-macros-that-works-like-.patch --]
[-- Type: text/x-patch, Size: 32519 bytes --]

From bfdc27b5684eebbd1b432f6062ca81b89385a9cb Mon Sep 17 00:00:00 2001
From: Earl Hyatt <okamsn@protonmail.com>
Date: Sun, 24 Mar 2024 11:49:21 -0400
Subject: [PATCH v4] Add command 'list-keyboard-macros' that works like
 'list-buffers'.

The command 'list-keyboard-macros' allows editing and re-arranging
macros using 'tabulated-list-mode'.  Existing keyboard macros can be
duplicated or deleted.  Macro counters and counter formats can take new
values read from the minibuffer.  Macro keys can be edited using
'edit-kbd-macro'.

* doc/emacs/kmacro.texi (Kmacro Menu): Document the new command
and the menu's commands.
* etc/NEWS (Kmacro Menu Mode): Mention the new mode and command.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-marked)
(kmacro-menu-flagged): Add faces for marks and flags.
* lisp/kmacro.el (kmacro-menu-mode-map, kmacro-menu-mode): Add mode
and map.
* lisp/kmacro.el (list-keyboard-macros, kmacro-menu): Add command.
* lisp/kmacro.el (kmacro-menu--deletion-flags, kmacro-menu--marks)
(kmacro-menu--id-kmacro, kmacro-menu--id-position, kmacro-menu--kmacros)
(kmacro-menu--refresh, kmacro-menu--map-ids, kmacro-menu--replace-all)
(kmacro-menu--replace-at, kmacro-menu--query-revert, kmacro-menu--assert-row)
(kmacro-menu--propertize-keys, kmacro-menu--do-region)
(kmacro-menu--marks-exist-p): Add utility functions of mode
and commands.
* lisp/kmacro.el (kmacro-menu-mark, kmacro-menu-flag-for-deletion)
(kmacro-menu-unmark, kmacro-menu-unmark-backward)
(kmacro-menu-unmark-all): Add commands for marks and flags.
* lisp/kmacro.el (kmacro-menu-do-flagged-delete, kmacro-menu-do-copy)
(kmacro-menu-do-delete): Add commands that modify the ring.
* lisp/kmacro.el (kmacro-menu-edit-position, kmacro-menu-transpose)
(kmacro-menu-edit-format, kmacro-menu-edit-counter)
(kmacro-menu-edit-keys, kmacro-menu-edit-column): Add commands that
modify a keyboard macro.
---
 doc/emacs/kmacro.texi | 162 ++++++++++++
 etc/NEWS              |  11 +
 lisp/kmacro.el        | 558 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 731 insertions(+)

diff --git a/doc/emacs/kmacro.texi b/doc/emacs/kmacro.texi
index e30def34475..4a8d4d4f093 100644
--- a/doc/emacs/kmacro.texi
+++ b/doc/emacs/kmacro.texi
@@ -42,6 +42,8 @@ Keyboard Macros
 * Edit Keyboard Macro::      Editing keyboard macros.
 * Keyboard Macro Step-Edit:: Interactively executing and editing a keyboard
                                macro.
+* Kmacro Menu::              An interface for listing and editing
+                               keyboard macros and the keyboard macro ring.
 @end menu
 
 @node Basic Keyboard Macro
@@ -616,3 +618,163 @@ Keyboard Macro Step-Edit
 keyboard macro; it then terminates the step-editing and replaces the
 original keyboard macro with the edited macro.
 @end itemize
+
+@node Kmacro Menu
+@section Listing and Editing Keyboard Macros
+@cindex Kmacro Menu
+
+@cindex listing current keyboard macros
+@kindex M-x list-keyboard-macros @key{RET}
+@findex kmacro-menu
+@findex list-keyboard-macros
+  To display a list of existing keyboard macros, type @kbd{M-x
+list-keyboard-macros @key{RET}}.  This pops up the @dfn{Kmacro Menu} in
+a buffer named @file{*Keyboard Macro List*}.  Each line in the list
+shows one macro's position, counter value, counter format, that counter
+value using that format, and macro keys.  Here is an example of a macro
+list:
+
+@smallexample
+Position  Counter  Format  Formatted  Keys
+0               8  %02d    08         N : SPC <F3> RET
+1               0  %d      0          l o n g SPC p h r a s e
+@end smallexample
+
+@noindent
+The macros are listed with the current macro at the top in position
+number zero and the older macros in the order in which they are found in
+the keyboard macro ring (@pxref{Keyboard Macro Ring}).  Using the Kmacro
+Menu, you can change the order of the macros and change their counters,
+counter formats, and keys.  The Kmacro Menu is a read-only buffer, and
+can be changed only through the special commands described in this
+section.  After a command is run, the Kmacro Menu displays changes to
+reflect the new values of the macro properties and the macro ring.  You
+can use the usual cursor motion commands in this buffer, as well as
+special motion commands for navigating the table.  To view a list of the
+special commands, type @kbd{C-h m} or @kbd{?} (@code{describe-mode}) in
+the Kmacro Menu.
+
+  You can use the following commands to change a macro's properties:
+
+@table @kbd
+@item #
+@findex kmacro-menu-edit-position
+@kindex # @r{(Kmacro Menu)}
+Change the position of the macro on the current line
+(@pxref{Keyboard Macro Ring}).
+
+@item C-x C-t
+@findex kmacro-menu-transpose
+@kindex C-x C-t @r{(Kmacro Menu)}
+Move the macro on the current line to the line above, like in
+@code{transpose-lines}.
+
+@item c
+@findex kmacro-menu-edit-counter
+@kindex c @r{(Kmacro Menu)}
+Change the counter value of the macro on the current line
+(@pxref{Keyboard Macro Counter}).
+
+@item f
+@findex kmacro-menu-edit-format
+@kindex f @r{(Kmacro Menu)}
+Change the counter format of the macro on the current line.
+
+@item e
+@findex kmacro-menu-edit-keys
+@kindex e @r{(Kmacro Menu)}
+Change the keys of the macro on the current line using
+@code{edit-kbd-macro} (@pxref{Edit Keyboard Macro}).
+
+@item @key{RET}
+@findex kmacro-menu-edit-column
+@kindex @key{RET} @r{(Kmacro Menu)}
+Change the value in the current column of the macro on the current line
+using commands above.
+@end table
+
+  The following commands delete or duplicate macros in the list:
+
+@table @kbd
+@item d
+@findex kmacro-menu-flag-for-deletion
+@item d @r{(Kmacro Menu)}
+Flag the macro on the current line for deletion, then move point to the
+next line (@code{kmacro-menu-flag-for-deletion}).  The deletion flag is
+indicated by the character @samp{D} at the start of line.  The deletion
+occurs only when you type the @kbd{x} command (see below).
+
+  If the region is active, this command flags all of the macros in the
+region.
+
+@item x
+@findex kmacro-menu-do-flagged-delete
+@item x @r{(Kmacro Menu)}
+Delete the macros in the list that have been flagged for deletion
+(@code{kmacro-menu-do-flagged-delete}).
+
+@item m
+@findex kmacro-menu-mark
+@item m @r{(Kmacro Menu)}
+Mark the macro on the current line, then move point to the next line
+(@code{kmacro-menu-mark}).  Marked macros are indicated by the character
+@samp{*} at the start of line.  Marked macros can be operated on by the
+@kbd{C} and @kbd{D} commands (see below).
+
+  If the region is active, this command marks all of the macros in the
+region.
+
+@item C
+@findex kmacro-menu-do-copy
+@item C @r{(Kmacro Menu)}
+This command copies macros by duplicating them at their current
+positions in the list (@code{kmacro-menu-do-copy}).  For example,
+running this command on the macro at position number zero will insert a
+copy of that macro into position number one and move the remaining
+macros down.
+
+  If the region is active, this command duplicates the macros in the
+region.  Otherwise, if there are marked macros, this command duplicates
+the marked macros.  If there is no region nor are there marked macros,
+this command duplicates the macro on the current line.  In the first two
+cases, the command prompts for confirmation before duplication.
+
+@item D
+@findex kmacro-menu-do-delete
+@item D @r{(Kmacro Menu)}
+This command deletes macros, removing them from the ring
+(@code{kmacro-menu-do-delete}).  For example, running this command on
+the macro at position number zero will delete the current macro and then
+make the first macro in the macro ring (previously at position number
+one) the new current macro, popping it from the ring.
+
+  If the region is active, this command deletes the macros in the
+region.  Otherwise, if there are marked macros, this command deletes the
+marked macros.  If there is no region nor are there marked macros, this
+command deletes the macro on the current line.  In all cases, the
+command prompts for confirmation before deletion.
+
+  This command is an alternative to the @kbd{d} and @kbd{x} commands
+(see above).
+
+@item u
+@findex kmacro-menu-unmark
+@item u @r{(Kmacro Menu)}
+Unmark and unflag the macro on the current line, then move point down
+to the next line (@code{kmacro-menu-unmark}).  If there is an active
+region, this command unmarks and unflags all of the macros in the
+region.
+
+@item @key{DEL}
+@findex kmacro-menu-unmark-backward
+@item @key{DEL} @r{(Kmacro Menu)}
+Like the @kbd{u} command (see above), but move point up to the previous
+line when there is no active region
+(@code{kmacro-menu-unmark-backward}).
+
+@item U
+@findex kmacro-menu-unmark-all
+@item U @r{(Kmacro Menu)}
+Unmark and unflag all macros in the list
+(@code{kmacro-menu-unmark-all}).
+@end table
diff --git a/etc/NEWS b/etc/NEWS
index eda84d588a8..0968f7cb7be 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1388,6 +1388,17 @@ When this is non-nil, the lines of key sequences are displayed with
 the most recent line first.  This is can be useful when working with
 macros with many lines, such as from 'kmacro-edit-lossage'.
 
+** Kmacro Menu Mode
+
++++
+*** New mode 'kmacro-menu-mode' and new command 'list-keyboard-macros'.
+The new command 'list-keyboard-macros' the is the keyboard-macro version
+of commands like 'list-buffers' and 'list-processes', creating a listing
+of the currently existing keyboards macros using the new
+'kmacro-menu-mode'.  It allows rearranging the macros in the ring,
+duplicating them, deleting them, and editing their counters, formats,
+and keys.
+
 ** Miscellaneous
 
 ---
diff --git a/lisp/kmacro.el b/lisp/kmacro.el
index 897ebf14330..a16f70105c1 100644
--- a/lisp/kmacro.el
+++ b/lisp/kmacro.el
@@ -1388,6 +1388,564 @@ kmacro-redisplay
     (let ((executing-kbd-macro nil))
       (redisplay))))
 
+;;; Mode and commands for working with the ring in a table
+
+(defface kmacro-menu-mark '((t (:inherit font-lock-constant-face)))
+  "Face used for the Keyboard Macro Menu marks."
+  :group 'kmacro
+  :version "30.1")
+
+(defface kmacro-menu-flagged '((t (:inherit error)))
+  "Face used for keyboard macros flagged for deletion."
+  :group 'kmacro
+  :version "30.1")
+
+(defface kmacro-menu-marked '((t (:inherit warning)))
+  "Face used for keyboard macros marked for duplication."
+  :group 'kmacro
+  :version "30.1")
+
+(defvar-keymap kmacro-menu-mode-map
+  :doc "Keymap for `kmacro-menu-mode'."
+  :parent tabulated-list-mode-map
+  "#" #'kmacro-menu-edit-position
+  "c" #'kmacro-menu-edit-counter
+  "e" #'kmacro-menu-edit-keys
+  "f" #'kmacro-menu-edit-format
+  "RET" #'kmacro-menu-edit-column
+
+  "C" #'kmacro-menu-do-copy
+  "D" #'kmacro-menu-do-delete
+  "m" #'kmacro-menu-mark
+
+  "d" #'kmacro-menu-flag-for-deletion
+  "x" #'kmacro-menu-do-flagged-delete
+
+  "u" #'kmacro-menu-unmark
+  "U" #'kmacro-menu-unmark-all
+  "DEL"#'kmacro-menu-unmark-backward
+
+  "<remap> <transpose-lines>" #'kmacro-menu-transpose)
+
+(define-derived-mode kmacro-menu-mode tabulated-list-mode
+  "Keyboard Macro Menu"
+  "Major mode for listing and editing keyboard macros."
+  (make-local-variable 'kmacro-menu--marks)
+  (make-local-variable 'kmacro-menu--deletion-flags)
+  (setq-local tabulated-list-format
+              [("Position" 8 nil)
+               ("Counter"  8 nil :right-align t :pad-right 2)
+               ("Format"  8 nil)
+               ("Formatted" 10 nil)
+               ("Keys" 1 nil)])
+  (setq-local tabulated-list-padding 2)
+  (add-hook 'tabulated-list-revert-hook #'kmacro-menu--refresh nil t)
+  (tabulated-list-init-header)
+  (unless (kmacro-ring-empty-p)
+    (kmacro-menu--refresh)
+    (tabulated-list-print)))
+
+;;;###autoload
+(defalias 'kmacro-menu #'list-keyboard-macros)
+;;;###autoload
+(defun list-keyboard-macros ()
+  "List the keyboard macros."
+  (interactive)
+  (let ((buf (get-buffer-create "*Keyboard Macro List*")))
+    (with-current-buffer buf
+      (kmacro-menu-mode))
+    (pop-to-buffer buf)))
+
+;;;; Utility functions and mode data
+
+(defvar kmacro-menu--deletion-flags nil
+  "Alist of entries flagged for deletion.")
+
+(defvar kmacro-menu--marks nil
+  "Alist of entries marked for copying and duplication.")
+
+(defun kmacro-menu--id-kmacro (entry-id)
+  "Return the keyboard macro that is part of the ENTRY-ID."
+  (car entry-id))
+
+(defun kmacro-menu--id-position (entry-id)
+  "Return the ordinal position that is part of the ENTRY-ID."
+  (cdr entry-id))
+
+(defun kmacro-menu--kmacros ()
+  "Return the list of the existing keyboard macros or nil, if none are defined."
+  (when last-kbd-macro
+    (cons (kmacro-ring-head)
+          kmacro-ring)))
+
+(defun kmacro-menu--refresh ()
+  "Reset the list of keyboard macros."
+  (setq-local tabulated-list-entries
+              (seq-map-indexed (lambda (km idx)
+                                 (let ((cnt (kmacro--counter km))
+                                       (fmt (kmacro--format km)))
+                                   `((,km . ,idx)
+                                     [,(format "%d" idx)
+                                      ,(format "%d" cnt)
+                                      ,fmt
+                                      ,(format fmt cnt)
+                                      ,(format-kbd-macro (kmacro--keys km))])))
+                               (kmacro-menu--kmacros))
+              kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (tabulated-list-clear-all-tags))
+
+(defun kmacro-menu--map-ids (function)
+  "Apply FUNCTION to the current table's entry IDs in order.
+
+Return a list of the output of FUNCTION."
+  (mapcar function
+          (mapcar #'car
+                  (seq-sort-by #'cdar #'< tabulated-list-entries))))
+
+(defun kmacro-menu--replace-all (kmacros)
+  "Replace the existing keyboard macros with those in KMACROS.
+
+The first element in the list overwrites the values of `last-kbd-macro',
+`kmacro-counter', and `kmacro-counter-format'.  The remaining elements
+become the value of `kmacro-ring'.
+
+KMACROS is a list of `kmacro' objects."
+  (if (null kmacros)
+      (setq last-kbd-macro nil
+            kmacro-counter-format kmacro-default-counter-format
+            kmacro-counter 0
+            kmacro-ring nil)
+    (if (not (seq-every-p #'kmacro-p kmacros))
+        (error "All elements must satisfy `kmacro-p'")
+      (kmacro-split-ring-element (car kmacros))
+      (setq kmacro-ring (cdr kmacros)))))
+
+(defun kmacro-menu--replace-at (kmacro n)
+  "Replace the keyboard macro at position N with KMACRO.
+
+This function replaces all of the existing keyboard macros via
+`kmacro-menu--replace-all'.  Except for the macro at position N, which will
+be KMACRO, the replacement macros are the existing macros identified in
+the table."
+  (kmacro-menu--replace-all
+   (kmacro-menu--map-ids (lambda (id)
+                           (if (= n (kmacro-menu--id-position id))
+                               kmacro
+                             (kmacro-menu--id-kmacro id))))))
+
+(defun kmacro-menu--query-revert ()
+  "If the table differs from the existing macros, ask whether to revert table."
+  (when (and (not (equal (kmacro-menu--kmacros)
+                         (kmacro-menu--map-ids #'kmacro-menu--id-kmacro)))
+             (yes-or-no-p "Table does not match existing keyboard macros.  Stop and revert table?"))
+    (tabulated-list-revert)
+    (signal 'quit nil)))
+
+(defun kmacro-menu--assert-row (&optional id)
+  "Signal an error if point is not on a table row.
+
+ID is the tabulated list id of the supposed entry at point."
+  (unless (or id (tabulated-list-get-id))
+    (user-error "Not on a table row")))
+
+(defun kmacro-menu--propertize-keys (face)
+  "Redisplay the macro keys on the current line with FACE."
+  (tabulated-list-set-col 4 (propertize (aref (tabulated-list-get-entry) 4)
+                                        'face face)))
+
+(defun kmacro-menu--do-region (function)
+  "Run FUNCTION on macros in the region or on the current line at the line start.
+
+If there is an active region, for each line in the region, move to the
+beginning of the line and apply FUNCTION to the table entry ID of the
+line.  If there is no region, apply FUNCTION only to the table entry ID
+of the current line.
+
+When there is no active region, advance to the beginning of the next
+line after applying FUNCTION."
+  (if (use-region-p)
+      (save-excursion
+        (let* ((reg-beg (region-beginning))
+               (reg-end (region-end))
+               (line-beg (progn
+                           (goto-char reg-beg)
+                           (pos-bol)))
+               (line-end (progn
+                           (goto-char reg-end)
+                           (if (bolp)
+                               reg-end
+                             (pos-bol 2)))))
+          (goto-char line-beg)
+          (let ((id))
+            (while (and (< (point) line-end)
+                        (setq id (tabulated-list-get-id)))
+              (kmacro-menu--assert-row id)
+              (funcall function id)
+              (forward-line 1)))))
+    (let ((id (tabulated-list-get-id)))
+      (kmacro-menu--assert-row id)
+      (goto-char (pos-bol))
+      (funcall function id)
+      (forward-line 1))))
+
+(defun kmacro-menu--marks-exist-p ()
+  "Return non-nil if markers exist for any table entries."
+  (let ((tag (gensym)))
+    (catch tag
+      (kmacro-menu--map-ids (lambda (id)
+                              (when (alist-get (kmacro-menu--id-position id)
+                                               kmacro-menu--marks)
+                                (throw tag t))))
+      nil)))
+
+;;;; Commands for Marks and Flags
+
+(defun kmacro-menu-mark ()
+  "Mark macros in the region or on the current line.
+
+If there's an active region, mark macros in the region; otherwise mark
+the macro on the current line.  If marking the current line, move point
+to the next line when done.
+
+Marked macros can be operated on by `kmacro-menu-do-copy' and
+`kmacro-menu-do-delete'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--marks)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-marked)
+     (tabulated-list-put-tag #("*" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-flag-for-deletion ()
+  "Flag macros in the region or on the current line.
+
+If there's an active region, flag macros in the region; otherwise flag
+the macro on the current line.  If there is no active region, move point
+to the next line when done.
+
+Flagged macros can be deleted via `kmacro-menu-do-flagged-delete'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (setf (alist-get (kmacro-menu--id-position id)
+                      kmacro-menu--deletion-flags)
+           t)
+     (kmacro-menu--propertize-keys 'kmacro-menu-flagged)
+     (tabulated-list-put-tag #("D" 0 1 (face kmacro-menu-mark))))))
+
+(defun kmacro-menu-unmark ()
+  "Unmark and unflag macros in the region or on the current line.
+
+If there's an active region, unmark and unflag macros in the region;
+otherwise unmark and unflag the macro on the current line.  If there is
+no active region, move point to the next line when done."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (kmacro-menu--do-region
+   (lambda (id)
+     (let ((pos (kmacro-menu--id-position id)))
+       (setf (alist-get pos kmacro-menu--deletion-flags) nil
+             (alist-get pos kmacro-menu--marks) nil))
+     (kmacro-menu--propertize-keys 'default)
+     (tabulated-list-put-tag " "))))
+
+(defun kmacro-menu-unmark-backward ()
+  "Like `kmacro-menu-unmark', but move backwards instead of forwards."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((go-back (not (use-region-p))))
+    (kmacro-menu-unmark)
+    (when go-back
+      (forward-line -2))))
+
+(defun kmacro-menu-unmark-all ()
+  "Unmark and unflag all listed keyboard macros."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (setq-local kmacro-menu--deletion-flags nil
+              kmacro-menu--marks nil)
+  (save-excursion
+    (goto-char (point-min))
+    (while (tabulated-list-get-id)
+      (kmacro-menu--propertize-keys 'default)
+      (forward-line 1))
+    (tabulated-list-clear-all-tags)))
+
+;;;; Commands that Modify the Ring
+
+(defun kmacro-menu-do-flagged-delete ()
+  "Delete keyboard macros flagged via `kmacro-menu-flag-for-deletion'."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((res)
+        (num-deletes 0))
+    (kmacro-menu--map-ids (lambda (id)
+                            (if (alist-get (kmacro-menu--id-position id)
+                                           kmacro-menu--deletion-flags)
+                                (setq num-deletes (1+ num-deletes))
+                              (push (kmacro-menu--id-kmacro id) res))))
+    (when (yes-or-no-p (if (= 1 num-deletes)
+                           "Delete 1 flagged keyboard macro?"
+                         (format "Delete %d flagged keyboard macros?"
+                                 num-deletes)))
+      (kmacro-menu--replace-all
+       (nreverse res))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-do-copy ()
+  "Duplicate macros in the region, those with markers, or the one at point.
+
+Macros are duplicated at their current position in the macro ring.
+
+If there's an active region, duplicate macros in the region; otherwise
+duplicate the marked macros or, if there are no marks, the macro on the
+current line."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let* ((region-exists (use-region-p))
+         (mark-exists (kmacro-menu--marks-exist-p))
+         (id-alist (if (or region-exists
+                           (not mark-exists))
+                       (let ((region-alist))
+                         (kmacro-menu--do-region
+                          (lambda (id)
+                            (push (cons (kmacro-menu--id-position id)
+                                        t)
+                                  region-alist)))
+                         region-alist)
+                     kmacro-menu--marks))
+         (num-duplicates 0))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (let ((pos (kmacro-menu--id-position id))
+                                    (km (kmacro-menu--id-kmacro id)))
+                                (push km res)
+                                (when (alist-get pos id-alist)
+                                  (push km res)
+                                  (setq num-duplicates (1+ num-duplicates))))))
+      ;; Confirm the action if we operated on marks or the region, but
+      ;; don't confirm if operating on a single line without a region.
+      (when (if (or mark-exists region-exists)
+                (yes-or-no-p (if (= 1 num-duplicates)
+                                 "Copy (duplicate) 1 keyboard macro?"
+                               (format "Copy (duplicate) %d keyboard macros?"
+                                       num-duplicates)))
+              t)
+        (kmacro-menu--replace-all (nreverse res))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-do-delete ()
+  "Delete macros in the region, those with markers, or the one at point.
+
+If there's an active region, delete macros in the region; otherwise
+delete the marked macros or, if there are no marks, the macro on the
+current line."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--query-revert)
+  (let ((num-deletes 0)
+        (id-alist (if (or (use-region-p)
+                          (not (kmacro-menu--marks-exist-p)))
+                      (let ((region-alist))
+                        (kmacro-menu--do-region
+                         (lambda (id)
+                           (push (cons (kmacro-menu--id-position id)
+                                       t)
+                                 region-alist)))
+                        region-alist)
+                    kmacro-menu--marks)))
+    (let ((res))
+      (kmacro-menu--map-ids (lambda (id)
+                              (if (alist-get (kmacro-menu--id-position id)
+                                             id-alist)
+                                  (setq num-deletes (1+ num-deletes))
+                                (push (kmacro-menu--id-kmacro id) res))))
+      (when (yes-or-no-p (if (= 1 num-deletes)
+                             "Delete 1 keyboard macro?"
+                           (format "Delete %d keyboard macros?"
+                                   num-deletes)))
+        (kmacro-menu--replace-all (nreverse res))
+        (tabulated-list-revert)))))
+
+;;;; Commands that Modify a Keyboard Macro
+
+(defun kmacro-menu-edit-position ()
+  "Move the keyboard macro at point to a new position.
+
+See the Info node `(emacs) Keyboard Macro Ring' for more information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((new-position (min (length tabulated-list-entries)
+                              (max 0
+                                   (read-number "New position: " 0))))
+           (old-km (kmacro-menu--id-kmacro id))
+           (old-pos (kmacro-menu--id-position id)))
+      (unless (= old-pos new-position)
+        (kmacro-menu--replace-all
+         (let ((res)
+               (true-new-pos (if (> new-position old-pos)
+                                 (1+ new-position)
+                               new-position)))
+           (kmacro-menu--map-ids (lambda (this-id)
+                                   (let ((this-km (kmacro-menu--id-kmacro this-id))
+                                         (this-pos (kmacro-menu--id-position this-id)))
+                                     (unless (= old-pos this-pos)
+                                       (when (= this-pos true-new-pos)
+                                         (push old-km res))
+                                       (push this-km res)))))
+           (when (>= true-new-pos
+                     (length tabulated-list-entries))
+             (push old-km res))
+           (nreverse res)))
+        (tabulated-list-revert)))))
+
+(defun kmacro-menu-transpose ()
+  "Swap the keyboard macro at point with the one above, then move to the next line.
+
+If point is on the first line (position number 0), then swap the macros
+at position numbers 0 and 1, then move point to the third line.
+
+Note that this is the earlier position in the ring, not the sorted
+table."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((old-pos (kmacro-menu--id-position id))
+           (first-line (= 0 old-pos))
+           (end-lines-forward (if first-line
+                                  2
+                                (+ 3 old-pos))))
+      ;; When transposing the first two macros, we don't use
+      ;; `kmacro-swap-ring' here because it is possible for the user to
+      ;; choose to not refresh the table when it is out of date.
+      (kmacro-menu--replace-all
+       (let ((res))
+         (kmacro-menu--map-ids
+          (if first-line
+              (let ((old-km (kmacro-menu--id-kmacro id)))
+                (lambda (this-id)
+                  (let ((this-pos (kmacro-menu--id-position this-id)))
+                    (unless (= 0 this-pos)
+                      (push (kmacro-menu--id-kmacro this-id) res)
+                      (when (= 1 this-pos)
+                        (push old-km res))))))
+            (let ((new-pos (1- old-pos)))
+              (lambda (this-id)
+                (let ((this-pos (kmacro-menu--id-position this-id)))
+                  (unless (= old-pos this-pos)
+                    (when (= new-pos this-pos)
+                      (push (kmacro-menu--id-kmacro id) res))
+                    (push (kmacro-menu--id-kmacro this-id) res)))))))
+         (nreverse res)))
+      (tabulated-list-revert)
+      (goto-char (point-min))
+      (forward-line end-lines-forward))))
+
+(defun kmacro-menu-edit-format ()
+  "Edit the counter format of the keyboard macro at point.
+
+Valid counter formats are those for integers accepted by the function
+`format'.
+
+See the command `kmacro-set-format' and the Info node `(emacs) Keyboard
+Macro Counter' for more information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--replace-at
+       (kmacro (kmacro--keys km)
+               (kmacro--counter km)
+               (read-string "New format: " nil nil
+                            (list kmacro-default-counter-format
+                                  (kmacro--format km))))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-counter ()
+  "Edit the counter of the keyboard macro at point.
+
+See Info node `(emacs) Keyboard Macro Counter' for more
+information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let ((km (kmacro-menu--id-kmacro id)))
+      (kmacro-menu--replace-at
+       (kmacro (kmacro--keys km)
+               (read-number "New counter: "
+                            (list 0
+                                  (kmacro--counter
+                                   (kmacro-menu--id-kmacro id))))
+               (kmacro--format km))
+       (kmacro-menu--id-position id))
+      (tabulated-list-revert))))
+
+(defun kmacro-menu-edit-keys ()
+  "Edit the keys of the keyboard macro at point via `edmacro-mode'.
+
+See Info node `(emacs) Edit Keyboard Macro' for more
+information."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (let ((id (tabulated-list-get-id)))
+    (kmacro-menu--assert-row id)
+    (kmacro-menu--query-revert)
+    (let* ((old-km (kmacro-menu--id-kmacro id)))
+      (edit-kbd-macro (kmacro--keys old-km)
+                      nil
+                      nil
+                      (lambda (mac)
+                        (kmacro-menu--replace-at
+                         (kmacro mac
+                                 (kmacro--counter old-km)
+                                 (kmacro--format old-km))
+                         (kmacro-menu--id-position id))
+                        (tabulated-list-revert))))))
+
+(defun kmacro-menu-edit-column ()
+  "Edit the value in the current column of the keyboard macro at point."
+  (declare (modes kmacro-menu-mode))
+  (interactive nil kmacro-menu-mode)
+  (kmacro-menu--assert-row)
+  (kmacro-menu--query-revert)
+  (pcase (get-text-property (point) 'tabulated-list-column-name)
+    ('nil        (let ((pos (point)))
+                   ;; If we didn't find a column, try moving forwards or
+                   ;; backwards to the nearest column.
+                   (tabulated-list-next-column 1)
+                   (when (= pos (point))
+                     (tabulated-list-previous-column 1))
+                   (if (null (get-text-property (point) 'tabulated-list-column-name))
+                       (user-error "No column at point")
+                     (kmacro-menu-edit-column))))
+    ("Position"  (call-interactively #'kmacro-menu-edit-position))
+    ("Counter"   (call-interactively #'kmacro-menu-edit-counter))
+    ("Format"    (call-interactively #'kmacro-menu-edit-format))
+    ("Formatted" (user-error "Formatted counter is not editable"))
+    ("Keys"      (call-interactively #'kmacro-menu-edit-keys))))
+
 (provide 'kmacro)
 
 ;;; kmacro.el ends here
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#70208: [PATCH] Add command `list-keyboard-macros`
  2024-04-13 19:24       ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-04-14  9:41         ` Eli Zaretskii
  0 siblings, 0 replies; 6+ messages in thread
From: Eli Zaretskii @ 2024-04-14  9:41 UTC (permalink / raw)
  To: Okamsn; +Cc: 70208-done

> Date: Sat, 13 Apr 2024 19:24:45 +0000
> From: Okamsn <okamsn@protonmail.com>
> Cc: 70208@debbugs.gnu.org
> 
> Please see the attached.

Thanks, installed, and closing the bug.





^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2024-04-14  9:41 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-04-05  3:34 bug#70208: [PATCH] Add command `list-keyboard-macros` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-05  6:16 ` Eli Zaretskii
2024-04-06 23:26   ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-07  7:55     ` Eli Zaretskii
2024-04-13 19:24       ` Okamsn via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-14  9:41         ` Eli Zaretskii

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).