meow-edit / meow

Yet another modal editing on Emacs / 猫态编辑
GNU General Public License v3.0
1.07k stars 128 forks source link

RFC: Statistically ergonomic keybinds for all keyboard layouts #506

Open udayvir-singh opened 8 months ago

udayvir-singh commented 8 months ago

After 6 months of research, testing and multiple revisions, here is the most ergonomic layout I could create for meow:

image

Some notable features:

Source code

;; -------------------- ;;
;;         UTILS        ;;
;; -------------------- ;;
(defun meow-word ()
  "Expand word/symbol under cursor."
  (interactive)
  (if (and (use-region-p)
           (equal (car (region-bounds))
                  (bounds-of-thing-at-point 'word)))
      (meow-mark-symbol 1)
    (progn
      (when (and (mark)
                 (equal (car (region-bounds))
                        (bounds-of-thing-at-point 'symbol)))
        (meow-pop-selection))
      (meow-mark-word 1))))

(defun meow-kill-line ()
  "Kill till the end of line."
  (interactive)
  (let ((select-enable-clipboard meow-use-clipboard))
    (kill-line)))

(defun meow-change-line ()
  "Kill till end of line and switch to INSERT state."
  (interactive)
  (meow--cancel-selection)
  (meow-end-of-thing
   (car (rassoc 'line meow-char-thing-table)))
  (meow-change))

(defun meow-save-clipboard ()
  "Copy in clipboard."
  (interactive)
  (let ((meow-use-clipboard t))
    (meow-save)))

(defvar meow--trim-yank nil)

(defun meow-insert-for-yank-advice (orig-fn str)
  "Advice for `insert-for-yank' function to correctly insert lines."
  (when meow--trim-yank
    (set 'str (string-trim-right str "\n")))
  (if (and (not (eq (point) (+ 1 (line-end-position 0))))
           (string-match-p "^.+\n$" str))
      (save-excursion
        (beginning-of-line)
        (funcall orig-fn str))
    (funcall orig-fn str)))

(defun meow-yank-dwim ()
  "Smart yank."
  (interactive)
  (advice-add 'insert-for-yank :around 'meow-insert-for-yank-advice)
  (if (use-region-p)
      (let ((meow--trim-yank t))
        (delete-region (region-beginning) (region-end))
        (meow-yank))
    (meow-yank))
  (advice-remove 'insert-for-yank 'meow-insert-for-yank-advice))

(defun meow-yank-pop-dwim ()
  "Smart yank pop."
  (interactive)
  (advice-add 'insert-for-yank :around 'meow-insert-for-yank-advice)
  (if (use-region-p)
      (let ((meow--trim-yank t))
        (delete-region (region-beginning) (region-end))
        (meow-yank-pop))
    (meow-yank-pop))
  (advice-remove 'insert-for-yank 'meow-insert-for-yank-advice))

(defun meow-smart-reverse ()
  "Reverse selection or begin negative argument."
  (interactive)
  (if (use-region-p)
      (meow-reverse)
    (negative-argument nil)))

(defun meow-kmacro ()
  "Toggle recording of kmacro."
  (interactive)
  (if defining-kbd-macro
      (meow-end-kmacro)
    (meow-start-kmacro)))

(defun meow-eldoc ()
  "Toggle the display of the eldoc window."
  (interactive)
  (if (get-buffer-window eldoc--doc-buffer)
      (delete-window (get-buffer-window eldoc--doc-buffer))
    (eldoc-doc-buffer t)))

;; -------------------- ;;
;;       VARIABLES      ;;
;; -------------------- ;;
(meow-thing-register 'angle
                     '(pair ("<") (">"))
                     '(pair ("<") (">")))

(setq meow-char-thing-table
      '((?f . round)
        (?d . square)
        (?s . curly)
        (?a . angle)
        (?r . string)
        (?v . paragraph)
        (?c . line)
        (?x . buffer)))

(setq meow-selection-command-fallback
      '((meow-change . meow-change-char)
        (meow-kill . meow-delete)
        (meow-cancel-selection . keyboard-quit)
        (meow-pop-selection . meow-pop-grab)
        (meow-beacon-change . meow-beacon-change-char)))

;; -------------------- ;;
;;       MAPPINGS       ;;
;; -------------------- ;;
(meow-define-keys 'normal
 ; expansion
 '("0" . meow-expand-0)
 '("1" . meow-expand-1)
 '("2" . meow-expand-2)
 '("3" . meow-expand-3)
 '("4" . meow-expand-4)
 '("5" . meow-expand-5)
 '("6" . meow-expand-6)
 '("7" . meow-expand-7)
 '("8" . meow-expand-8)
 '("9" . meow-expand-9)
 '("'" . meow-smart-reverse)

 ; movement
 '("i" . meow-prev)
 '("k" . meow-next)
 '("j" . meow-left)
 '("l" . meow-right)

 '("y" . meow-search)
 '("/" . meow-visit)

 ; expansion
 '("I" . meow-prev-expand)
 '("K" . meow-next-expand)
 '("J" . meow-left-expand)
 '("L" . meow-right-expand)

 '("u" . meow-back-word)
 '("U" . meow-back-symbol)
 '("o" . meow-next-word)
 '("O" . meow-next-symbol)

 '("a" . meow-word)
 '("s" . meow-line)
 '("w" . meow-block)
 '("q" . meow-join)
 '("g" . meow-grab)
 '("G" . meow-pop-grab)
 '("p" . meow-cancel-selection)
 '("P" . meow-pop-selection)

 '("x" . meow-till)
 '("X" . meow-find)

 '("," . meow-beginning-of-thing)
 '("." . meow-end-of-thing)
 '("<" . meow-inner-of-thing)
 '(">" . meow-bounds-of-thing)

 '("[" . indent-rigidly-left-to-tab-stop)
 '("]" . indent-rigidly-right-to-tab-stop)

 ; editing
 '("b" . open-line)
 '("B" . split-line)
 '("d" . meow-kill)
 '("D" . meow-kill-line)
 '("f" . meow-change)
 '("F" . meow-change-line)
 '("c" . meow-save)
 '("C" . meow-save-clipboard)
 '("v" . meow-yank-dwim)
 '("V" . meow-yank-pop-dwim)

 '("e" . meow-insert)
 '("E" . meow-open-above)
 '("r" . meow-append)
 '("R" . meow-open-below)

 '("z" . query-replace-regexp)

 '("h" . undo-only)
 '("H" . undo-redo)

 '("m"  . meow-kmacro)
 '("M"  . kmacro-call-macro)
 '("nm" . kmacro-edit-macro) ;; 'n' prefix is for editing commands

 '("nf" . meow-comment)

 '("N"  . upcase-dwim)
 '("nn" . downcase-dwim)
 '("nN" . capitalize-dwim)

 ; eldoc
 '("t" . eldoc-box-help-at-point)
 '("T" . meow-eldoc)

 ; general
 '(";f" . save-buffer) ;; ';' prefix is for general commands
 '(";F" . save-some-buffers)

 ; ignore escape
 '("<escape>" . ignore))

This source code here is different from my actual dotfiles, as it uses some other plugins like expand-region, undo-tree, smartparens. Here are my actual config files which contain much more keybinds:

init-meow.el init-mappings.el

Further improvements:

meow-block can be replaced with er/expand-region for more selection range. Here is config for expand-region that I use:

(require 'expand-region)

(setq er/try-expand-list
      '(er/mark-inside-quotes
        er/mark-outside-quotes
        er/mark-inside-pairs
        er/mark-outside-pairs))
udayvir-singh commented 8 months ago

fixes #23

DogLooksGood commented 8 months ago

@udayvir-singh Hey, it looks cool.

I saw there are lot reasonable points, I also like your idea about protecting the pinky finger, by leaving them for some other usages or putting a non-repeatable commands. The overall feeling of the command looks great. I just found some tricky combinations when I went through it. For example, az/aaz to replace current word/symbol. I know Xah Lee did great statistics based on the command frequency. However most of time we are not using a single command, so it could be important those combinations are easy to press. I never did, but how do you think about key travel based analysis. The two keys pressed by different hands ought to have a significant lower number than the two keys pressed by a single finger with crossing the rows. e.g. az, wc are bad and df, jk, ks are good. There aren't much bad combinations in this layout, but there aren't much good combinations neither based on this approach. Let me know what do you think?

udayvir-singh commented 8 months ago

@DogLooksGood

  1. z is query-replace-rexep, az does not replace current word/symbol. To replace current symbol/word do av or aav which is very ergonomic, it also work with sv to replace the current line.

  2. wc is also very ergonomic in practice. On a normal keyboard your wrists are angled at 60-70deg, hence wc is very fast to do as your wc is inline with your first 3 fingers due to the stagger.

udayvir-singh commented 8 months ago

The keyboard layout also takes into account normal english word usage, that was insert mode is e as it is close to the most used consonants r and t. The same is true for f which is bellow e and r.

udayvir-singh commented 8 months ago

There aren't much bad combinations in this layout, but there aren't much good combinations neither based on this approach.

The layout tries to minimize bad combinations, which there are very few. The first iterations tried to maximize good combinations but that ended up being way worse, here is a example:

  1. Would you have a layout that has 50% good combinations, 30% ok combinations and 20% bad combinations.

  2. Or would you have a layout that has 30% good combinations 70% ok combinations and no bad combinations.

I choose the later, as over a long coding session those bad combinations add up and start hurting your fingers really bad.

udayvir-singh commented 8 months ago

I prefer a smother experience over a fast and jerky one:

  1. The smother experience goes like this:

fast meh meh fast meh meh ...

  1. the faster goes like this:

fast fast bad fast bad fast ...

All those bad's add up really quickly.

DogLooksGood commented 8 months ago

The keyboard layout also takes into account normal english word usage, that was insert mode is e as it is close to the most used consonants r and t. The same is true for f which is bellow e and r.

This is a good point I've never thought about.

And there are some DWIM style commands I won't go with.

  1. The DWIM word/symbol command. By design, meow-word and meow-symbol push selected content into regexp-search-ring. And make it a DWIM command will add unnecessary history.
  2. After making yank/replace a DWIM command, we can't yank directly when there's a selection. The same to the DWIM commands of delete-char/kill and reverse/negative-argument. Because most meow commands create selection by default.
udayvir-singh commented 8 months ago

After making yank/replace a DWIM command, we can't yank directly when there's a selection

@DogLooksGood You could just use the global keybinds in those edge cases, like C-y or do M-x yank. The layout doesn't take edge cases like this into account as you rarely encounter them. Vim also replaces selection and nobody notices any problems in it.

udayvir-singh commented 8 months ago

And make it a DWIM command will add unnecessary history.

I just fixed it, so it doesn't pollute the history:

(defun meow-word ()
  "Expand word/symbol under cursor."
  (interactive)
  (if (and (use-region-p)
           (equal (car (region-bounds))
                  (bounds-of-thing-at-point 'word)))
      (progn
        (when (string-match-p 
               (or (car regexp-search-ring) "")
               (buffer-substring (region-beginning) (region-end)))
          (pop regexp-search-ring))
        (meow-mark-symbol 1))
    (progn
      (when (and (mark)
                 (equal (car (region-bounds))
                        (bounds-of-thing-at-point 'symbol)))
        (meow-pop-selection))
      (meow-mark-word 1))))

It can still just optionally included with the default suggestion being meow-mark-word and meow-mark-symbol on a and <shift>a.

udayvir-singh commented 8 months ago

I can understand reverse/negative-argument dwim. It was just a experimental command. Just replace it will normal meow-reverse.

udayvir-singh commented 8 months ago

However, you can't replace delete-char with kill-line as the entire keyboard layout is based around d and f. It would break the entire layout. That's why kill-line is set to D.

udayvir-singh commented 8 months ago

To summarize:

  1. All the custom commands like meow-word, meow-yank-dwim, meow-change-line, meow-smart-reverse can be excluded as they are not important and don't affect the overall efficiency.

  2. However, delete-char can only be used with meow-kill, otherwise the entire layout goes to shit. I can understand why you would want to separate them as delete-char doesn't work with kill ring and doesn't work properly in beacon mode but the layout was based on real world command statistics collected by myself and xahlee.

The goal of the keyboard layout was to be good 90% of the time and be consistent in quality, after 8 iterations this is the final layout I came up with which fully exploits the weakness of a traditional stagger layout, Thus was never meant to be perfect in ideological sense but was made with real world data in mind.

DogLooksGood commented 8 months ago

In this case

foo bb|ar  baz
    ^    ^

How you delete the b in the beginning of the word bbar, or the extra whitespace after this word. It is supposed to be back-word delete-char and forward-word delete-char

udayvir-singh commented 8 months ago

@DogLooksGood upd

udayvir-singh commented 8 months ago

Now that I think about it, meow-delete could just be mapped to t and d could retain the functionality of kill-line. I think you are right about keeping it the same. It wouldn't have any effect on the ergonomics. Now I remember the reason why I mapped d to meow-delete, It was because f change char but d deletes line, and I wanted to keep the behavior consistent.

You could also swap t and x and get vim like binding with t for till and x for delete-char.

udayvir-singh commented 8 months ago

Here is the final layout that I suggest:

image

;; -------------------- ;;
;;         UTILS        ;;
;; -------------------- ;;
(defun meow-change-line ()
  "Kill till end of line and switch to INSERT state."
  (interactive)
  (let ((beg (point)))
    (end-of-line)
    (delete-region beg (point))
    (meow-insert-mode)))

(defun meow-save-clipboard ()
  "Copy in clipboard."
  (interactive)
  (let ((meow-use-clipboard t))
    (meow-save)))

(defun meow-kmacro ()
  "Toggle recording of kmacro."
  (interactive)
  (if defining-kbd-macro
      (meow-end-kmacro)
    (meow-start-kmacro)))

;; -------------------- ;;
;;       VARIABLES      ;;
;; -------------------- ;;
(meow-thing-register 'angle
                     '(pair ("<") (">"))
                     '(pair ("<") (">")))

(setq meow-char-thing-table
      '((?f . round)
        (?d . square)
        (?s . curly)
        (?a . angle)
        (?r . string)
        (?v . paragraph)
        (?c . line)
        (?x . buffer)))

;; -------------------- ;;
;;       MAPPINGS       ;;
;; -------------------- ;;
(meow-define-keys 'normal
 ; expansion
 '("0" . meow-expand-0)
 '("1" . meow-expand-1)
 '("2" . meow-expand-2)
 '("3" . meow-expand-3)
 '("4" . meow-expand-4)
 '("5" . meow-expand-5)
 '("6" . meow-expand-6)
 '("7" . meow-expand-7)
 '("8" . meow-expand-8)
 '("9" . meow-expand-9)

 ; movement
 '("i" . meow-prev)
 '("k" . meow-next)
 '("j" . meow-left)
 '("l" . meow-right)

 '("y" . meow-search)
 '("/" . meow-visit)

 ; expansion
 '("I" . meow-prev-expand)
 '("K" . meow-next-expand)
 '("J" . meow-left-expand)
 '("L" . meow-right-expand)

 '("u" . meow-back-word)
 '("U" . meow-back-symbol)
 '("o" . meow-next-word)
 '("O" . meow-next-symbol)

 '("a" . meow-mark-word)
 '("A" . meow-mark-symbol)
 '("s" . meow-line)
 '("w" . meow-block)
 '("q" . meow-join)
 '("g" . meow-grab)
 '("G" . meow-pop-grab)
 '("p" . meow-cancel-selection)
 '("P" . meow-pop-selection)

 '("t" . meow-till)
 '("T" . meow-find)

 '("," . meow-beginning-of-thing)
 '("." . meow-end-of-thing)
 '("<" . meow-inner-of-thing)
 '(">" . meow-bounds-of-thing)

 '("[" . indent-rigidly-left-to-tab-stop)
 '("]" . indent-rigidly-right-to-tab-stop)

 ; editing
 '("b" . open-line)
 '("B" . split-line)
 '("d" . meow-kill)
 '("f" . meow-change)
 '("F" . meow-change-line)
 '("x" . meow-delete)
 '("c" . meow-save)
 '("C" . meow-save-clipboard)
 '("v" . meow-yank-dwim)
 '("V" . meow-yank-pop-dwim)

 '("e" . meow-insert)
 '("E" . meow-open-above)
 '("r" . meow-append)
 '("R" . meow-open-below)

 '("z" . query-replace-regexp)

 '("h" . undo-only)
 '("H" . undo-redo)

 '("m" . meow-kmacro)
 '("M" . kmacro-call-macro)

 ; prefix n
 '("nm" . kmacro-edit-macro)
 '("nf" . meow-comment)

 ; prefix ;
 '(";f" . save-buffer)
 '(";F" . save-some-buffers)

 ; ignore escape
 '("<escape>" . ignore))
DogLooksGood commented 8 months ago

I think we should add a for thing angle into core.

I think we don't really have a way to define the key for arbitrary keyboard layout with one command. So I will add it as an option in README for an ergonomic layout for QWERTY.

udayvir-singh commented 8 months ago

@DogLooksGood What do you think about adding meow-change-line and meow-kmacro into core, these commands are bug free, especially considering the utility of meow-kmacro, there is no reason to not add it.

udayvir-singh commented 8 months ago

@DogLooksGood After some more modifications, I found a extra improvement by replacing z with find.

image

;; -------------------- ;;
;;         UTILS        ;;
;; -------------------- ;;
(defun meow-change-line ()
  "Kill till end of line and switch to INSERT state."
  (interactive)
  (let ((beg (point)))
    (end-of-line)
    (delete-region beg (point))
    (meow-insert-mode)))

(defun meow-kmacro ()
  "Toggle recording of kmacro."
  (interactive)
  (if defining-kbd-macro
      (meow-end-kmacro)
    (meow-start-kmacro)))

;; -------------------- ;;
;;       VARIABLES      ;;
;; -------------------- ;;
(meow-thing-register 'angle
                     '(pair ("<") (">"))
                     '(pair ("<") (">")))

(setq meow-char-thing-table
      '((?f . round)
        (?d . square)
        (?s . curly)
        (?a . angle)
        (?r . string)
        (?v . paragraph)
        (?c . line)
        (?x . buffer)))

;; -------------------- ;;
;;       MAPPINGS       ;;
;; -------------------- ;;
(meow-define-keys 'normal
 ; expansion
 '("0" . meow-expand-0)
 '("1" . meow-expand-1)
 '("2" . meow-expand-2)
 '("3" . meow-expand-3)
 '("4" . meow-expand-4)
 '("5" . meow-expand-5)
 '("6" . meow-expand-6)
 '("7" . meow-expand-7)
 '("8" . meow-expand-8)
 '("9" . meow-expand-9)
 '("'" . meow-reverse)

 ; movement
 '("i" . meow-prev)
 '("k" . meow-next)
 '("j" . meow-left)
 '("l" . meow-right)

 '("y" . meow-search)
 '("/" . meow-visit)

 ; expansion
 '("I" . meow-prev-expand)
 '("K" . meow-next-expand)
 '("J" . meow-left-expand)
 '("L" . meow-right-expand)

 '("u" . meow-back-word)
 '("U" . meow-back-symbol)
 '("o" . meow-next-word)
 '("O" . meow-next-symbol)

 '("a" . meow-mark-word)
 '("A" . meow-mark-symbol)
 '("s" . meow-line)
 '("S" . meow-goto-line)
 '("w" . meow-block)
 '("q" . meow-join)
 '("g" . meow-grab)
 '("G" . meow-pop-grab)
 '("p" . meow-cancel-selection)
 '("P" . meow-pop-selection)

 '("x" . meow-till)
 '("z" . meow-find)

 '("," . meow-beginning-of-thing)
 '("." . meow-end-of-thing)
 '("<" . meow-inner-of-thing)
 '(">" . meow-bounds-of-thing)

 '("[" . indent-rigidly-left-to-tab-stop)
 '("]" . indent-rigidly-right-to-tab-stop)

 ; editing
 '("b" . open-line)
 '("B" . split-line)
 '("d" . meow-kill)
 '("f" . meow-change)
 '("F" . meow-change-line)
 '("t" . meow-delete)
 '("c" . meow-save)
 '("v" . meow-yank)
 '("V" . meow-yank-pop)

 '("e" . meow-insert)
 '("E" . meow-open-above)
 '("r" . meow-append)
 '("R" . meow-open-below)

 '("h" . undo-only)
 '("H" . undo-redo)

 '("m" . meow-kmacro)
 '("M" . kmacro-call-macro)

 ; prefix n
 '("nm" . kmacro-edit-macro)
 '("nf" . meow-comment)
 '("nd" . meow-swap-grab)
 '("ns" . meow-sync-grab)
 ;; .. etc

 ; prefix ;
 '(";f" . save-buffer)
 '(";F" . save-some-buffers)
 '(";d" . meow-query-replace-regexp)
 ;; .. etc

 ; ignore escape
 '("<escape>" . ignore))
DogLooksGood commented 7 months ago

Hey, @udayvir-singh sorry for the late reply.

I'm considering some minor modifications when merging into the doc.

I personally don't think meow-kmacro is something I'd like to add. Meow has no interest to simulate Vim behaviors. The emacs builtins have one command to start kmacro or insert counter, and another command to end or call kmacro. I see there's no disadvantage compares to Vim's solution, eventually we need at least 2 commands in total.

As for meow-change-line, it doesn't follow Meow's design. Since we completely separated the objects and behaviors, so it should be meow-end-of-line and meow-change instead of single command. we have existing thing command that can be used to select to the end of line, so we probably don't want a end-of-line command.

How do you think?

udayvir-singh commented 7 months ago

@DogLooksGood No problem, the layout is meant as a guideline anyways for people to create there own layout that fits there editing style, I just wanted to add better starting keybinds for beginners.

udayvir-singh commented 7 months ago

Also I wanted to ask, what do you think about moving all the *.org docs in a docs/ folder instead of in the project root like this:

.
├── docs
│   ├── keybindings
│   │   ├── COLEMAK.ORG
│   │   ├── DVORAK.ORG
│   │   ├── DVP.ORG
│   │   └── QWERTY.ORG
│   ├── CHANGELOG.MD
│   ├── COMMANDS.ORG
│   ├── CUSTOMIZATIONS.ORG
│   ├── EXPLANATION.ORG
│   ├── FAQ.ORG
│   ├── GET_STARTED.ORG
│   ├── TUTORIAL.ORG
│   └── VIM_COMPARISON.ORG
├── LICENSE
├── meow-beacon.el
├── meow-cheatsheet.el
├── meow-cheatsheet-layout.el
├── meow-command.el
├── meow-core.el
├── meow.el
├── meow-esc.el
├── meow-face.el
├── meow-helpers.el
├── meow-keymap.el
├── meow-keypad.el
├── meow-shims.el
├── meow.svg
├── meow-thing.el
├── meow-tutor.el
├── meow-util.el
├── meow-var.el
├── meow-visual.el
└── README.org

Or without capitalization:

.
├── docs
│   ├── keybindings
│   │   ├── colemak.org
│   │   ├── dvorak.org
│   │   ├── dvp.org
│   │   └── qwerty.org
│   ├── changelog.md
│   ├── commands.org
│   ├── customizations.org
│   ├── explanation.org
│   ├── faq.org
│   ├── get_started.org
│   ├── tutorial.org
│   └── vim_comparison.org
├── LICENSE
├── meow-beacon.el
├── meow-cheatsheet.el
├── meow-cheatsheet-layout.el
├── meow-command.el
├── meow-core.el
├── meow.el
├── meow-esc.el
├── meow-face.el
├── meow-helpers.el
├── meow-keymap.el
├── meow-keypad.el
├── meow-shims.el
├── meow.svg
├── meow-thing.el
├── meow-tutor.el
├── meow-util.el
├── meow-var.el
├── meow-visual.el
└── README.org
udayvir-singh commented 7 months ago

@DogLooksGood Here is the final final revision of the layout with no extra bells-and-whistles:

image

;; -------------------- ;;
;;      THING TABLE     ;;
;; -------------------- ;;
(meow-thing-register 'angle
                     '(pair ("<") (">"))
                     '(pair ("<") (">")))

(setq meow-char-thing-table
      '((?f . round)
        (?d . square)
        (?s . curly)
        (?a . angle)
        (?r . string)
        (?v . paragraph)
        (?c . line)
        (?x . buffer)))

;; -------------------- ;;
;;       MAPPINGS       ;;
;; -------------------- ;;
(meow-define-keys 'normal
 ; expansion
 '("0" . meow-expand-0)
 '("1" . meow-expand-1)
 '("2" . meow-expand-2)
 '("3" . meow-expand-3)
 '("4" . meow-expand-4)
 '("5" . meow-expand-5)
 '("6" . meow-expand-6)
 '("7" . meow-expand-7)
 '("8" . meow-expand-8)
 '("9" . meow-expand-9)
 '("'" . meow-reverse)

 ; movement
 '("i" . meow-prev)
 '("k" . meow-next)
 '("j" . meow-left)
 '("l" . meow-right)

 '("y" . meow-search)
 '("/" . meow-visit)

 ; expansion
 '("I" . meow-prev-expand)
 '("K" . meow-next-expand)
 '("J" . meow-left-expand)
 '("L" . meow-right-expand)

 '("u" . meow-back-word)
 '("U" . meow-back-symbol)
 '("o" . meow-next-word)
 '("O" . meow-next-symbol)

 '("a" . meow-mark-word)
 '("A" . meow-mark-symbol)
 '("s" . meow-line)
 '("S" . meow-goto-line)
 '("w" . meow-block)
 '("q" . meow-join)
 '("g" . meow-grab)
 '("G" . meow-pop-grab)
 '("m" . meow-swap-grab)
 '("M" . meow-sync-grab)
 '("p" . meow-cancel-selection)
 '("P" . meow-pop-selection)

 '("x" . meow-till)
 '("z" . meow-find)

 '("," . meow-beginning-of-thing)
 '("." . meow-end-of-thing)
 '("<" . meow-inner-of-thing)
 '(">" . meow-bounds-of-thing)

 ; editing
 '("d" . meow-kill)
 '("f" . meow-change)
 '("t" . meow-delete)
 '("c" . meow-save)
 '("v" . meow-yank)
 '("V" . meow-yank-pop)

 '("e" . meow-insert)
 '("E" . meow-open-above)
 '("r" . meow-append)
 '("R" . meow-open-below)

 '("h" . undo-only)
 '("H" . undo-redo)

 '("b" . open-line)
 '("B" . split-line)

 '("[" . indent-rigidly-left-to-tab-stop)
 '("]" . indent-rigidly-right-to-tab-stop)

 ; prefix n
 '("nf" . meow-comment)
 '("nt" . meow-start-kmacro-or-insert-counter)
 '("nr" . meow-start-kmacro)
 '("ne" . meow-end-or-call-kmacro)
 ;; ...etc

 ; prefix ;
 '(";f" . save-buffer)
 '(";F" . save-some-buffers)
 '(";d" . meow-query-replace-regexp)
 ;; ... etc

 ; ignore escape
 '("<escape>" . ignore))