Subject: bug#70208: [PATCH] Add command `list-keyboard-macros`

From: Okamsn <[email-masked]>
Date: Sat, 13 Apr 2024 19:24:45 +0000

Eli Zaretskii wrote:
>> Date: Sat, 06 Apr 2024 23:26:15 +0000
>> From: Okamsn <[email-masked]>
>> Cc: [email-masked]
>>
>> +@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.

From bfdc27b5684eebbd1b432f6062ca81b89385a9cb Mon Sep 17 00:00:00 2001
From: Earl Hyatt <[email-masked]>
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