2024-06-30 Sun 00:00

Switching from evil mode to meow

About Meow

Evil mode is a great vi emulator and was helpful for transitioning to Emacs from Vim, but I've been wanting a more minimal modal solution. I've found that there are some things in Emacs that I much prefer to the evil solution (such as Emacs macros). I have found meow and I think it's change from the vi verb-object paradigm to object-verb paradigm to be interesting. But coming from years of vim/evil muscle memory, I want to make the transition to meow as easy as possible. My goal in this configuration is to make many of the key bindings familiar to a vi-user.

Digit arguments

The default meow keybindings have the numeric keys expand the region in normal mode. However, if there is no region selected then the keys do nothing. So I want the number keys to act as a C-u prefix argument if there is no region selected, and if there is a region then they act as meow normally expects. Let's define a macro so we don't have to create a function for all 10 digits:

(defmacro m-meow-numeric-macro (num)
  `(defun ,(intern (format "m/meow-numeric-%s" num)) ()
     (interactive)
     (if (region-active-p)
         (call-interactively #',(intern (format "meow-expand-%s" num)))
       (call-interactively #'meow-digit-argument))))

Now, I can bind numbers 0-9 with ease:

(meow-normal-define-key
 `("0" . ,(m-meow-numeric-macro 0))
 `("9" . ,(m-meow-numeric-macro 9))
 `("8" . ,(m-meow-numeric-macro 8))
 `("7" . ,(m-meow-numeric-macro 7))
 `("6" . ,(m-meow-numeric-macro 6))
 `("5" . ,(m-meow-numeric-macro 5))
 `("4" . ,(m-meow-numeric-macro 4))
 `("3" . ,(m-meow-numeric-macro 3))
 `("2" . ,(m-meow-numeric-macro 2))
 `("1" . ,(m-meow-numeric-macro 1)))

Making the letter commands more like evil

I want to make the letter commands similar to evil to make it easier to learn meow. There are a few things I did to try to make this behavior more intuitive for me as an evil user:

  • A appends to end of line while I inserts at beginning of line
  • C changes to end of line
  • F and T are the reversed version of f and t

There are a few notable differences between this and evil:

  • C-f and C-b are forward-char and backward-char, instead of scrolling the window. This is due to how meow uses keyboard macros internally and relies on C-f and C-b being bound to those commands. Scrolling can still be done with C-v and M-v.
  • Most of the commands following the g prefix are not implemented.
(meow-normal-define-key
 '("-" . negative-argument)
 '(";" . meow-reverse)
 '("," . meow-beginning-of-thing)
 '("." . meow-end-of-thing)
 '("=" . meow-indent)
 '("/" . meow-visit)
 ;;'("/" . isearch-forward) ;; alternatively use emacs built in isearch
 ;;'("?" . isearch-backward)
 `("a" . ,(defun m-meow-append ()
            (interactive)
            (if (region-active-p)
                (call-interactively #'meow-append)
              (if (eolp)
                  ;; when at eol, we don't want to go onto the next line
                  (call-interactively #'meow-insert)
                (call-interactively #'meow-append)))))
 `("A" . ,(defun m-meow-append-eol ()
            (interactive)
            (end-of-line)
            (meow-insert)))
 '("b" . meow-back-word)
 '("B" . meow-back-symbol)
 '("c" . meow-change)
 `("C" . ,(defun m-meow-change-eol ()
            (interactive)
            (if (region-active-p)
                (call-interactively #'meow-change)
              (call-interactively #'kill-line)
              (call-interactively #'meow-insert))))
 '("d" . meow-kill)
 '("e" . meow-next-word)
 '("E" . meow-next-symbol)
 '("f" . meow-find)
 `("F" . ,(defun m-meow-reverse-find ()
            (interactive)
            (let ((current-prefix-arg (or current-prefix-arg -1)))
              (call-interactively #'meow-find))))
 '("g g" . meow-goto-line)
 '("G" . meow-grab)
 '("h" . meow-left)
 '("H" . meow-left-expand)
 '("i" . meow-insert)
 `("I" . ,(defun m-meow-insert-bol ()
            (interactive)
            (meow-join nil)
            (meow-append)))
 '("j" . meow-next)
 '("J" . meow-next-expand)
 '("k" . meow-prev)
 '("K" . meow-prev-expand)
 '("l" . meow-right)
 '("L" . meow-right-expand)
 '("m" . meow-join)
 `("n" . ,(defun m-meow-reverse-search ()
            (interactive)
            (let ((current-prefix-arg (if (meow--direction-forward-p) nil 1)))
              (call-interactively #'meow-search))))
 `("N" . ,(defun m-meow-reverse-search ()
            (interactive)
            (let ((current-prefix-arg (if (meow--direction-backward-p) nil -1)))
              (call-interactively #'meow-search))))
 '("o" . meow-open-below)
 '("O" . meow-open-above)
 `("p" . ,(defun m-meow-paste-after ()
            (interactive)
            ;; if the current kill ends with \n we assume it should be pasted as a line
            (if (string= (substring-no-properties (current-kill 0 t) -1) "\n")
                (progn
                  (next-line)
                  (beginning-of-line)
                  (call-interactively #'meow-yank))
              (forward-char 1)
              (call-interactively #'meow-yank))))
 `("P" . ,(defun m-meow-paste-before ()
            (interactive)
            ;; if the current kill ends with \n we assume it should be pasted as a line
            (if (string= (substring-no-properties (current-kill 0 t) -1) "\n")
                (progn
                  (beginning-of-line)
                  (call-interactively #'meow-yank))
              (call-interactively #'meow-yank))))
 '("M-p" . meow-yank-pop) ; M-p instead of C-p like in evil
 '("q" . meow-pop-selection)
 '("Q" . meow-swap-grab)
 '("r" . meow-replace)
 '("R" . undo-redo)
 '("s" . meow-inner-of-thing)
 '("S" . meow-bounds-of-thing)
 '("t" . meow-till)
 `("T" . ,(defun m-meow-reverse-till ()
            (interactive)
            (let ((current-prefix-arg (or current-prefix-arg -1)))
              (call-interactively #'meow-till))))
 '("u" . meow-undo)
 '("U" . meow-undo-in-selection)
 '("v" . meow-line)
 '("w" . meow-mark-word)
 '("W" . meow-mark-symbol)
 '("x" . meow-delete)
 '("X" . meow-backward-delete)
 '("y" . meow-save)
 '("Y" . meow-sync-grab)
 '("<escape>" . meow-cancel-selection))

z-prefixed commands

I also frequently use the commands prefixed with z to scroll the window to center my cursor, or put my cursor on top or bottom. By looking at the implementation of recenter-top-bottom, I was able to add these bindings myself.

(meow-normal-define-key
 '("z z" . recenter-top-bottom)
 `("z t" . ,(defun m-meow-zt ()
              (interactive)
              (let ((this-scroll-margin
                     (min (max 0 scroll-margin)
                          (truncate (/ (window-body-height) 4.0)))))
                (recenter this-scroll-margin t))))
 `("z b" . ,(defun m-meow-zb ()
              (interactive)
              (let ((this-scroll-margin
                     (min (max 0 scroll-margin)
                          (truncate (/ (window-body-height) 4.0)))))
                (recenter (- -1 this-scroll-margin) t)))))

Evil matchit

We can also use evil matchit to get the same behavior as % in evil. While the package is called evil-matchit, it doesn't need evil to function.

(use-package evil-matchit
  :commands
  'evilmi-jump-items-native
  :init
  (meow-normal-define-key
   '("%" . evilmi-jump-items-native)))

Trying it out

To try it out, you can find the full configuration here.

View the post changelog and blog source code on GitLab.

Emacs 29.4 (Org mode 9.6.15)

Copyright (C) 2024 Marcus Quincy (blog@marcusquincy.org). Unless otherwise stated, all code snippets are licensed under the MIT License and post content is licensed under Creative Commons Attribution license. Thanks for reading!