Project: M-x-ify Evil Macros

I often make keyboard macros in Emacs's evil-mode, and I want to save them by name so that they persist when I close and re-open Emacs.

This has been incredibly useful for me. For example, here's a macro I use every day to write new entries into the logbooks on this site:

(defalias 'macro/insert-project-log-header-with-timestamp
   (kmacro "o # # # <escape> : r ! SPC d a t e <return> k J o <return>"))

For a time, I typed every key in that kmacro string 3 or 4 times a day to insert well-formatted Markdown headings with a timestamp, like this:

### <the output of vi Ex-command `r! date` here>
<new line>
<my cursor ends up here, in insert mode!>

Dependencies

This only works with kmacro which is a recent edition to Emacs with no backwards compatibility. If (kmacro-p nil) throws an error for you instead of returning nil, then unfortunately the strategy I describe below won't work. Though you may have luck starting from the same StackExchange message I did and working out from there!

I also use Doom Emacs but I hope that nothing I'm writing here is dependent on Doom. Please let me know if anything doesn't work for you!

How to use

Step 1. Add this interactive function definition to your configuration:

(defun insert-kbd-macro-in-register-with-name (register name)
  "insert macro from register. prompt for register key
if no argument passed. you may need to revise inserted s-expression."
  (interactive "cthe register key:\nsname: ")
  (let* ((macro (cdr (assoc register register-alist)))
         (macro-string (with-temp-buffer
                         ;; Reed modified this to use a unique (but not safe) symbol
                         ;; instead of 'last-kbd-macro because that uses defalias instead
                         (fset (intern name) macro)
                         (insert-kbd-macro (intern name))
                         (buffer-string))))
    (insert macro-string)))

Step 2. Record an evil macro. I don't know if this works with normal Emacs macros, but I assume it does. I only use evil keyboard macros, but I believe they are implemented as Emacs macros under the hood? I have never had a reason to look deeply into it.

Step 3. Put your pointer where you'd like to output the macro definition. I use a separate +macros file which I source in my configuration, however you could put it anywhere, even directly above or below the defun in Step 1.

Step 4. M-x insert-kbd-macro-in-register-with-name RET <register name> RET <your-name>. The register name will be the key you recorded the macro into, so if you recorded the macro starting with q a then a will be the register name.

Step 5. A wild defalias has appeared! You can evaluate this form (C-x C-e) immediately or restart Emacs to rerun your new configuration.

Step 6. M-x <your-name> RET should run your macro!

Logbook

Fri Dec 22 08:29:43 AM PST 2023

I know the title I began with, M-x-ify Evil Macros, was jargon filled and exclusionary, but I couldn't resist the weirdness once it popped into my head. Also, I wasn't going to explain Emacs and vi macros from the ground up in this post, so I felt it was okay if this wasn't the most broadly applicable title.

Before this project, the only way I knew to add something to the M-x menu in Emacs was to write Elisp and use the (interactive) declaration.

My first commit private for this work appears to be March 19, 2023, though it may be older as git history isn't a great source.

I started with this StackExchange message and modified the code there to use a unique symbol (but not safely unique a la gensym) instead of last-kbd-macro. I don't remember exactly why.

That code looks like this:

(defun insert-kbd-macro-in-register (register)
  "insert macro from register. prompt for register key
if no argument passed. you may need to revise inserted s-expression."
  (interactive "cthe register key:")
  (let* ((macro (cdr (assoc register register-alist)))
         (macro-string (with-temp-buffer
                         (fset 'tmp-my-macro macro)
                         (insert-kbd-macro 'tmp-my-macro)
                         (buffer-string))))
    (insert macro-string)))

Then I adapted that code to give a custom name for the defalias output. The code is very similar. If I were better at Elisp, I may have abstracted the two into a common root instead of copying and pasting. Though I almost never use the first version, so I might just get rid of it. Here's the named version:

(defun insert-kbd-macro-in-register-with-name (register name)
  "insert macro from register. prompt for register key
if no argument passed. you may need to revise inserted s-expression."
  (interactive "cthe register key:\nsname: ")
  (let* ((macro (cdr (assoc register register-alist)))
         (macro-string (with-temp-buffer
                         ;; Reed modified this to use a unique (but not safe) symbol
                         ;; instead of 'last-kbd-macro because that uses defalias instead
                         (fset (intern name) macro)
                         (insert-kbd-macro (intern name))
                         (buffer-string))))
    (insert macro-string)))

That's currently the exact same as at the top of this file in the "How to use" section.

I wanted to do a quick write up to share with some fellow Emacsers!