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

From: [email-masked] (GNU bug Tracking System)
Date: Sun, 14 Apr 2024 09:42:02 +0000

Your bug report

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

which was filed against the emacs package, has been closed.

The explanation is attached below, along with your original report.
If you require more details, please reply to [email-masked].

-- 
70208: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=70208
GNU Bug Tracking System
Contact [email-masked] with problems

Subject: Re: bug#70208: [PATCH] Add command `list-keyboard-macros`
From: Eli Zaretskii <[email-masked]>
To: Okamsn <[email-masked]>
Cc: [email-masked]
Date: Sun, 14 Apr 2024 12:41:30 +0300
List-Id: <debbugs-submit.debbugs.gnu.org>

> Date: Sat, 13 Apr 2024 19:24:45 +0000
> From: Okamsn <[email-masked]>
> Cc: [email-masked]
> 
> Please see the attached.

Thanks, installed, and closing the bug.

Subject: [PATCH] Add command `list-keyboard-macros`
From: Okamsn <[email-masked]>
To: [email-masked]
Date: Fri, 05 Apr 2024 03:34:59 +0000
List-Id: <debbugs-submit.debbugs.gnu.org>

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.

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