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

From: Okamsn <[email-masked]>
Date: Sat, 06 Apr 2024 23:26:15 +0000

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