Skip to content

Latest commit

 

History

793 Commits

Folders and files

Repository files navigation

DONKEY

Table of Contents

Start here

Everyday use

Making it yours

When something is not right

Reference

What DONKEY is

DONKEY gives Emacs two states. In INSERT state Emacs is exactly the Emacs you know. In NORMAL state the letter keys run editing commands instead of typing letters — j moves down, d deletes, y copies. C-g takes you from INSERT to NORMAL; i takes you back.

It is an addition, not a replacement. Nothing is taken away to make room: every C-x and C-c sequence, M-x, C-h, isearch, the arrow keys and every Meta binding work in both states exactly as they always did. No DONKEY keymap binds a Meta key or the ESC prefix at all.

If you have used Vim or Helix, the letters will feel familiar. If you have not, the only thing you must know is the C-g / i pair; the rest you can learn a key at a time, and M-x donkey-tutor will walk you through it in a buffer you can practise in.

What changes, and what does not

INSERT state changes exactly one key: C-g returns to NORMAL state. If a package has something open under the cursor when you press it — a completion list, a snippet with fields, extra cursors — that package’s own quit runs first, so the one press closes what was open and returns you to NORMAL state.

NORMAL state differs in four ways, and only four:

  1. Letters run commands instead of typing.
  2. Digits are not counts — C-u 3 is, as in stock Emacs.
  3. RET does nothing in a buffer you are editing, and is handed back to the mode in buffers you are not.
  4. BACKSPACE and DELETE do nothing.

Everything else falls through: C-a, C-e, C-k, C-w, M-w, C-y, C-s, C-SPC, C-/, M-f, M-b, M-^, C-x …, C-c …, M-x, C-h …, TAB, C-u, arrows, Home/End, PageUp/PageDown.

A test walks every key of DONKEY’s keymaps against a plain Emacs buffer, so a fifth difference cannot appear without somebody noticing, and another walks that list above and checks each key really does resolve to what it resolves to with DONKEY off.

A key one of your packages binds works as it always did too, unless NORMAL state binds that key as well — there it is NORMAL state’s, and the package’s key is waiting for you in INSERT state. If a Key Is Not Doing What You Want is the chapter for that, and it has three ways out.

Where DONKEY steps aside completely

Some buffers are not documents. In a terminal, a shell, or a language REPL the keys belong to the program on the other end, so DONKEY stays in INSERT passthrough there and NORMAL state cannot be reached at all. The modeline says DONKEY[E] instead of DONKEY[I] so you can see it without pressing anything. You can add your own modes to that list — see Modes Where DONKEY Steps Aside.

Installing

DONKEY is one file with no dependencies beyond Emacs 29.1.

From a clone

git clone https://github.com/yardquit/donkey ~/src/donkey
;; Where the clone lives.  One place, read by both `:load-path' and
;; the `:init' block under it.
(defvar my-donkey-dir (expand-file-name "~/src/donkey"))

(use-package donkey
  :load-path my-donkey-dir

  :init
  ;; A package archive byte-compiles what it installs.  A clone does
  ;; not, and nothing in DONKEY compiles itself, so compile it here --
  ;; and again whenever donkey.el turns out to be newer than the .elc
  ;; beside it, which is what a `git pull' leaves behind.
  (let* ((src (expand-file-name "donkey.el"  my-donkey-dir))
         (elc (expand-file-name "donkey.elc" my-donkey-dir))
         (recompiled nil))
    (if (not (file-exists-p src))
        (message "DONKEY: no donkey.el under %s -- check my-donkey-dir"
                 my-donkey-dir)
      (when (file-newer-than-file-p src elc)
        (require 'bytecomp)
        (let ((start (current-time)))
          (setq recompiled
                (and (ignore-errors (byte-compile-file src))
                     (float-time (time-since start))))))
      (let ((compiled (and (file-exists-p elc)
                           (not (file-newer-than-file-p src elc)))))
        (load (if compiled elc src) nil t)
        (message "DONKEY %s loaded %s%s"
                 (donkey-version)
                 (if compiled "compiled" "from source (interpreted)")
                 (if recompiled
                     (format ", recompiled in %.2fs" recompiled)
                   "")))))

  :config
  ;; Whatever DONKEY does not bind is yours; see "Making it yours".
  (keymap-set donkey-normal-mode-map "F" #'delete-other-windows)
  (keymap-set donkey-leader-map "b" '("switch buffer" . switch-to-buffer))
  (donkey-mode 1))

What the block is doing

:load-path on its own would be enough to load DONKEY: use-package ends with (require 'donkey), and require would find donkey.el there. It would also run interpreted, and DONKEY works from post-command-hook, so that cost is paid on every keystroke.

So :init picks the file instead of leaving the choice to require. That is what the load call is for. Emacs prefers donkey.elc to donkey.el whenever both are on the path, which makes a stale .elc the copy require takes — and a later (load "donkey.el") cannot undo it. Choosing explicitly sidesteps that, and by the time use-package runs its own require, donkey is already in features and the require does nothing.

Both halves read my-donkey-dir, so there is one path to edit and no way for the two to drift apart. Pointing :load-path at one directory and the compile step at another is the mistake this arrangement exists to prevent: DONKEY would still load, from the :load-path copy and interpreted, behind a warning about the other.

The echo area says which copy you got — loaded compiled, or loaded from source (interpreted) — with the version, and the time the recompile took when there was one.

If the compile fails, DONKEY loads from source and says so. That is deliberate: a file that will not compile must not stop Emacs from starting. It also means the reason is not shown, so run M-x byte-compile-file on donkey.el when you want to see it.

One thing to know if you run the test suite over a clone: four tests mock forward-char, forward-line and delete-region, which byte-code open-codes, so delete donkey.elc before running them.

Turning it on and off

M-x donkey-mode toggles DONKEY everywhere. Turning it off clears every trace of it from every buffer and gives you stock Emacs back.

Your first five minutes

  1. M-x donkey-mode. The modeline shows DONKEY[N]: you are in NORMAL state.
  2. Press i. The modeline shows DONKEY[I]. Type a sentence — this is ordinary Emacs.
  3. Press C-g. Back to DONKEY[N].
  4. h j k l move left, down, up, right. w and b move by words.
  5. v starts a selection, j and l grow it, y copies it, p pastes.
  6. d deletes the character under the cursor, or the selection if you have one.
  7. u undoes. C-x C-s saves, as always.

That is enough to work. ? shows every key DONKEY binds, in a buffer you can read.

The tutor

M-x donkey-tutor opens a buffer you learn in by editing it, the way M-x help-with-tutorial and vimtutor do.

Sixteen lessons, ending with the five that go beyond a single cursor — wrapping a selection, the characters the keyboard does not have, editing every match at once and choosing what it holds, and cursors in a column:

  1. Moving around (including S for undoing a mis-keyed jump)
  2. Counts
  3. Typing
  4. Deleting and changing
  5. Selecting things (words, sentences, delimiters, levels)
  6. Whole lines
  7. Copy and paste
  8. Banking, which is DONKEY’s own idea
  9. Columns (rectangles)
  10. Changing your mind (undo, and the exchange key)
  11. When two selections disagree
  12. Wrapping a selection
  13. Characters your keyboard does not have (digraphs)
  14. Editing every match at once (Split mode)
  15. Choosing what a split holds (selections, banks, regexps)
  16. Cursors in a column

The keys it names are your keys: the lessons are written with substitute-command-keys escapes, so anyone who has rebound something is taught the binding they actually have rather than the default.

Every key stands out in the face Emacs’s own help gives keys, the names of the states in donkey-tutor-state, and the arrow before each line to practice on in donkey-tutor-arrow; customize either face to change its color.

Running M-x donkey-tutor again returns you to the buffer as you left it. Kill the buffer to start over — nothing is written to disk.

Moving

KeyDoes
hleft one character
jdown one line
kup one line
lright one character
wforward one word
bback one word
Wforward one balanced expression
Bback one balanced expression
g gbeginning of the buffer
Gend of the buffer
g eend of the buffer (Helix’s spelling)
g hbeginning of the line
g lend of the line
:go to a line by number
z zrecentre the screen

Long lines: j, k, J and K

On a line too long for the window, j and k move by what you can see: the next and previous screen row, keeping the column, as Emacs’s own C-n and C-p do. J and K move by whole lines instead, to the start of the next or previous line however far it wraps.

Inside a whole-line selection (V, below) J and K grow and shrink it by a line. j and k move the cursor inside it, and the next J or K squares it up to whole lines again.

Counts

Digits type digits in stock Emacs and DONKEY leaves that alone, so a count is C-u and the number, exactly as everywhere else in Emacs:

  • C-u 5 j — down five lines
  • C-u 3 d — delete three characters
  • C-u 10 w — forward ten words

C-u 0 j and a negative count do the sensible thing: zero is a bare press, and a negative count reverses the direction where that means something.

Coming back: S

S walks back through the places you have been. DONKEY keeps a ring of the last donkey-position-ring-max positions (10 by default); pressing S repeatedly steps back through them, and any other command starts the ring collecting again from where you are.

(setq donkey-position-ring-max 20)

Entering INSERT state

KeyWhere you end up
ibefore the cursor
aafter the cursor
Iat the first non-blank character of the line
Aat the end of the line
oon a new line below
Oon a new line above
creplacing the selection, or the character at point

C-g brings you back from any of them.

Editing

KeyDoes
ddelete the selection, or the character under the cursor
xthe same as d (Vim’s spelling; d is Helix’s)
Ddelete to the end of the line
cchange: delete, then INSERT state
ycopy the selection
ppaste
Ppaste as a rectangle
uundo
Uredo — including undo-tree’s own redo, where that is on
Ccomment or uncomment
g jjoin this line and the next
>indent the selection, or this line
g qfill the selection
g Qfill the paragraph
.repeat the last command
%select the whole buffer

d is deliberately not an operator the way Vim’s is: there is no d w or d d. Select first, or use D for the rest of the line.

Two smaller rules worth knowing, because they are choices rather than accidents:

  • Deleting a single character does not go on the kill ring. Single characters are typo fixes, not cuts; the kill ring keeps what you deliberately cut.
  • What a selection replaces is always recoverable — from the kill ring or from undo, and each command’s documentation says which.

Selecting

A plain selection: v

v sets the mark and starts a selection that any motion extends. v again re-anchors it where the cursor is; C-g cancels it.

Whole lines: V

V selects the line the cursor is on, and J and K grow it a line at a time (see Long lines above); C-u 3 V selects three lines at once. V again lets go.

The highlight stops at the end of the last line, but y, d and p take that line’s newline as well, so lines go and come back whole. So do Emacs’s own C-w, M-w and C-x r s, and anything else that asks Emacs for the region’s text.

C-x C-x trades the selection’s ends, as o does in Vim: J and K then move the end the cursor went to. C-SPC lets the line selection go and starts a plain one at the cursor.

A line is the line the screen shows. A folded Org or outline heading is one line, so V takes it with the body it hides and J and K step over it in one press, the way m l banks it and Emacs’s kill-whole-line kills it.

A command that reads the region’s two ends for itself sees it stop before the last newline: M-x keep-lines leaves the last line out, M-| hands its command the lines without their last newline, and C-x n n and M-x append-to-buffer leave that newline behind. For those, select with v from the start of the first line to the start of the line after — g h v j j for two lines.

Text objects: the m prefix

KeySelects
m wthe word at point
m Wthe symbol at point
m bthe word before point
m Bthe symbol before point
m sthe sentence
m Sthe sentence before
m pthe paragraph
m Pthe paragraph before
m iinside a pair — quotes, brackets, anything
m athe pair as well as what it holds
m Iinside the balanced expression
m Athe balanced expression as well
m va rectangle

m i and m a ask which pair: press the delimiter after them, and either half will do — m i ( and m i ) mean the same thing. With the cursor already on a delimiter they use that one without asking.

donkey-mark-inner=/=donkey-mark-outer support ( [ { < " ' ` ‘ ’ “ ” « » ‹ › = * ~ | \ / : + _ $ by default — 21 pairs, kept in donkey-mark-pair-delimiters. Where a character both opens and closes, the same key does both jobs. Adding your own is one line, and what m i can select, a key can wrap.

Pressing again grows the selection

Press m w twice and you get two words, not the same word twice. The same holds for m W, m s and m p, and it keeps going on each further press until the buffer runs out — so m s m s m s is another way to write C-u 3 m s.

The backward keys grow backward, and continue the same run: starting on “that” in This buffer is for text that is not saved, m w m w m b selects “text that is” — two words forward, one back.

Pressed fresh, with no run to continue, a backward key selects exactly what its forward partner would. The pair differ only in the direction further presses grow.

Objects mix freely, each press adding one object of its own kind at its own end: m w m s grows a word selection forward to the end of its sentence, and m s m b takes the sentence plus the word before it.

The mark run: M

M is the m prefix held down for you. In a mark run the bare letters w W b B s S do what their m-prefixed versions do, so M w b selects what m w m w m b selects, and the run keeps growing with every letter.

M arrives already holding a word — the word m w would have marked — because that is what nearly every run starts from. So M d takes the word under the cursor, M w is two words, M b is the word and the one before it.

One sentence covers the whole design: =M= is =m w=, and each letter after it is one more =m=-prefixed press.

Inside a run:

KeysDo
w W b B s Smark and grow, as the m versions do
m w, m p, m P …still work, and continue the run — paragraphs are reached this way
h j k l, g h, g l, g g, g e, J, Kcontinue a visible run by character, line and buffer
u, Ustep back and forward through the run’s history
.repeat
*swap which end of the selection grows
p, Pstay the paste keys — M p replaces the marked word
d, y, x, cact on the run’s selection, as everywhere else
v, Vrefused: neither can take a selection the run owns
Mend the run
anything elsea beep, and the run survives it

Counts work: M C-u 3 w is four words, exactly as m w C-u 3 m w is.

A reminder in the echo area stays for as long as the run does. It names only the keys whose subject the run changes — w moves by a word in NORMAL state and marks one here, which nobody could guess — and leaves out the keys that keep their meaning. The full list is above, in ?, and in the tutor.

A run follows you between buffers and frames rather than dying when you leave; see A mark run follows you between buffers.

Rectangles: m v

m v starts a rectangle. P pastes one back; over a V selection it replaces the lines with the block’s rows, one row to a line. Rectangles work with the wrap keys and with banking, below.

c on a rectangle empties the block on every row and puts a cursor on each, in INSERT state, so what you type appears on every row at once — the cursors of t, below. C-g comes back to the cursors and C-g again ends them. A row too short to reach the block is padded out to its column, and the block goes to the rectangle store, where P pastes it back. A block one row high is a plain change.

A rectangle costs a motion the same however tall it is: only the rows a window could show are highlighted, and a middle-click in another program is answered with the rectangle when it asks, rather than Emacs copying every row after every key. What the middle-click pastes is unchanged: the rectangle as it stands, as it was left, or what a change took out of it.

Banking lines: keeping a selection that is not contiguous

Sometimes the lines you want are not next to each other. m l banks the current selection — puts it aside, visibly highlighted — and you can go and bank another somewhere else. The banked lines together are what the next command acts on.

KeyDoes
m lbank the selection
m utake back the last bank
m Utake back the whole run of banks
m DELclear every bank

Banked lines survive C-g: they are a store you put something in on purpose, like the kill ring, and only the key that means discard discards them. A line you open or paste beside a banked line is not banked with it, and a change of major mode or a revert of the file keeps the bank.

Wrapping a selection in a pair

With a selection, press a delimiter and the selection is wrapped in it. Nineteen keys do this out of the box —

"  $  '  (  )  *  +  /  <  =  [  \  ]  _  `  {  |  }  ~

— and so do the curved and angled quotes « » ‘ ’ “ ” ‹ ›, which you can reach with a digraph even if your keyboard cannot type them. Either half of a pair does the same job: ) wraps in ( and ) just as ( does.

Press the same delimiter with the pair already around the selection and it comes off again. Which one you get is decided by what you selected: select what the pair holds and the pair comes off; select the pair as well and another goes on around it.

Characters your keyboard cannot reach are available too — see Typing characters your keyboard lacks — and rectangles can be wrapped line by line.

What a wrap key does with no selection

Nothing, in a buffer you can edit — the same as any other suppressed key. In a buffer you cannot edit, the press goes back to the major mode: the Org agenda keeps + and <, and a help buffer, an Occur list and a compilation log keep <. A wrap key is borrowed only for as long as a selection lasts, and a read-only buffer is one where nothing could be typed anyway, so both questions have the same answer.

A mode command that would type is refused even there.

Two more rules worth knowing

  • A read-only buffer, or read-only text where the pair would land, is refused before anything changes, and your selection is left standing.
  • A count is ignored: one delimiter press, one wrap.

Wrapping something that is already wrapped

Press a delimiter on a selection that already sits inside that delimiter and the three engines part company:

Your selectionDONKEYelectric-pair-modeSmartparens
word in say "word" nowsay word nowsay ""word"" nowsay ""word"" now
word in (setq x "word")(setq x word)(setq x ""word"")(setq x "\"word\"")

DONKEY takes the pair off — the press that puts a pair on is the press that removes it, and nothing is escaped in any mode. Smartparens escapes, but only inside a string, where the syntax needs it. electric-pair-mode does neither: it nests, unescaped, and leaves you with source that will not read.

So the two engines are not two ways of doing one thing. DONKEY’s own wrap also keeps apart two things a pairing package ties together: whether ' pairs while you type is your package’s business, and whether ' wraps a selection is DONKEY’s. Turn the first off — many people do in Lisp and Org, where ' and ` are quote characters — and the second goes on working in every mode.

When you do want your package’s answer, including its escaping, M-x donkey-toggle-wrap-engine is the short way: flip, press the delimiter, flip back, without leaving NORMAL state.

Pairing as you type

DONKEY can close a delimiter for you while you type, so that pressing { gives you {} with the cursor between the halves. It is off, and stays off unless you ask for it:

(donkey-pair-mode 1)

That is deliberate. INSERT state is otherwise entirely Emacs’s — C-g is the only key DONKEY holds there — and a package that started changing how you type on upgrade would be taking a liberty.

With it on, as you type:

  • an opening delimiter writes its closing half and leaves the cursor between the two;
  • the closing half, typed where that character already stands, steps over it rather than doubling it;
  • in overwrite mode (the <insert> key) nothing pairs: a delimiter replaces the character under the cursor, as it does in plain Emacs;
  • DEL between the halves of an empty pair takes both, and DEL anywhere else, or in a buffer you cannot edit such as Dired, is whatever your major mode bound it to;
  • a count types and pairs nothing: C-u 3 ( gives you three of them.

The last two are DONKEY’s own answers. Where electric-pair-mode is doing the pairing, DEL and counts are Emacs’s, and it pairs once under a count rather than not at all. Pairing: every setting says who does the job when, and what each one does differently.

Which delimiters pair

They come out of donkey-mark-pair-delimiters, the same table m i selects from and the wrap keys use. Add a pair there and you can type it as well as select and wrap it:

(setopt donkey-mark-pair-delimiters
        (cons '(?# . ?#) donkey-mark-pair-delimiters))

Now # closes itself. There is no second list to keep in step.

donkey-pair-delimiters decides how much of that table is used:

ValueMeans
safethe table, less the ordinary punctuation (default)
allevery pair in the table
a listexactly those characters
nilnothing pairs, with the mode still on
;; the default: the table, less the ordinary punctuation
(setopt donkey-pair-delimiters 'safe)

;; everything the table knows, punctuation included
(setopt donkey-pair-delimiters 'all)

;; exactly these and nothing else
(setopt donkey-pair-delimiters '(?\( ?\[ ?\{ ?\"))

;; the mode on, and nothing pairing
(setopt donkey-pair-delimiters nil)

A list replaces the set rather than adding to it, so name every character you want, not only the extra one.

Here is the whole shipped table under safe, so you can see at a glance what will close itself and what will not:

OpenersafeWhy
( [ {yesbrackets, and nothing else
"yesthe string quote
‘ “yestypographic quotes, typed on purpose
« ‹yesguillemets, typed on purpose
<noless-than, far more often
'nothe apostrophe in “don’t”
`nothe quote in Lisp
= * ~ | \ / : + _ $noordinary punctuation

Any pair you add to donkey-mark-pair-delimiters yourself is in: the list above is what SHIPS, and safe only ever removes from it. The # in the worked configuration below pairs for exactly that reason.

Under all the bottom row comes back, so a colon typed in prose gives you two of them and an apostrophe fires in the middle of “don’t”.

If you already use a pairing package

You do not have to choose, and you do not have to configure anything. DONKEY works out who should do the job.

What is onWho does the pairing
neitherDONKEY
electric-pair-modeEmacs, as it did before
smartparens-modeSmartparens

Where Emacs is doing it, DONKEY hands Emacs the delimiters, so a pair you added to the table is one Emacs pairs too. Where Smartparens is on, DONKEY stays out of the way entirely.

To see who has the job in the buffer you are in, run M-x donkey-debug-platform and read its pairing line.

That is the whole of everyday use. Pairing: every setting has the rest: holding one character back from safe, turning one on or off in a single major mode, and what each package does differently.

The Enter key

In a buffer you are editing — code, prose, org — RET does nothing in NORMAL state. That is deliberate: NORMAL state does not type, and RET is the last key that should put a newline in a buffer you are reading.

In a buffer you are not editing, RET is handed back to the mode: org-agenda-switch-to in the Org agenda, compile-goto-error in a compilation log, man-follow in a man page. In Org and Markdown it follows the link at point, toggles the checkbox, or flips the TODO — see Enter Rules to change or extend that.

(In Dired, Magit, Info and the other modes DONKEY steps aside from, RET is the mode’s for a simpler reason: DONKEY is not there at all. See Modes Where DONKEY Steps Aside.)

Search and replace

KeyRuns
r rreplace-regexp
r qquery-replace

isearch is untouched: C-s and C-r work in both states, as does everything else Emacs binds to a control or meta key.

Editing every match at once: f

f (donkey-split) asks for a regexp and holds every match in the selection. It changes nothing by itself — it selects, and then waits for you to say what to do:

KeyWhat it does
itype before every match
atype after every match
cempty every match, then type
ddelete every match
w, or a pairwrap every match, or take the pair off
C-gend it

A cursor is drawn at every match, in the shape and place of the real one, so you can see where each of them is. While you type, what you write appears at every match at once. Backspace, retype, keep going: it is ordinary INSERT state, and C-g ends it keeping what you typed, exactly as C-g does anywhere else.

There is no limit on how many matches a split holds. Past a couple of thousand, a keystroke writes the matches on screen at once and the rest as fast as they can be written, between your keystrokes; scroll and what you scroll onto is written first, and C-g waits for the last of them, as saving the file does. Once the split has ended, one u takes back everything it wrote, at every match, and U puts it back; what c, d or a wrap did is one undo step too.

A wrap leaves the split standing, so pairs nest and a verb can still follow — f ( [ a wraps twice and then types after. The other verbs end it.

What c and d remove reaches the kill ring as one kill: a single copy when the matches agree, so p pastes what was there rather than a column of copies, and every match on a line of its own when they differ.

What gets searched

SelectionSearched
nonethe current line
v, V, %the selection
m vinside the block
C-u then fwith m v, each row’s whole line
banked (m l)the banked lines, plus a live v
any, %foothe whole buffer, the bank left alone

There is no whole-buffer default on purpose: press % first and the buffer is a selection like any other, or type the regexp with % in front — f %foo RET — and the whole buffer is searched whatever is selected. Either way you reach it by choosing it. The % is not part of the search; \% or [%] at the start searches for a percent sign.

A match hidden from view — inside a folded Org subtree or outline heading, or in any other invisible text — is left out, so nothing is written where you cannot see it, and the reminder says how many were left out. Folded text is never changed by f, whatever search-invisible says. To change it, use r r (replace-regexp) or r q (query-replace); for r q, unfold first, so you can see each match you answer y or n to.

Banked lines are how the split reaches lines that are not next to each other. Each run of banked lines is searched up to the end of its last line and no further, so ^ and $ stay on the lines you banked. A v selection made after banking is searched with the bank, exactly as selected; the line the cursor happens to be on is not added. Opening the split spends the bank, as y and d do, so C-g on the split ends it without bringing the bank back; if nothing matches, the bank stays. A live m v block is searched instead of the bank and leaves it alone.

Two things follow from searching inside a block. A match to the right of the block is not found — widen with C-u if you want the rows rather than the columns. And $ and ^ match a real line end and line start, so in a block that stops mid-line they match nothing; with v or with no selection they work as you would expect.

Matches that differ, and case

The matches do not have to agree. [0-9]+ over 1, 22 and 333 holds three different texts: i and a then type at the start or the end of each, so what you type is added and none of them is replaced; c empties every one before you type; d deletes them.

Case is ignored the way r r (replace-regexp) ignores it: a regexp with no capital letter matches any case, so todo finds TODO, Todo and todo, and one with a capital is exact. case-fold-search and search-upper-case decide it, as they do for every search in Emacs.

The regexp

It is Emacs’s own regexp, the one r r, r q, C-M-s and M-x occur read, and five spellings differ from what other tools use:

  • \s- is whitespace; \s+ here matches nothing at all.
  • \S- is anything but whitespace; \S+ here matches every character.
  • [0-9] is a digit; \d here is the letter d.
  • \(a\|b\) is a or b; plain parentheses match themselves.
  • x\{3\} is xxx; plain braces match themselves.

M-x re-builder highlights what a regexp matches as you type it.

Two examples

Align three assignments, then end each line:

v j j g l   f  =  RET   c  :=
v j j g l   f  $  RET   a  ;

Comment out a region, by holding the start of every line:

v j j j   f  ^  RET   i  # SPC

What it will not do

Matches that touch are refused — a over aaa, or a single space over two — because text typed where two of them meet would belong to both. An empty regexp is refused too.

Where the matches differ, the split types only at their edges: moving into one ends it, since there is no one text to edit in all of them. Editing inside matches that differ is what a multiple-cursors package is for; this is not one.

A deletion just past a match is made at every match. Backspace and C-d, with a count or without, take as many characters at each, and so does typing in overwrite mode, which writes over the character after every match alike; a command that deletes as far as the text goes — M-d, C-k, M-\ — does so only where the same text stands past every match. Where it does not, the split ends and says so, and the other matches keep what they hold.

A split belongs to the buffer it was made in. Text changed where it cannot follow — from an indirect buffer sharing the text, such as one C-x 4 c or Org makes, or by a program that turns the change hooks off — ends the writing and says so. Nothing is written over that change; what was typed stays, and u then takes back each edit as Emacs recorded it rather than every match at once. A clone made while a split is live does not carry the split with it.

Cursors in a column: t

t (donkey-split-add-cursor) puts a second cursor on the line below, at the same column; each t after adds one more below the last, and a count adds that many. T (donkey-split-add-cursor-above) adds above instead, and DEL takes back the cursor added last. A line shorter than the column gets its cursor at its end, and a line hidden in a fold gets none: the line below is the one j goes to. When a motion leaves the cursors in different columns, = lines them up under the real one again; with selections it wraps them, as any delimiter does.

With lines selected, t puts a cursor on every one of them at once: V j j j j t gives five cursors, each at its line’s start, and a v selection or an m v block gives each line a cursor at point’s column. The real cursor stays on point’s line, or goes to the last line selected where the selection stops at the start of point’s line: g h v j j t is two lines and two cursors. The other cursors are drawn in the real cursor’s shape, a box or a bar as the state changes, and always in the text’s own color rather than the real cursor’s; a background given to the face donkey-split-cursor-face is used instead. Every cursor then does what the real one does:

KeyAt every cursor
h l w b W B g h g lmove
v then a motionselect from where the cursor was
Vselect the cursor’s line
m w, m s, m i, and the other mselect that object
i a I A ctype; C-g comes back to the cursors
d x, D, y, pdelete, kill, copy, paste
a pairwrap each selection, as in Normal
M, then w b s h l g ha mark run at every cursor
t T, DELadd a cursor below, above; drop one
=line the cursors up under the real one
m l, m ubank or unbank each cursor’s line
M-u M-l M-ccase of each selection, or the word
o Oopen a line at each cursor and type
> Cindent, comment each cursor’s line
.the cursors’ last command, again
u Uundo, redo — the cursors stay
fsearch each cursor’s selection or line
C-glet go of the selections, then end

With nothing selected, a types after the character under each cursor and i before it, as they do with one cursor; a cursor at its line’s end types there rather than on the next line. With a selection, a types after the whole of it.

d, D, y and p keep the cursors. What they take reaches the kill ring as one kill, one line per cursor, and p gives each cursor its own line back when the kill has as many lines as there are cursors — so m w y, a move, and p copies each cursor’s word to where that cursor went. Whole lines taken with V are the lines as V y at one cursor takes them, and V p replaces each cursor’s line the way it replaces one. A key m i would ask for is asked once.

M at the cursors is the mark run at every cursor: each selects its word, or keeps what it had selected, and the run’s keys grow and adjust every selection at once — M w d takes two words from every line, and u and U step every cursor’s selection back and forward as they step one. The run’s keys that would leave the line, j k J K g g g e, beep and keep the run. Any other key ends the run, and a key the cursors answer does its job at every cursor.

The case keys are Emacs’s own, and a key you gave upcase-region or downcase-region in Normal state works at the cursors too. An edit at the cursors is one change: u takes it back everywhere at once, and an edit that fails at one cursor is taken back at all of them, the cursors and their selections left as they were. z z and C-l move the view and keep the cursors.

Every key but the ones that type runs once at every cursor: a motion or a selection takes about ten microseconds a cursor, so ten thousand cursors answer in a tenth of a second and a hundred thousand in a second or two. Typing is written at the cursors as it is at the matches of f, however many. t refuses to make more than donkey-split-cursor-limit (10000), and says so; set it higher, or to nil for no limit, or set donkey-split-cursor-limit-ask to be asked instead. c on a rectangle taller than the limit asks for its text in the minibuffer and writes it on every row at once. f has no limit.

What a key changes at the cursors is one undo step, whatever their number: u takes it back at every cursor and U puts it back.

Three limits keep the cursors out of each other’s way. A cursor never leaves its own line: a motion that would cross stops at the line’s edge. Cursors that come to share a line — after V d, for one — become one. And a command that fails at one cursor is undone at all of them. Any key not in the table, j and k included, ends the cursors and does its ordinary job.

Undo history at scale

Every step a split takes — a wrap, a deletion, the writing between i and C-g, a key at the cursors — is one undo entry, and u takes it back everywhere at once. The entry holds a position for every place and the text that changed, so a step is about 8 bytes a place plus the text: 100 KB at ten thousand places, 0.8 MB at a hundred thousand. Emacs keeps history up to undo-limit, 160 KB by default, and always keeps the newest step whole, so at those sizes the history is a step or two. DONKEY counts its entries at their true size, where Emacs’s own count would take each for 32 bytes and let the history grow until a garbage collection threw all of it away at once.

To keep more, raise the limits. The cost is the memory the kept records take, and a garbage collection that is longer by about a quarter of a millisecond per megabyte kept — some 25 ms more with a hundred megabytes of history; typing, the split’s keys and undo itself are no slower. undo-strong-limit must stay above undo-limit, and a single step larger than undo-outer-limit (24 MB by default) is not recorded at all, and the split says so.

(setopt undo-limit (* 100 1024 1024)          ; history kept, per buffer
        undo-strong-limit (* 120 1024 1024)   ; must stay above undo-limit
        undo-outer-limit (* 200 1024 1024))   ; the largest single step recorded

Typing characters your keyboard lacks

SPC i & types one character from a two-key digraph — SPC i & a ' gives á, SPC i & - > gives →. With a selection, the digraph wraps it.

For more than one, turn the input method on with SPC i . and type & followed by the two keys as you go; SPC i - turns it off again.

M-x donkey-digraph opens a chart of every digraph, sorted with the twelve most useful first.

Other languages

donkey-input-methods puts your own input methods under SPC i:

(setq donkey-input-methods
      '(("s" "swedish"   "swedish-postfix")
        ("n" "norwegian" "norwegian-postfix")
        ("p" "polish"    "polish-slash")))

That gives you SPC i s, SPC i n and SPC i p. DONKEY’s own entries under SPC i sit on symbols — &, . and - — precisely so that every letter stays free for a language of yours.

Emacs’s own way in, C-x RET C-\ and M-x set-input-method, works exactly as it always did.

Suppressed keys

These do nothing in NORMAL state, on purpose:

KeyWhy
BACKSPACE, DELETE (all four key names)silenced — pressed from habit, and a beep on every stray press is noise nobody can act on
, - ;refused with “is undefined”: a punctuation key pressed in NORMAL state is a question, and this answers it

They are bound to something harmless rather than left unbound, and that is the point: an unbound key falls through to the major mode, and some major modes type with punctuation. The rest of the punctuation is not on this list because it wraps — see Wrapping a selection.

Every other printable key is refused as well

NORMAL state answers for every key from ! to ~. One DONKEY does not bind, and has not lent to a wrap pair, is refused with “is undefined” rather than reaching the major mode.

That is what lets a key mean one thing everywhere. Thirty-two printable keys used to fall through, and what they did depended on the mode: the suppression is a remap of self-insert-command and the commands like it, which catches a mode that types with a NAMED command and cannot catch one that does not. idlwave-mode binds & to an anonymous command that inserts an ampersand, and & typed into a buffer you were navigating, whatever that list said.

The two rows above are listed separately because those keys are bound in DONKEY’s own keymap. The rest are answered from underneath it, which is why ? does not list them: they are not keys DONKEY chose to do something with, they are what is left.

Making it yours

Everything below goes in your init file. DONKEY has to be loaded first, so wrap anything that touches its keymaps or options in with-eval-after-load, or put it in a use-package :config block.

One key of your own

(with-eval-after-load 'donkey
  (keymap-set donkey-normal-mode-map "F" #'delete-other-windows))

That key is yours in every NORMAL-state buffer.

Several keys: the SPC leader

Rather than spending single letters, put your keys under SPC. Write each as ("name" . command) and the name shows up in ? and in which-key, with nothing depending on which-key being installed:

(with-eval-after-load 'donkey
  (keymap-set donkey-leader-map "b" '("switch buffer"  . switch-to-buffer))
  (keymap-set donkey-leader-map "f" '("find file"      . find-file))
  (keymap-set donkey-leader-map "g" '("magit"          . magit-status))
  ;; a prefix of your own, with its own leaves
  (keymap-set donkey-leader-map "w h" '("window left"  . windmove-left))
  (keymap-set donkey-leader-map "w l" '("window right" . windmove-right)))

SPC b then runs switch-to-buffer, and ? shows the row:

SPC b          switch-to-buffer  switch buffer

Every letter under SPC is yours. DONKEY keeps only symbols there — SPC i for input methods — so nothing of yours can collide with something added later.

Naming a prefix of your own

A prefix with no name shows as +prefix in the which-key popup, which tells a reader nothing. Name it the same way a leaf is named — by binding its keymap as ("name" . keymap).

Either define the prefix whole and name it as you bind it:

(defvar my-open-map
  (define-keymap
    "g" '("magit" . magit-status)
    "m" '("ement" . ement-connect)))

(with-eval-after-load 'donkey
  (keymap-set donkey-leader-map "o" (cons "open/apps" my-open-map)))

— or let keymap-set make the prefix for you, and put a name on it afterwards:

(with-eval-after-load 'donkey
  (keymap-set donkey-leader-map "t l" #'display-line-numbers-mode)
  (keymap-set donkey-leader-map "t w" #'whitespace-mode)
  (keymap-set donkey-leader-map "t"
              (cons "toggle" (keymap-lookup donkey-leader-map "t"))))

Either way the popup then shows +open/apps and +toggle where it would otherwise show +prefix. Keys added under the prefix afterwards keep the name, and naming it a second time renames it. The name travels in the binding rather than in another package’s table, which is why none of this needs which-key installed: DONKEY’s own SPC i is named this way and shows as +input-method.

To name a prefix you did not bind — one belonging to another package — which-key’s own table is the way, and it is the only way:

(which-key-add-key-based-replacements
  "SPC a" "org/agenda"
  "SPC d" "denote")

Name a prefix both ways and the table wins: it rewrites the name the keymap offered, so the one in the binding is the one you stop seeing. Nothing warns you, which is reason enough to pick one place and keep every prefix there. DONKEY’s own SPC i is no exception —

(with-eval-after-load 'donkey
  (which-key-add-key-based-replacements "SPC i" "input/information"))

— renames it in the popup, and the keys under it go on doing what they did.

? does not name a prefix

Whichever way you set it, a prefix name is for the popup. The ? chart lists every key under a prefix, each with its own name, and gives the prefix itself no row.

That is a deliberate silence rather than a gap. ? reads the keymap, so the only name it could ever show is one bound into it — and a chart that named the prefixes you bound while saying nothing about the ones you named in which-key’s table would be telling half the story, in a way that reads as a bug rather than a rule. The popup has all of them.

Taking a key away

A key in NORMAL state is DONKEY’s, one you bound, or nothing: whatever the major mode puts on a key, NORMAL state never lets it through, so a key pressed by mistake does nothing you did not ask for. Take one of DONKEY’s keys away and it does nothing:

(with-eval-after-load 'donkey
  (keymap-unset donkey-normal-mode-map "x"))   ; x does nothing now

With or without keymap-unset’s REMOVE argument, the key is refused rather than handed to the buffer. That holds for every printable key, SPC, RET and the delete keys, the keys NORMAL state answers for. The rest behave as they always did: C-x, C-c, M-x, the arrows and TAB reach Emacs and the major mode, which is how TAB folds a headline in Org.

To give a key a command, bind the command, and it is a key you chose. For one mode only, remap DONKEY’s command in that mode’s own map; If a Key Is Not Doing What You Want shows how.

If a Key Is Not Doing What You Want

NORMAL state gives the letters to DONKEY. Most of the time that is the point, and now and then it takes a key you wanted for something else.

First: it still works in INSERT state. Press i and the key is the package’s again, exactly as it was.

If you want it in NORMAL state too, there are three ways, in order of how much they change.

1. Give one key back, in one mode

Say you use org-noter, and in its document window you would rather i wrote a note than started INSERT state:

(with-eval-after-load 'org-noter
  (define-key org-noter-doc-mode-map
              [remap donkey-insert-here] #'org-noter-insert-note))

Now i writes a note in an org-noter document buffer and is INSERT state everywhere else. You do not have to remember which buffer you are in — the mode does.

The pattern is always the same: take the DONKEY command you want out of the way, wrap it in [remap ...], bind that in the mode’s own map. M-x donkey-check-bindings lists it afterwards as a key that mode remaps, which is its way of saying “somebody meant this”.

Three things it cannot do:

  • It moves a command, not a key. d and x are both donkey-delete, so remapping that moves both; every wrap key is donkey-wrap-region, so remapping that moves all nineteen.
  • It cannot give a key back to a prefix — a key that opens a further menu of keys, as AUCTeX’s ` does.
  • It needs a command to name. “Whatever that mode would have done” is not something you can write here.

For those, exclude the mode.

2. Turn NORMAL state off for that kind of buffer

See Modes Where DONKEY Steps Aside, just below. One line, and the mode has all of its keys back.

3. Take the key for yourself

If it is your key rather than the mode’s, bind it in donkey-normal-mode-map as above — that wins everywhere.

Modes DONKEY Supports Rather Than Takes Over

Dired has its own alphabet. So does Ibuffer, Magit, Info. Those keys are the whole of what the mode is, and a modal editor that lands on top of them has taken away the thing you opened the buffer to use.

The old answer was to exclude the mode, which gave the keys back but took hjkl with them. A support mode is the middle: DONKEY keeps h, j, k and l, and the mode keeps everything else.

DONKEY[S]     h j k l   are DONKEY's
              a b c …   are the mode's, all of them

Every buffer a program made for you is a support mode. DONKEY does not need to be told which: a mode derived from special-mode — Emacs’s own word for it — or a read-only buffer, and not a mode you write in. That last clause matters: a source file opened read-only is still a file you read as a writer, and DONKEY keeps its keys there.

What a section adds is what h and l mean where the mode has somewhere to go. Forty-one sections ship; twenty-six of them give h or l a meaning:

Modehl
dired-modeup a directoryopen the file or enter the directory
ibuffer-mode—visit the buffer
speedbar-modeup a directoryopen the file or enter the directory
Man-modeprevious sectionnext section
woman-mode—follow
apropos-mode—follow
image-modeprevious filenext file
doc-view-modeprevious pagenext page
tar-mode—extract the entry
archive-mode—extract the entry
Custom-modeup to the parent group—
occur-mode—go to the occurrence
compilation-mode—go to the error (covers grep-mode)
emacs-lisp-compilation-mode—go to the error
package-menu-mode—describe the package
Buffer-menu-mode—visit the buffer
org-agenda-modea week earliera week later
bookmark-bmenu-mode—jump to the bookmark
vc-dir-mode—visit the file
proced-mode—refine by the field at point
profiler-report-mode—expand or collapse the entry
xref--xref-buffer-mode—go to the reference
finder-mode—select
flymake-diagnostics-buffer-mode—go to the diagnostic
ebrowse-member-mode—go to the member definition
calendar-modea day earliera day later

Where no section says otherwise, h and l are backward-char and forward-char, and j and k always move by a line.

Two of them are there only because the rule misses them: Custom-mode and org-agenda-mode are neither derived from special-mode nor read-only, and a section makes a mode a support mode whatever the rule thinks.

Info-mode is not in the table. It has an alphabet of its own that hjkl cannot be fitted around without taking something a reader needs, so it is excluded instead and DONKEY holds no key there at all.

The leader comes with you

SPC is the leader in a support mode exactly as it is in NORMAL state, and it is the same keymap: whatever you hang under SPC reaches Dired, help, Man and the rest without being named a second time.

(with-eval-after-load 'donkey
  (define-key (lookup-key donkey-normal-mode-map " ") "b"
              #'switch-to-buffer))

That SPC b now works in every buffer, whichever state it is in.

What the mode had on SPC is not carried anywhere, and almost nothing is lost by that. Twenty-five of the forty-one put scroll-up-command on SPC and scroll-down-command on S-SPC; C-v and M-v are those two commands in every Emacs buffer, and S-SPC is left alone, so page up still works where it always did. image-mode and doc-view-mode are counted among them — the pair is their own rather than Emacs’s, but C-v and M-v still do it. Eight put a line motion on SPC, which is what j does. so-long-mode binds it to nothing but a space.

Three more put a command that already has another key: speedbar-mode toggles a tree node, which +, = and - also do; finder-mode put finder-select, which its own section puts on l; and calendar-mode put scroll-other-window, which is C-M-v.

That leaves four modes losing something with no obvious home: org-agenda-mode, where SPC showed the entry and scrolled it; flymake-diagnostics-buffer-mode, where it showed the diagnostic without leaving the list; and ebrowse-tree-mode and ebrowse-member-mode, where it viewed the class or the member without leaving the tree. A section takes SPC back for a mode that needs it:

(with-eval-after-load 'donkey
  (setopt donkey-support-modes
          (cons '(org-agenda-mode (?\s . org-agenda-show-and-scroll-up)
                                  (?h . org-agenda-earlier)
                                  (?l . org-agenda-later))
                donkey-support-modes)))

h and l are written out again because this section replaces the shipped one rather than adding to it, and a section answers with nothing from any later one. Leaving them out would take the agenda’s week-earlier and week-later keys away while giving SPC back.

The keys a section moves rather than takes

Where h, j, k or l displaces something the mode had, the section usually gives it the shift of the key that took it:

ModeKeyRuns
dired-modeJ Kgo to file, kill lines
ibuffer-modeJ Kjump to buffer, kill lines
speedbar-modeKkill the buffer on the line
help-modeH L Rback, forward, revert
eww-modeLback a page
dictionary-modeH Lprevious, match words
shortdoc-modeCcopy the function as kill
log-view-modeTtoggle the mark on the entry
bookmark-bmenu-modeLload a bookmark file
ert-results-modeJjump between summary and result
calendar-modeHthe holidays here

Modes you read rather than drive

Nineteen sections name the prose package instead of pairs: Man-mode, woman-mode, help-mode, apropos-mode, shortdoc-mode, dictionary-mode, messages-buffer-mode, debugger-mode, vc-annotate-mode, log-view-mode, emacs-authors-mode, tags-table-mode, url-cookie-mode, ebrowse-tree-mode, so-long-mode, ebrowse-member-mode, compilation-mode and emacs-lisp-compilation-mode, and DONKEY’s own bindings chart.

A package is a named list of sequences a section can ask for in one word, and prose is the reading half of NORMAL state: the motions and word keys, the g jumps and G, the selection and mark-run keys, y to copy, and ?. These are buffers full of text to read and copy with links to follow, and none of it types, so the mode loses nothing it was using.

One key it does cost. prose puts g g, g e, g h and g l under g, which makes g a prefix — and g is revert-buffer or recompile in most of these buffers. Where that matters the section carries it on R: R reverts in help-mode and the four reading modes, and recompiles in a compilation buffer. so-long-mode binds nothing of its own, so it carries nothing.

A compilation buffer takes the package because there is little else in it: n and p still walk the errors, l and RET still go to one, and only g moved. grep-mode and emacs-lisp-compilation-mode are the same buffer with a different producer — the elisp one is written before compilation-mode in the table, because a section answers with nothing from any later one and it has to carry its own l.

Enter is the exception a package makes: it is never taken from a mode that has its own use for it, because in a buffer a program made for you that is the action key. Where the mode has nothing on Enter, the package takes it like any other key.

In Dired that means j and k walk the listing, l opens what is under the cursor and h goes back up — and d, x, m, g, w, q and the other forty are Dired’s, exactly as they are without DONKEY.

What hjkl displace, and where it went

Four keys are taken, so four commands lose their key. Two of them were never really lost:

ModeKeyWasStill reachable
dired-modehdescribe-modeyes — C-h m
dired-modeldired-do-redisplay≈ g, which reverts
ibuffer-modeh(unbound)nothing was there
ibuffer-modelibuffer-redisplayg, which is the same command

The other four had no second key, so the section gives them one — the shifted form of the key that took them:

ModeKeyRuns
dired-modeJdired-goto-file
dired-modeKdired-do-kill-lines
ibuffer-modeJibuffer-jump-to-buffer
ibuffer-modeKibuffer-do-kill-lines

J and K were unbound in both modes, so nothing was displaced in turn. This is the section doing it, not DONKEY editing the mode’s keymap: what DONKEY does in a buffer is all in one table you can read.

And in a mode you write in, nothing is displaced at all

Not by care — by measurement. Across every prog-mode, text-mode and conf-mode derivative in a stock Emacs, and again across a configuration with the usual language and markup packages installed, exactly one binds a plain letter: org-mode, which binds all fifty-two to org-self-insert-command.

That is typing, and typing is what INSERT state is for. So there is no command to relocate in a writing mode, because none of them put one on a letter to begin with — they use C-c and the modifiers. The promise that DONKEY owns the letters where you write costs those modes nothing.

What is decided, and what is not

One thing is decided by a rule: is this a buffer a program made for me? Emacs answers it — special-mode or read-only, and not a writing mode — and DONKEY takes the answer.

Nothing else is. Which keys DONKEY holds is fixed at h j k l; what h and l run is read off a section, never worked out from the mode’s keymap; and every other key is the mode’s because no section named it. Nothing changes when a mode gains a binding next year.

donkey-support-mode-exceptions is where the rule is wrong. occur-edit-mode is the case it ships for: it derives from occur-mode, so it looks like a program’s buffer, but it is the mode e puts an Occur buffer in so the matches can be edited — a buffer you type in, where NORMAL state belongs.

Adding a section is how you change what h and l do:

(with-eval-after-load 'donkey
  (setopt donkey-support-modes
          (append '((pdf-view-mode (?h . pdf-view-previous-page)
                                   (?l . pdf-view-next-page))
                    (elfeed-search-mode (?l . elfeed-search-show-entry)))
                  donkey-support-modes)))

j and k are never written down — they are DONKEY’s in every support mode, and a section that names one is ignored. Where a section does not name h or l they are backward-char and forward-char.

A mode matches by derivation as well as by name, and the first section it matches answers, so list a specific mode before the general one. A command you do not have is passed over rather than bound.

Disagreeing with a section

The support map is an emulation map, which Emacs consults before a major mode’s own keymap. That is what makes the four motion keys reliable — and it means a binding you put in the mode’s map with :bind (:map dired-mode-map ...) is silently ignored on any key DONKEY holds. In Dired those are h, j, k, l, SPC, J and K; everything else reaches your binding as normal.

There are three levers, smallest first. Each is shown twice — in DONKEY’s block and in the affected package’s — because a mode’s settings are usually easiest to find where the rest of that mode’s configuration already lives.

A section names the key

Any key but j and k, including SPC. In DONKEY’s :config, beside the (donkey-mode 1) from Installing:

(setopt donkey-support-modes
        (cons '(dired-mode (?h . dired-up-directory)
                           (?l . dired-open-file)
                           (?y . dired-copy-filename-as-kill)
                           (?J . dired-goto-file)
                           (?K . dired-do-kill-lines))
              donkey-support-modes))

or in Dired’s own block:

(use-package dired
  :config
  (with-eval-after-load 'donkey
    (setopt donkey-support-modes
            (cons '(dired-mode (?h . dired-up-directory)
                               (?l . dired-open-file)
                               (?y . dired-copy-filename-as-kill)
                               (?J . dired-goto-file)
                               (?K . dired-do-kill-lines))
                  donkey-support-modes))))

cons, never a literal list: setopt with a list of your own replaces the table and takes the other forty sections with it. Yours is written before the shipped entry, so it is the one that answers.

And because it is the one that answers, it answers with nothing from the shipped one — so carry every pair you still want. That is why J and K are written out above: Dired’s section puts dired-goto-file and dired-do-kill-lines there, and a replacement that left them out would take away the only keys those two have.

A remap reaches what a section may not name

j and k are the floor and no section can take them — but a remap of the command they run is consulted even from the mode’s own map, which is how j reaches dired-next-line in the first place. This is the one lever that names no DONKEY symbol, so it is safe anywhere and reads most naturally in Dired’s block:

(use-package dired
  :bind (:map dired-mode-map
         ("<remap> <next-line>"       . my-down)
         ("<remap> <previous-line>"   . my-up)
         ("<remap> <dired-find-file>" . dired-open-file)))

The same thing written in DONKEY’s :config instead:

(with-eval-after-load 'dired
  (keymap-set dired-mode-map "<remap> <next-line>"       #'my-down)
  (keymap-set dired-mode-map "<remap> <previous-line>"   #'my-up)
  (keymap-set dired-mode-map "<remap> <dired-find-file>" #'dired-open-file))

Three things to know about a remap:

  • Remap the command, not the key. The key stays DONKEY’s, which is why C-h k still tells the truth about it.
  • Remap the command DONKEY’s map holds, not the one the mode already reached. j is next-line there, and Dired’s own remap turns that into dired-next-line; a remap of dired-next-line would be a second hop, and Emacs does not take it.
  • A remap follows the command everywhere it is reachable. Remapping dired-find-file moves l, RET and e together. That is usually what you want — it is how dired-open works — but it is not a way to change one key.

An exception hands the whole buffer back

Where no single key is the problem — you know Ibuffer’s own keys and want all of them — put the mode on donkey-excluded-modes. In DONKEY’s :config:

(setopt donkey-excluded-modes
        (cons 'ibuffer-mode donkey-excluded-modes))

or in Ibuffer’s own block:

(use-package ibuffer
  :config
  ;; with-eval-after-load matters here and did not in the remap
  ;; above: if Ibuffer loads before DONKEY, the option is a void
  ;; variable, use-package swallows the error, and the mode stays
  ;; =[S]= with nothing said about it.
  (with-eval-after-load 'donkey
    (setopt donkey-excluded-modes
            (cons 'ibuffer-mode donkey-excluded-modes))))

The modeline then reads DONKEY[E], and every Ibuffer key is Ibuffer’s again — j is ibuffer-jump-to-buffer, k is ibuffer-do-kill-lines.

donkey-support-mode-exceptions is the other exception and a different thing: it gives the buffer back to NORMAL state, for a program-looking buffer you type in, which is what occur-edit-mode ships for. It is the wrong lever for a list you drive. Put Ibuffer on it and DONKEY owns every letter: the modeline reads DONKEY[I], and m, t and D stop marking and deleting.

[S] and [E]

Both mean Normal state is off in this buffer. [E] is the excluded list — DONKEY holds no key at all, not even hjkl. [S] is a section — DONKEY holds four.

A mode on both lists is excluded. If you have dired-mode in your own donkey-excluded-modes, that still wins, and you will see [E] rather than [S] until you take it off.

Modes Where DONKEY Steps Aside

In a mode on donkey-excluded-modes, DONKEY is out of the way completely: every key is the mode’s, NORMAL state cannot be reached by any route, C-g means what it means in stock Emacs, and the modeline says DONKEY[E].

Thirteen modes ship on the list. Add your own like this:

(with-eval-after-load 'donkey
  (dolist (mode '(pdf-view-mode eww-mode elfeed-search-mode))
    (add-to-list 'donkey-excluded-modes mode)))

Naming a parent mode covers its children, which saves a lot of typing: magit-mode alone covers the status, log, diff, revision, refs, reflog, stash and process buffers, and the forge ones too.

wdired is the exception worth knowing: it is not built on dired-mode, so excluding dired leaves it alone — which is what you want, since a wdired buffer is one you are editing. C-x C-q takes you there in NORMAL state, like any other buffer you are editing; press i to type a new name.

Editing donkey-excluded-modes reaches buffers that are already open, both ways: add a mode and its open buffers step aside on the next command, remove one and they get NORMAL state back. INSERT state you asked for yourself is never taken away by either.

What is on the list already

Three kinds of mode, for three different reasons.

Terminals and REPLs — NORMAL state would break them

ModeWhy
comint-modeevery key goes to a subprocess. Covers its derivatives: shell-mode, ielm-mode, inferior-python-mode, sql-interactive-mode, geiser-repl-mode, inf-ruby-mode, gud-mode and the rest
term-modea full terminal — character at a time, raw mode
vterm-modethe same, faster
eshell-modeits own command line and input loop
eat-modea terminal that derives from none of the above
mistty-modethe same, wrapping a shell
slime-repl-modeCommon Lisp REPL, not a comint derivative
cider-repl-modeClojure REPL, the same
racket-repl-modeRacket REPL, the same
haskell-interactive-modeGHCi REPL, the same

The first four cover a great many modes by derivation. The six after them derive from none of the four and had to be named one at a time.

Applications — their letters are their commands

ModeWhat NORMAL state would take
magit-modecommit, log, branch, diff, push, stash. Covers every magit and forge buffer
git-rebase-modepick, reword, drop, exec, break — an interactive rebase cannot be finished without them

Nothing could have been damaged in any of them: they are read-only, so every editing key is refused with Buffer is read-only. What went missing was the mode, not your work.

A mode whose alphabet would not fit

ModeWhy
Info-moden, p, u, l, m, i, d, t, s, SPC and DEL are the whole of how you move through a manual

Info is the one mode where the support mode answer was tried and lost. Four ways of fitting h j k l around its alphabet were measured, and each cost something a reader needs: the node keys, the menu, the history, or the scroll. It is excluded instead, so DONKEY holds no key there at all — not even the four.

What you give up by excluding magit

j and k. Magit remaps next-line, so while NORMAL state is on those two keys move by magit sections — and with the mode excluded they go back to what the mode itself binds, which is magit-delete-thing. If you want the motion back, two lines:

(Dired used to be on this list for the same reason. It is a support mode now, which keeps j and k walking the listing without taking anything else, so nothing has to be put back by hand there.)

(with-eval-after-load 'magit
  (keymap-set magit-mode-map "j" #'magit-next-line)
  (keymap-set magit-mode-map "k" #'magit-previous-line))

Modes that are NOT on the list

Deliberately, because a support mode is the better answer there: help-mode, occur-mode, compilation-mode, grep-mode, Man-mode and org-agenda-mode keep their own alphabet while DONKEY keeps the four motion keys and the leader. They were NORMAL state until 1.12.0, which is what made excluding them look necessary.

Deliberately, because they are buffers you are editing: wdired-mode, git-commit-mode, diff-mode.

No longer on it, because they are support modes now: dired-mode and ibuffer-mode. They keep every key they ever had except hjkl, which is what excluding them was really for.

Not on it because they do not need to be: elfeed-search-mode, deft-mode, vundo-mode, pdf-view-mode, the docker and kubernetes UIs. A mode DONKEY has never heard of still gets an answer — see below — so excluding one is a choice rather than a repair. Add any of them as above if you want DONKEY out of the way entirely, and please open an issue so the next person does not have to. (eww-mode and ranger-mode came off this list: both are support modes now.)

What a mode with no section gets

Nothing has to be on either list for DONKEY to behave. A mode you write in — any prog-mode or text-mode derivative, so rust-mode, go-mode, markdown-mode, yaml-mode and the rest — is NORMAL state, and its letters are DONKEY’s. A buffer a program made — anything derived from special-mode, or read-only — is a support mode on the floor alone: h, j, k, l and the leader, with every other key the mode’s. kubernetes-mode, log4e-mode and tablist-mode land there today without a line of configuration.

So most packages get support for free. The sections DONKEY ships are for stock Emacs, where the maintainer can test them; a section for a third-party mode is added on request. If one you use would be better with h and l meaning something, open an issue saying which mode and which two commands.

That makes this a list rather than a rule, and it stops where the reading stopped. Why Those Two Modes Ship Excluded sets out the reasoning and how to undo any of it.

To see the list in force: M-x eval-expression donkey-excluded-modes.

The Cursor

DONKEY changes the cursor shape with the state, so you can see which one you are in without reading the modeline.

(setq donkey-cursor-normal 'box)          ; NORMAL state
(setq donkey-cursor-insert '(bar . 2))    ; INSERT state
(setq donkey-cursor-support 'box)         ; a support mode,  =[S]=
(setq donkey-cursor-excluded '(bar . 2))  ; an excluded mode, =[E]=

Any of them accepts what Emacs’s cursor-type accepts: box, bar, hbar, hollow, or a cons like (bar . 2) for a bar two pixels wide. Set one to nil to leave the cursor alone in that state.

The last two are the same shapes as the first two, on purpose. A support mode takes NORMAL’s box because the keys there are NORMAL’s — h, j, k and l move, and nothing you press becomes text. An excluded mode takes INSERT’s bar because most of them are terminals and REPLs, where what you type goes to the program on the other end.

So out of the box the cursor answers is this buffer taking my letters? rather than which of the four states you are in, and the modeline is still where you read the difference between [S], [E] and [I]. Give either its own shape if you would rather see all four.

In a terminal

In a graphical frame Emacs draws the cursor itself. In a terminal the shape has to be asked for with an escape sequence (DECSCUSR), and not every terminal understands it. DONKEY sends it only where it is known to work, and refuses terminals by name:

(setq donkey-decscusr-denied-terminals '("dumb" "linux"))

Those two are the default: dumb supports no escape sequences at all, and the Linux console changes its cursor by a different mechanism. Matching is by prefix on what tty-type reports.

When Emacs exits or is suspended, or an emacsclient -t frame closes, the terminal is sent its own cursor shape back, so the shell you return to does not keep DONKEY’s. Coming back from a suspension sends the state’s shape again.

If your terminal prints stray characters when the state changes, add its type:

(with-eval-after-load 'donkey
  (donkey-add-denylist-entry "eterm"))

M-x donkey-add-denylist-entry and M-x donkey-remove-denylist-entry do the same interactively, and remember the change.

M-x donkey-debug-platform prints what DONKEY thinks it is running in — window system, terminal type, whether the cursor escape is allowed, which clipboard tool was found — which is the first thing to paste into a bug report.

The Clipboard

y and p use the system clipboard where there is one, falling back to the kill ring. p reads the clipboard once per press and puts another program’s copy on the kill ring once, however often it is pasted, so M-y after a paste reaches the kill before it. On most systems this needs no configuration at all: DONKEY looks for wl-copy, xclip, xsel, pbcopy or Windows PowerShell and uses the first that fits.

M-x donkey-debug-platform shows which one it found.

Daemon with both graphical and terminal frames

A Wayland session with emacs --daemon and a mix of emacsclient -c and emacsclient -nw frames has a known trap that is not DONKEY’s: wl-paste can deadlock when Emacs owns the clipboard itself. If you run that combination, route terminal clipboard reads through a live graphical frame:

(use-package xclip
  :config
  (setq xclip-method 'wl-copy)   ; autodetection picks xsel first
  (xclip-mode 1))

Configuring the Enter Key

In Normal state RET tries these in order and stops at the first that answers:

  1. An Org buffer — the rules below: follow the link, toggle the checkbox, flip the TODO.
  2. A Markdown buffer — follow the link at point.
  3. A mode not listed in donkey-editing-modes — whatever the mode itself binds RET to, asked at the moment you press it. A command that would type or break a line is refused, so RET stays inert rather than inserting a newline in a buffer you are reading.

Where none of them answers, RET does nothing. That is every buffer of a mode in the list, away from what the Org and Markdown steps act on:

;; the default
(setq donkey-editing-modes
      '(prog-mode text-mode org-mode fundamental-mode
        conf-mode markdown-mode gfm-mode))

Derived modes are covered, so those seven names cover nearly every buffer you actually edit. Org and Markdown are in it for what their own steps leave over: RET on an Org checkbox still toggles it. Add a mode to make RET inert there; remove one to let RET reach whatever that mode binds.

Rules of your own

;; (donkey-add-enter-rule ELEMENT-TYPE PROPERTY COMMAND...)
(donkey-add-enter-rule item :checkbox org-toggle-checkbox)
(donkey-add-enter-rule headline :todo-type donkey-org-todo)
(donkey-add-enter-rule link nil org-open-at-point)

Those three are the defaults. A rule names an Org element type, a property to test (or nil for “any”), and one or more commands tried in order until one is bound and callable — so a rule can name a command from a package you may not have installed.

Rules added later are tried first, which lets you override a default without removing it. Within one press, the element at point is tried against every rule before the element that contains it, and so on outward, so the most specific rule wins.

Turn the defaults off entirely:

(setq donkey-default-enter-rules-enabled nil)

Element types you can write rules for are Org’s own: headline, item, link, table-row, src-block, quote-block, example-block, footnote-reference, timestamp, inline-src-block and the rest of org-element’s vocabulary.

Wrapping: Choosing Your Own Pairs

The pairs m i and m a can select are the pairs a key can wrap — one table, read by both, so a pair added once is added everywhere:

;; add a pair of your own
(with-eval-after-load 'donkey
  (setopt donkey-mark-pair-delimiters
          (cons '(?# . ?#) donkey-mark-pair-delimiters)))

setopt matters here: it runs the setter that claims the key. With plain setq or add-to-list, the key is claimed at the first idle moment after startup instead — which is still before you can press it, but M-x donkey-refresh-wrap-keys is the way to ask by hand if you change the table later.

To wrap with only some of them:

(setq donkey-wrap-delimiters '(?\( ?\[ ?\" ?*))   ; these four only

all, the default, means every pair in the table.

A key that already runs something is never taken

If a character you name already runs a DONKEY command, that command keeps the key and the character simply does not wrap. : goes to a line and > indents, so neither wraps, and DONKEY says so if you ask:

DONKEY: the wrap delimiter : is donkey-goto-line, so it does not wrap

Either rebind the key yourself or pick another character.

Letting your pairing package wrap instead

If you use smartparens, electric-pair-mode or similar and would rather it did the wrapping:

(setq donkey-wrap-region-engine 'pairing-package)

DONKEY then hands the keypress to self-insert-command with the mark still active and lets your package’s hook do the work. The trade: what wraps and how is then your package’s business, including whether pressing the same delimiter again takes the pair off. M-x donkey-toggle-wrap-engine flips between the two so you can feel the difference.

In a buffer where nothing but donkey-pair-mode is pairing, DONKEY’s own pairing is the package, so the wrap there is DONKEY’s own, taking the pair off included, and the toggle says so.

Pairing: every setting

Everyday use is Pairing as you type: turn the mode on, and choose the delimiters. This is everything else it can be told.

Who actually does the pairing

This is the part worth reading, because DONKEY is usually not the one doing it. Emacs comes first:

In this bufferWho pairsWhat DONKEY does
electric-pair-mode onEmacshands Emacs its delimiters
smartparens-mode onsmartparensnothing at all
neitherDONKEYpairs, from its own table

So if you already use electric-pair-mode, turning this on does not replace it or compete with it. Emacs goes on pairing exactly as it did, and DONKEY simply tells it about the delimiters it did not know — which is the job smartparens was doing for you. Drop smartparens, keep electric-pair-mode, turn this on, and your own pairs keep working.

And if you prefer smartparens, DONKEY stays out of the way entirely, the same way it does about wrapping.

M-x donkey-debug-platform says which of the three you are looking at:

Pair typing:    on, electric-pair-mode pairs (9 delimiters, 6 handed to it)

What Emacs already does, and what DONKEY adds

Worth knowing before you turn anything on: electric-pair-mode on its own already closes the common brackets nearly everywhere.

Mode([{"<'
textyesyesyesyesnono
orgyesyesyesyesyesno
emacs-lispyesyesnoyesnono
python, c, jsyesyesyesyesnoyes
htmlyesyesyesyesyesyes
latexyesyesyesyesnono

So what DONKEY adds is not the brackets. It is the pairs Emacs has no way to know about: #, the guillemets, and whatever else you put in donkey-mark-pair-delimiters.

What pairs where is Emacs’s answer, not one answer

Look at the < column above. It closes itself in Org and in HTML and in nothing else — and that is nothing to do with DONKEY, which never pairs < in any mode. Emacs decides by the syntax table, and Org gives < bracket syntax because of <<targets>>, timestamps and the <s templates.

This is the trade you make by leaving Emacs in charge, and it is worth saying plainly: where =electric-pair-mode= is on, what pairs in which buffer is Emacs’s answer, mode by mode. DONKEY hands it the extra delimiters and Emacs settles the rest.

Only in the third arrangement — no electric-pair-mode, no smartparens, DONKEY doing the pairing — do you get one answer everywhere: the same delimiters behaving the same way in a Lisp buffer and in a letter, which is what DONKEY’s wrap keys already do.

Neither is wrong. They are different promises, and which you get depends on what else is on.

Why a delimiter pairs in one mode and not another

Emacs asks two questions, in this order.

First, is the character named outright? electric-pair-pairs is a plain list, and what it names pairs regardless of anything else. It ships holding the double quote and the two typographic quote pairs. That is why " closes itself even in text-mode, where the syntax table calls it mere punctuation.

Second, what does this mode call it? Failing the list, Emacs asks the major mode’s syntax table for the character’s class, and pairs it only if the class is open bracket or string quote. Everything else — punctuation, symbol, word — is not a pair and never closes itself.

So the answer is per mode because syntax tables are per mode:

Charactertextorgemacs-lisppythonhtmllatex
( [openopenopenopenopenopen
{openopensymbolopenopenopen
"punctstringstringstringstringpunct
<symbolopensymbolpunctopensymbol
'wordwordquotestringstringword

Read that table and the behaviour above stops being a surprise:

  • { fails to pair in Emacs Lisp alone, because Lisp does nothing with braces, so they are ordinary symbol characters there.
  • < pairs in Org and HTML alone, because only those two call it a bracket — Org for <<targets>> and timestamps, HTML for tags.
  • " pairs everywhere, including the two modes that call it punctuation, because the named list wins over the table.

It is stable: the table above is identical on Emacs 29.1, 30.1, 30.2 and 31.1, and DONKEY has a test that reads it out of this README and checks it against the running Emacs.

What the other two do instead. Smartparens asks neither question: it keeps explicit pair definitions, globally and per major mode, which is how it can offer Org’s / and === or Ruby’s module … end — none of which any syntax table could express. DONKEY asks neither either: it reads donkey-mark-pair-delimiters the same way in every buffer, which is what makes its own pairing identical in a Lisp file and a letter, and precisely what it gives up when it hands the delimiters to Emacs instead.

Choosing which one you get

Turning DONKEY’s mode on is the same line whichever you want. What decides the arrangement is which other pairing mode is running.

To keep Emacs in charge and let DONKEY supply the extra delimiters — the usual choice, and the one that leaves your editor as it is:

(electric-pair-mode 1)
(donkey-pair-mode 1)

To have DONKEY pair everything, with one answer in every buffer, turn the other two off. electric-pair-mode is already off in stock Emacs, so usually only the first line applies:

(smartparens-global-mode -1)   ; or drop the package
(electric-pair-mode -1)        ; off by default anyway
(donkey-pair-mode 1)

To keep smartparens, do nothing: DONKEY stands down wherever it is on, whether or not donkey-pair-mode is enabled.

M-x donkey-debug-platform says which of the three you ended up with, for the buffer you ran it from.

What safe holds back, and putting one back

Every no in the safe table under Pairing as you type is a setting rather than a rule. The thirteen characters those rows name are donkey-pair-safe-exclusions, and you add to it when a pair you put in the table is one you never want to TYPE. A letter is the case that matters: add (?X . ?X) so that m i X can select between two of them, and X would otherwise close itself, so typing faX would give you faXX.

(setopt donkey-pair-safe-exclusions
        (cons ?X donkey-pair-safe-exclusions))

X stays in the table and stays a wrap key. It simply stops pairing as you type. To drop a delimiter in one major mode rather than everywhere, that is donkey-pair-delimiter-exceptions instead.

When you change the table with setq

DONKEY’s own pairing reads both variables at every press, so a change is live at once. Emacs’s pairing is told the set rather than asking for it, so M-x donkey-pair-refresh hands it over again after a plain setq or add-to-list. setopt and M-x customize-variable need no help.

Turning the mode off gives Emacs back exactly the pairs DONKEY added, and leaves alone any you put in electric-pair-pairs yourself.

Turning it off in some buffers

donkey-pair-excluded-modes is its own list, so a mode can keep NORMAL state and stop pairing:

(setopt donkey-pair-excluded-modes '(org-mode))

Nothing pairs where DONKEY steps aside altogether — a terminal or a REPL on donkey-excluded-modes — whatever that list says.

To drop one delimiter in one mode rather than all of them, name it in donkey-pair-delimiter-exceptions. The shipped entries keep ', ` and # from pairing in Lisp buffers, where #' is a function quote:

(setopt donkey-pair-delimiter-exceptions
        '((emacs-lisp-mode ?\' ?\` ?#)))

This one reaches DONKEY’s own pairing only. Where Emacs is doing the work it is handed the delimiters and decides the rest itself; if you want a delimiter inhibited there, electric-pair-inhibit-predicate is Emacs’s own way to say so.

Turning one on in one mode

donkey-pair-delimiter-inclusions is the other direction, and HTML is what it is for. The less-than sign is less-than in prose, which is why safe leaves it out, but almost every line of an HTML buffer sits between one and its closer:

(setopt donkey-pair-delimiter-inclusions '((html-mode ?<)))

Typing < then script now gives you <script>, and typing > where one already stands steps over it. The character pairs in that mode and in the modes deriving from it, mhtml-mode among them, and in no other buffer.

It adds to the list in force rather than replacing it, so brackets and quotes go on pairing there. Where a mode is named in both lists for the same character the exception wins, and a character donkey-mark-pair-delimiters does not carry is not something any list can add.

Emacs does not need telling this, because html-mode gives < the syntax of an opening bracket and > that of a closing one. That is why electric-pair-mode pairs them there with no configuration, and why DONKEY needs the line: DONKEY pairs from your lists, which are the same in every buffer, rather than from the mode’s syntax table. Modes whose syntax table does NOT say it, nxml-mode among them, want the line just as much.

Where it stops

Nothing pairs in NORMAL state, because nothing types there: a delimiter key wraps the selection instead, which is the section above.

DONKEY’s own pairing does not read the buffer’s syntax to decide whether a quote you typed opens or closes. The rule is simpler and the same in every buffer: if that character already stands after the cursor, step over it. So typing " immediately before an existing " steps over that one rather than opening a new pair. electric-pair-mode asks the syntax table and gets that case right — one more reason to let it keep the job where you have it.

Smartparens

If you use smartparens, one call improves C-g inside its overlays, especially in a terminal:

(with-eval-after-load 'smartparens
  (donkey-setup-smartparens))

Keys a Major Mode Types With

NORMAL state suppresses typing by remapping self-insert-command, which catches every key a mode types through Emacs’s normal path — and that is most of them, Org included.

A mode that binds a key directly to an insert command of its own is a different matter: org-mode puts org-force-self-insert on |, cc-mode puts electric commands on #, * and /, AUCTeX puts its own on $, \ and ^, python-mode (the MELPA one) puts py-electric-comment on #, tagedit puts one on !, and fortran-mode, pascal-mode and tcl-mode each have their own. Twenty-one such commands are refused by name:

Finding the name of one that got through

You will meet this as a key that types in NORMAL state when it should not. To name the command it ran, press it, then C-h l (view-lossage): the last line names the key and the command beside it.

;; forward-char
;; mymode-electric-quote

M-: last-command answers the same question in one line. With the name in hand:

(with-eval-after-load 'donkey
  (add-to-list 'donkey-self-insert-commands 'mymode-electric-quote)
  (donkey-refresh-suppressed-commands))

donkey-refresh-suppressed-commands is only needed when you use setq or add-to-list; through Customize the list re-installs itself.

This is a list and not a rule: a mode with a typing command nobody has met yet will type. If you find one, add it and open an issue — then the next reader does not have to find it twice.

Why Those Two Modes Ship Excluded

For readers who want the reasoning rather than the setting. Nothing here is needed to use DONKEY — it is here so the default can be argued with rather than taken on trust, and so that undoing it is a decision rather than a guess.

What NORMAL state actually costs in an application buffer

Dired is the clearest case, because its keymap is the biggest. Press a key there with NORMAL state on and one of four things happens.

It is refused, because NORMAL state answers every key

!  #  &  0 1 2 3 4 5 6 7 8 9  E  H  L  N  Q  R  T  X  Y  Z  ^  e  f  n  q  s  t

These are the keys Dired binds that DONKEY’s keymap does not. They used to reach Dired; NORMAL state now answers every printable key itself, so they are refused with “is undefined” instead — see Suppressed keys.

It makes the argument below stronger rather than weaker. The mode’s own keys are one more thing NORMAL state costs in a buffer a program made, not one fewer, which is the whole reason such buffers are not in NORMAL state to begin with.

It reaches Dired through Dired’s own remap

j, k and u. Dired remaps next-line, previous-line and undo, and a remap catches the command whichever key ran it — so DONKEY’s motion keys arrive at dired-next-line and dired-previous-line.

This one is a happy accident worth knowing, because it is better than what Dired binds those keys to itself:

KeyDired’s own bindingWhat you get with NORMAL state on
jdired-goto-filedired-next-line
kdired-do-kill-linesdired-previous-line
udired-unmarkdired-undo

It is borrowed, and handed back at the press

The wrap keys — $ ( + < = ~ — and RET. A wrap key is borrowed only while a selection could use it, and a buffer you cannot type into gets it back the moment you press it. What C-h k reports and what the key runs are different things here:

+    binding: donkey-wrap-region   pressing runs: dired-create-directory
<    binding: donkey-wrap-region   pressing runs: dired-prev-dirline
RET  binding: donkey-enter-dwim    pressing runs: dired-find-file

It is DONKEY’s, and the mode does not get it

Everything else. In Dired that is a c d i m o p v w x y, A C D G I M O P S U W, SPC . - >, and the * and % mark prefixes.

Read that last row again and the argument makes itself. m marks, d flags, x executes, C copies, D deletes, and * and % are both of the mark prefixes. Those are not incidental keys — they are what Dired is. A file manager you cannot mark, flag or execute in is not a file manager with some keys moved; it is a file manager you cannot use.

Nothing was ever at risk

Worth saying plainly, because it is the first thing a reader worries about. Every one of these buffers is read-only, so every editing key was refused before it did anything:

d  →  error: Buffer is read-only
x  →  error: Buffer is read-only
c  →  error: Buffer is read-only

What went missing was the mode, not your work. This is a usability default and not a safety fix.

Two wrinkles you may have met and wondered about:

  • i and a put you in INSERT state in a buffer you cannot type into — no error, no message, just DONKEY[I] in the modeline. C-g comes back out.
  • o and D move point before they fail, so a mistyped key can lose your place in a long listing.

Why each of the six

ModeIts letters areSo NORMAL state answered with
magit-modecommit, log, branch, diff, push, stashchange, forward-char, backward-word
git-rebase-modepick, reword, drop, exec, breakyank, delete, change

git-rebase-mode is the sharpest of them: seven of its twelve verbs were gone, pick and drop among them, so an interactive rebase could not be finished with the keys that finish it.

Two of the six are parents, which matters more than it looks.

magit-mode covers the status buffer, the log, the diff, the revision, refs, reflog, stash, cherry, log-select, process and every forge buffer. Naming magit-status-mode instead covers exactly one of them — the rest are its siblings, not its children, since they all derive from magit-mode directly.

tabulated-list-mode is the same idea one level up. A tabulated list is a control panel by construction: rows you act on, single letters as verbs, always read-only. One entry covers the package menu, the buffer menu, and the tabulated UIs that other packages build on it — pdf-tools’s tablist, docker and tmr among them.

When DONKEY[E] appears and the mode is not on the list

This one surprises people, so it is worth being explicit. The modeline says DONKEY[E] exactly when DONKEY has stepped aside — it is never showing you something that is not true. But the mode you are looking at may not be the mode that is on the list, because a parent covers its children:

Buffer’s major modeOn the list?What matched it
magit-log-modenomagit-mode
shell-modenocomint-mode
package-menu-modenotabulated-list-mode
magit-modeyesitself

So M-x eval-expression donkey-excluded-modes can quite correctly fail to mention the mode you are standing in. Rather than work it out, ask:

M-x donkey-check-bindings

DONKEY: Normal state is off here: magit-log-mode derives from
        magit-mode, on donkey-excluded-modes

It names the entry that decided, whether that is the mode itself or an ancestor of it, and says the same for an exception that put NORMAL state back. In an ordinary buffer it says nothing about state at all — nothing decided it there.

A consequence worth knowing before you try it: you cannot take a child off the list while its parent is on it. Removing magit-log-mode does nothing at all, because it was never there and magit-mode still matches:

;; does nothing — magit-log-mode is covered by magit-mode
(setopt donkey-excluded-modes (remq 'magit-log-mode donkey-excluded-modes))

The way to do it is donkey-excluded-mode-exceptions, which is read first and wins:

;; DONKEY out of the way in Magit, except in a log buffer
(with-eval-after-load 'donkey
  (setopt donkey-excluded-mode-exceptions '(magit-log-mode)))

It matches the way donkey-excluded-modes does — an exact mode, or a parent of one — so naming a parent there exempts everything under it. It is empty by default and costs nothing while it is.

The older way, if you would rather keep it all in one list, is to take the parent off and name the children you still want:

;; NORMAL state in the log, DONKEY out of the way everywhere else in Magit
(with-eval-after-load 'donkey
  (setopt donkey-excluded-modes
          (append (remq 'magit-mode donkey-excluded-modes)
                  '(magit-status-mode magit-diff-mode magit-refs-mode
                    magit-process-mode magit-stash-mode))))

magit-revision-mode is not in that list because it derives from magit-diff-mode and follows it — the same rule, one level down.

Why ship the list rather than leave it to you

Three reasons, in the order they matter.

A wrong default should fail where you can see it

If DONKEY ships ON in Magit, you press c to commit, something else happens, and nothing anywhere says why. The keys do not fail — they do other things, quietly and plausibly. There is no thread to pull, and the bug that gets filed is filed against Magit.

If DONKEY ships OFF in Magit and you wanted NORMAL state there, you notice at once, the modeline tells you why (DONKEY[E]), and the fix is one line, below.

Both defaults are wrong for somebody. Only one of them is wrong out loud.

A rule would have been worse than a list

The tempting rule is “step aside in every read-only buffer”, or in every special-mode derivative. Measured across the read-only modes, the numbers split rather than converge. The application half hide most of the mode’s keys. The help-like half hide a handful and gain DONKEY’s motion in exchange, which is the better trade there — so help-mode, occur-mode, compilation-mode, grep-mode, Man-mode and org-agenda-mode are read-only and deliberately not on the list. A rule that caught the six would have caught those as well.

The honest cost of a list is that it has to admit where it stops. A mode nobody has met yet is not on it: eww-mode, elfeed-search-mode, deft-mode, vundo-mode, ranger-mode and pdf-view-mode are all reasonable additions that nobody has asked for. Add them as Modes Where DONKEY Steps Aside shows, and please open an issue so the next reader does not have to.

Undoing it is one line

;; NORMAL state back in Magit, everything else left alone
(with-eval-after-load 'donkey
  (setopt donkey-excluded-modes (remq 'magit-mode donkey-excluded-modes)))

remq rather than delq: delq edits the list in place whenever what it removes is not the first element, and the list it would be editing is the shipped default.

If what you wanted was only Magit’s or Dired’s line motion back, that is a smaller change than un-excluding the whole mode — see What you give up by excluding magit and dired.

The Digraph Chart

M-x donkey-digraph opens the chart of every two-key sequence. If the rows look cramped or too airy in your font:

(setq donkey-digraph-line-spacing 0.3)   ; a third of a row's height

A whole number is pixels; a fraction is that share of the row, so the spacing keeps its proportion if you scale the text.

A Complete Configuration

Every option DONKEY has, in one use-package block, with the shipped value shown for each. A line marked ; default sets what you already have — it is there so you can see the option exists and what shape its value takes, not because you need it. Read this as a menu and delete what you do not want.

setopt rather than setq throughout. Four of these options rebuild something through a setter when they change — the two wrap options, the pair table and the suppressed commands — and setopt runs it the way Customize would, where setq would leave the keymap as it was.

donkey-input-methods looks like a fifth and is not: the SPC i keymap is rebuilt from a variable watcher, which plain setq also trips. Using setopt for everything means never having to know which is which.

;; The clone and the compile step, the same block as [[#installing][Installing]].
;; From a package archive instead: drop my-donkey-dir, :load-path
;; and :init, and use :ensure t.
(defvar my-donkey-dir (expand-file-name "~/src/donkey"))

(use-package donkey
  :load-path my-donkey-dir

  :init
  ;; A package archive byte-compiles what it installs.  A clone does
  ;; not, and nothing in DONKEY compiles itself, so compile it here --
  ;; and again whenever donkey.el turns out to be newer than the .elc
  ;; beside it, which is what a `git pull' leaves behind.
  (let* ((src (expand-file-name "donkey.el"  my-donkey-dir))
         (elc (expand-file-name "donkey.elc" my-donkey-dir))
         (recompiled nil))
    (if (not (file-exists-p src))
        (message "DONKEY: no donkey.el under %s -- check my-donkey-dir"
                 my-donkey-dir)
      (when (file-newer-than-file-p src elc)
        (require 'bytecomp)
        (let ((start (current-time)))
          (setq recompiled
                (and (ignore-errors (byte-compile-file src))
                     (float-time (time-since start))))))
      (let ((compiled (and (file-exists-p elc)
                           (not (file-newer-than-file-p src elc)))))
        (load (if compiled elc src) nil t)
        (message "DONKEY %s loaded %s%s"
                 (donkey-version)
                 (if compiled "compiled" "from source (interpreted)")
                 (if recompiled
                     (format ", recompiled in %.2fs" recompiled)
                   "")))))

  :config

  ;;; Where DONKEY steps aside

  ;; Thirteen modes ship on the list: the terminals and REPLs, where
  ;; NORMAL state would break the program on the other end, the
  ;; applications whose single letters are their commands, and Info,
  ;; whose alphabet would not fit around h j k l.  Add yours -- this
  ;; adds to the list rather than replacing it.
  (dolist (mode '(pdf-view-mode elfeed-search-mode))
    (add-to-list 'donkey-excluded-modes mode))

  ;; An exception is read first and wins, which is how a child comes
  ;; out from under a listed parent: magit-log-mode is not on the
  ;; list, magit-mode is, so removing it there would do nothing.
  (setopt donkey-excluded-mode-exceptions nil)                 ; default

  ;;; Modes DONKEY supports rather than takes over

  ;; Every buffer a program made for you is a support mode, decided
  ;; by the rule in donkey--program-buffer-p.  A section adds what h
  ;; and l mean there; j and k are never written down.  Forty-one
  ;; ship -- see the chapter above for the whole table.
  ;;
  ;; CONS rather than a literal list: setopt with a list of your own
  ;; REPLACES the table and takes the other forty with it.  The
  ;; first section a mode matches answers, so one written here wins
  ;; over the shipped one -- which is how to disagree with a section
  ;; without excluding the mode.
  (setopt donkey-support-modes
          (cons '(dired-mode (?h . dired-up-directory)
                             (?l . dired-open-file)
                             (?y . dired-copy-filename-as-kill)
                             (?J . dired-goto-file)
                             (?K . dired-do-kill-lines))
                donkey-support-modes))

  ;; Where the rule is wrong the other way: a buffer that looks like
  ;; a program's but is one you type in.  This is NORMAL state, not
  ;; [E] -- DONKEY holds every letter there again -- so it is the
  ;; wrong lever for a list you drive.
  (setopt donkey-support-mode-exceptions '(occur-edit-mode))   ; default

  ;; A bare symbol in a section names a key PACKAGE: the set of keys
  ;; that package holds, written once and taken by naming it.  One
  ;; ships, `prose', the reading set of 45 keys.  Your own goes on
  ;; the front the same way a section does:
  ;; (setopt donkey-key-packages
  ;;         (cons '(mine "h" "l" "RET") donkey-key-packages))


  ;;; The Enter key

  ;; Where RET does nothing in NORMAL state.  Everywhere else it is
  ;; handed back to the mode.
  (setopt donkey-editing-modes
          '(prog-mode text-mode org-mode fundamental-mode
            conf-mode markdown-mode gfm-mode))                 ; default

  ;; The three built-in Org rules: checkbox, TODO, link.
  (setopt donkey-default-enter-rules-enabled t)                ; default
  ;; Your own, one element type at a time:
  ;; (donkey-add-enter-rule link nil org-open-at-point)

  ;;; The cursor

  (setopt donkey-cursor-normal 'box)                           ; default
  (setopt donkey-cursor-insert '(bar . 2))                     ; default

  ;; A support mode takes NORMAL's shape and an excluded mode takes
  ;; INSERT's, so the cursor says whether the buffer is taking your
  ;; letters.  Give either its own to tell all four states apart.
  (setopt donkey-cursor-support 'box)                          ; default
  (setopt donkey-cursor-excluded '(bar . 2))                   ; default
  (setopt donkey-decscusr-denied-terminals
          '("dumb" "linux"))                                   ; default

  ;;; Cursors in a column

  ;; T refuses to make more cursors than this; nil for no limit.
  (setopt donkey-split-cursor-limit 10000)                     ; default
  ;; Ask past the limit instead of refusing.
  (setopt donkey-split-cursor-limit-ask nil)                   ; default

  ;;; Undo history

  ;; Emacs's own limits, not DONKEY's.  A split's step is one undo
  ;; entry of about 8 bytes a place plus the text, so Emacs's 160 KB
  ;; keeps a step or two at ten thousand places; these keep about a
  ;; thousand there, or a hundred and thirty at a hundred thousand.
  (setopt undo-limit (* 100 1024 1024))
  (setopt undo-strong-limit (* 120 1024 1024))
  (setopt undo-outer-limit (* 200 1024 1024))

  ;;; Selecting and wrapping

  ;; Twenty-one pairs ship.  What m i and m a select, the keys wrap,
  ;; so a pair added here is a wrap key too.
  (setopt donkey-mark-pair-delimiters
          (cons '(?# . ?#) donkey-mark-pair-delimiters))

  ;; Which of those pairs get a key: all of them, or name characters.
  (setopt donkey-wrap-delimiters 'all)                         ; default

  ;; Who puts the pair on: DONKEY itself, or smartparens, or
  ;; electric-pair-mode.
  (setopt donkey-wrap-region-engine 'donkey)                   ; default

  ;;; Closing a delimiter as you type

  ;; Off unless asked for.  With electric-pair-mode on, Emacs goes on
  ;; doing the pairing and DONKEY only hands it the delimiters; with
  ;; smartparens on, DONKEY does nothing at all; with neither, DONKEY
  ;; pairs.  M-x donkey-debug-platform says which you have.
  ;; (donkey-pair-mode 1)

  ;; How much of the pair table types: `safe' is that table less the
  ;; ordinary punctuation, `all' is every pair in it, and a list is
  ;; exactly those characters and no others.
  (setopt donkey-pair-delimiters 'safe)                        ; default
  ;; (setopt donkey-pair-delimiters 'all)
  ;; (setopt donkey-pair-delimiters '(?\( ?\[ ?\{ ?\"))

  ;; What `safe' leaves out: characters that are ordinary text far
  ;; more often than they are delimiters.  Add to it when a pair you
  ;; put in the table is one you never want to type -- a letter, say,
  ;; added so that m i can select between two of them.
  (setopt donkey-pair-safe-exclusions
          '(?< ?\' ?\` ?= ?* ?~ ?\| ?\\ ?/ ?: ?+ ?_ ?$))       ; default

  ;; Modes where typing does not pair, DONKEY's own list.
  (setopt donkey-pair-excluded-modes nil)                      ; default

  ;; One delimiter dropped in one mode.  The shipped entries keep the
  ;; Lisp quote characters and # from pairing in Lisp buffers.
  ;; (setopt donkey-pair-delimiter-exceptions
  ;;         '((emacs-lisp-mode ?\' ?\` ?#)))

  ;; One delimiter added in one mode, the other direction.  < is
  ;; punctuation in prose, which is why `safe' leaves it out, and a
  ;; delimiter in HTML.
  (setopt donkey-pair-delimiter-inclusions nil)                ; default
  ;; (setopt donkey-pair-delimiter-inclusions '((html-mode ?<)))

  ;; Stay out of smartparens' way where it is on.
  (setopt donkey-pair-stand-down t)                            ; default

  ;; DONKEY's own pairing reads both variables at every press.  Emacs
  ;; is TOLD the set instead, so after a plain setq or add-to-list on
  ;; either of them, hand it over again:
  ;; M-x donkey-pair-refresh

  ;; How far S remembers.
  (setopt donkey-position-ring-max 10)                         ; default

  ;;; Keys a major mode types with

  ;; Twenty-one commands are refused by name.  To add one you have
  ;; met, press the key and C-h l names it:
  ;; (setopt donkey-self-insert-commands
  ;;         (cons 'mymode-electric-quote donkey-self-insert-commands))

  ;;; Other languages

  ;; Each entry is (KEY LABEL INPUT-METHOD), reached under SPC i.
  (setopt donkey-input-methods nil)                            ; default
  ;; '(("s" "swedish" "swedish-postfix")
  ;;   ("p" "polish"  "polish-slash"))

  ;;; The digraph chart

  (setopt donkey-digraph-line-spacing 0)                       ; default

  ;;; What DONKEY says for itself

  ;; The one line at first idle when something has taken its keys.
  ;; M-x donkey-check-bindings asks for the whole answer any time.
  (setopt donkey-report-binding-changes t)                     ; default

  ;;; Keys of your own

  ;; A single letter, yours in every NORMAL-state buffer.
  (keymap-set donkey-normal-mode-map "F" #'delete-other-windows)

  ;; Under the SPC leader, where (NAME . COMMAND) carries the name
  ;; into ? and into which-key.
  (keymap-set donkey-leader-map "b" '("switch buffer" . switch-to-buffer))
  (keymap-set donkey-leader-map "g" '("magit"         . magit-status))

  ;;; One key back to its own mode

  ;; Remapping the DONKEY command in that mode's own map: i writes a
  ;; note in an org-noter document buffer and is INSERT state
  ;; everywhere else, with nothing to remember.
  (with-eval-after-load 'org-noter
    (define-key org-noter-doc-mode-map
                [remap donkey-insert-here] #'org-noter-insert-note))

  ;;; Hooks

  ;; donkey-normal-mode-hook and donkey-insert-mode-hook run when
  ;; their state is switched ON *and* when it is switched OFF, and
  ;; one state change fires both of them -- so a function here reads
  ;; the variable rather than assuming.  This keeps the current line
  ;; highlighted while you are reading and drops it while you type:
  (add-hook 'donkey-normal-mode-hook
            (lambda () (hl-line-mode (if donkey-normal-mode 1 -1))))

  ;; donkey-mode-hook runs when the global mode itself is toggled,
  ;; for setup that needs DONKEY to be there already, and
  ;; donkey-pair-mode-hook when the pairing is switched on or off.

  ;;; And on

  (donkey-mode 1))

When Something Is Not Right

What does this key do?

C-h k then the key, as always in Emacs: it tells you what that key runs in this buffer, whoever bound it.

? in NORMAL state opens DONKEY’s own chart — every key it binds, grouped by prefix, with the mark run’s keys listed too because they live in a transient map that C-h b cannot show. Command names in it are buttons; click one for its documentation.

What happened to DONKEY’s keys?

M-x donkey-check-bindings compares the keys DONKEY bound when it loaded with the keys as they stand, and answers for the current buffer as well:

DONKEY: every key is as DONKEY left it; 2 wrap delimiters cannot
take their key -- see the message log

The lines it can print, in plain terms:

  • ; is consult-outline now, was donkey-goto-line — you, or a package, bound that key. Nothing is broken; you are being told so you are not surprised later.
  • D runs org-kill-line in this org-mode buffer, which remaps kill-line — the mode has its own better version of the same command and DONKEY’s key gets it. Counted separately from keys that changed hands, because nothing was lost.
  • the wrap delimiter : is donkey-goto-line, so it does not wrap — you asked for : as a wrap pair, but that key already runs a DONKEY command. Pick another character, or rebind the key.
  • the wrap delimiter # has no key yet; M-x donkey-refresh-wrap-keys — you added a pair after startup. Run that command.

DONKEY says the quieter half of this once, a moment after startup, and nothing at all when nothing differs. Turn even that off with:

(setq donkey-report-binding-changes nil)

M-x donkey-debug-platform

Prints what DONKEY sees: window system, terminal type, whether the cursor escape sequence is allowed here and why, which clipboard tool was found, and the same binding report. The first thing to paste into a bug report.

A C-g in INSERT state that sometimes does nothing

The symptom: you press C-g to leave INSERT state and nothing happens — or Quit flashes — and the second or third press works. More often on large frames and prose-heavy buffers.

This is not a DONKEY binding misfiring, and no package can intercept it, because the press never becomes a keypress at all. C-g is also Emacs’s interrupt character: arriving while Emacs is running Lisp it is consumed interrupting that work, below every keymap and every hook. The moment right after an edit is exactly when Emacs tends to be running Lisp — refontifying the window, re-spellchecking it, running checkers.

To confirm: reproduce it, then press C-h l (view-lossage). A press eaten as an interrupt is simply absent from the log — you pressed three times, the log shows one.

What helps is shrinking the busy window:

;; redisplay skips fontification while input is pending, so a fast
;; keypress is read as a key instead of interrupting jit-lock
(setq redisplay-skip-fontification-on-input t)

;; if you run jinx: move its recheck out from under your fingers
;; (the default 0.2 is squarely where a quick C-g lands)
(setq jinx-delay 0.8)

The same applies to any checker with an idle delay: raise it past your reaction time.

DONKEY recovers where recovery is possible: a quit that unwinds to the command loop while INSERT state is on is given the meaning the press had, so the exit runs instead of Quit being printed. It cannot catch a quit that redisplay or a timer swallows before the command loop sees it — which is what the settings above are for.

A mark run follows you between buffers

That is deliberate. A mark run is a transient keymap, and Emacs keeps those per terminal — two graphical frames on one display are one terminal — so a plain transient map would be armed in the other frame too, and any command outside the run would end it.

DONKEY puts the run down instead: leave its buffer, or let its frame lose focus, and the run is stored with its selection and its u=/=U history; come back, and it is armed again exactly as it was. Everywhere in between, every key is itself.

Three things to know:

  • The selection must still be active when you come back. A run whose selection is gone is forgotten, like one you ended with C-g.
  • A command that stays in the run’s buffer but is not one of the run’s still ends it: x deletes the selection, and the run is over.
  • Clicking into a frame to focus it is a mouse command in that buffer and ends the run. Switch frames with the keyboard, or click once and start the run again.

Every key is undefined in a frame I just switched to

The symptom: you switch frames with a desktop shortcut, press a key, and the echo area says s-w is undefined; every key does the same until you press C-g or a modifier once.

The s- is the tell — the keys are arriving with Super held. The shortcut that moved the focus held Super, its press reached the new frame as part of the focus change, and its release went to the compositor, so GDK keeps Super down until it sees the key again. DONKEY never sees those keys at all: s-w is not w. C-h l shows them with the modifier you did not press.

What to do: tap Super once after switching, or switch frames with a click or a shortcut that releases its modifier first. Terminal frames do not have the problem.

Reference

Every key

Movement

h j k lleft, down, up, right
w bforward, back one word
W Bforward, back one balanced expression
J Kdown, up one whole line
Sback through where you have been
:go to line
%select the whole buffer
z zrecentre

The g prefix

g gbeginning of bufferg eend of buffer
g hbeginning of lineg lend of line
g jjoin linesg qfill region
g Qfill paragraphGend of buffer

Entering INSERT

ibefore pointaafter point
Iline startAline end
oline belowOline above
cchange

Editing

d xdelete selection or characterDto end of line
cchangeCcomment
ycopyppaste
Ppaste rectangle>indent
uundoUredo
.repeat

Selecting, and the m prefix

vselectionVvisual line / line selection
Mmark runm vrectangle
m wwordm Wsymbol
m bword backwardm Bsymbol backward
m ssentencem Ssentence backward
m pparagraphm Pparagraph backward
m iinside a pairm apair and contents
m Iinside a sexpm Asexp and contents
m lbank selectionm uunbank last
m Uunbank the runm DELclear the bank

The r, z and SPC prefixes

r rreplace-regexp
r qquery-replace
z zrecentre
SPCyour leader
SPC i &insert one digraph
SPC i .digraph input method
SPC i -input method off

Wrapping

Nineteen keys — " $ ' ( ) * + / < = [ \ ] _ ` { | } ~ — plus « » ‘ ’ “ ” ‹ ›. With a selection they wrap it; press again with the pair around the selection and it comes off.

Refused on purpose

, - ; answer “is undefined”. BACKSPACE and DELETE (under all four of their key names) do nothing at all.

Every command

80 commands. These have no key and are reached with M-x:

CommandDoes
donkey-modeturn DONKEY on or off everywhere
donkey-normal-modeNORMAL state in this buffer
donkey-insert-modeINSERT state in this buffer
donkey-enter-normalenter NORMAL state
donkey-tutorthe tutor buffer
donkey-digraphthe digraph chart
donkey-check-bindingswhat has taken DONKEY’s keys
donkey-debug-platformwhat DONKEY sees on this machine
donkey-versionthe loaded version
donkey-org-scratchan *org-scratch* buffer
donkey-org-todotoggle a headline’s TODO state
donkey-switch-other-bufferback to the previous buffer
donkey-mark-run-adoptadopt the current selection into a mark run
donkey-refresh-wrap-keysclaim wrap keys after changing the pair table
donkey-refresh-suppressed-commandsre-apply donkey-self-insert-commands
donkey-toggle-wrap-engineswitch between DONKEY and your pairing package
donkey-pair-modeclose a delimiter as you type it
donkey-pair-refreshhand the delimiters to electric-pair-mode again
donkey-setup-smartparensimprove C-g inside smartparens overlays
donkey-add-denylist-entrystop sending cursor escapes to a terminal
donkey-remove-denylist-entryundo that

The rest are bound to keys. In full, so that every command can be found by name — one row per key, and every row is checked against the keymap by the test suite:

KeyCommand
m ldonkey-bank-selection
cdonkey-change
m <delete>donkey-clear-banked-selection
m <deletechar>donkey-clear-banked-selection
m DELdonkey-clear-banked-selection
Cdonkey-comment-dwim
ydonkey-copy
ddonkey-delete
xdonkey-delete
?donkey-describe-bindings
SPC i -donkey-disable-input-method
<enter>donkey-enter-dwim
RETdonkey-enter-dwim
:donkey-goto-line
>donkey-indent-region-or-line
SPC i .donkey-input-method-digraphs
adonkey-insert-after
Idonkey-insert-beginning-of-line
SPC i &donkey-insert-digraph
Adonkey-insert-end-of-line
idonkey-insert-here
g jdonkey-join-line
Sdonkey-jump-back
m idonkey-mark-inner
m adonkey-mark-outer
m pdonkey-mark-paragraph
m Pdonkey-mark-paragraph-backward
Mdonkey-mark-run-toggle
m sdonkey-mark-sentence
m Sdonkey-mark-sentence-backward
m Idonkey-mark-sexp-inner
m Adonkey-mark-sexp-outer
m Wdonkey-mark-symbol
m Bdonkey-mark-symbol-backward
%donkey-mark-whole-buffer
m wdonkey-mark-word
m bdonkey-mark-word-backward
Odonkey-open-above
odonkey-open-below
m vdonkey-rectangle-mark-mode
Udonkey-redo
vdonkey-set-mark
m udonkey-unbank-line
m Udonkey-unbank-section
Vdonkey-visual-line-toggle
Jdonkey-visual-next-line
Kdonkey-visual-previous-line
"donkey-wrap-region
$donkey-wrap-region
'donkey-wrap-region
(donkey-wrap-region
)donkey-wrap-region
*donkey-wrap-region
+donkey-wrap-region
/donkey-wrap-region
<donkey-wrap-region
=donkey-wrap-region
[donkey-wrap-region
\donkey-wrap-region
]donkey-wrap-region
_donkey-wrap-region
`donkey-wrap-region
{donkey-wrap-region
|donkey-wrap-region
}donkey-wrap-region
~donkey-wrap-region
«donkey-wrap-region
»donkey-wrap-region
‘donkey-wrap-region
’donkey-wrap-region
“donkey-wrap-region
”donkey-wrap-region
‹donkey-wrap-region
›donkey-wrap-region
pdonkey-yank
Pdonkey-yank-rectangle

Inside a mark run the letters mean something else. Listed the other way round, because these are true of the run and not of NORMAL state:

CommandKey in a run
donkey-mark-run-buffer-endG
donkey-mark-run-buffer-endg e
donkey-mark-run-buffer-startg g
donkey-mark-run-cancelM
donkey-mark-run-downj
donkey-mark-run-exchange*
donkey-mark-run-lefth
donkey-mark-run-line-backwardK
donkey-mark-run-line-endg l
donkey-mark-run-line-forwardJ
donkey-mark-run-line-startg h
donkey-mark-run-refuseV
donkey-mark-run-refusev
donkey-mark-run-rightl
donkey-mark-run-step-backu
donkey-mark-run-step-forwardU
donkey-mark-run-upk

w W b B s S inside a run mark and grow, running the same commands their m-prefixed versions run in NORMAL state.

? shows the same list inside Emacs, with the mark run’s keys as well, and M-x apropos-command donkey- finds any of them.

Every user option

OptionDefaultWhat it does
donkey-excluded-modes13 modes, see abovewhere DONKEY steps aside completely
donkey-support-modes41 sections, see abovewhat h and l do in a support mode
donkey-key-packages1 package, prosethe keys a section takes by naming it
donkey-support-mode-exceptionsoccur-edit-modeprogram-looking buffers you type in
donkey-excluded-mode-exceptionsnilmodes that stay on under an excluded parent
donkey-editing-modes7 broad modeswhere RET does nothing
donkey-position-ring-max10how far S remembers
donkey-cursor-normalboxcursor in NORMAL state
donkey-cursor-insert(bar . 2)cursor in INSERT state
donkey-cursor-supportboxcursor in a support mode, [S]
donkey-cursor-excluded(bar . 2)cursor in an excluded mode, [E]
donkey-decscusr-denied-terminals("dumb" "linux")terminals that get no cursor escapes
donkey-split-cursor-limit10000most cursors t makes; nil for no limit
donkey-split-cursor-limit-asknilask past the limit instead of refusing
donkey-mark-pair-delimiters21 pairswhat m i=/=m a select and the keys wrap
donkey-wrap-delimitersallwhich of those pairs get a key
donkey-wrap-region-enginedonkeywho puts the pair on: DONKEY or your pairing package
donkey-pair-modenilclose a delimiter as you type it; opt-in
donkey-pair-delimiterssafewhich delimiters close themselves as you type
donkey-pair-safe-exclusions13 characterswhat safe leaves out of the typing set
donkey-pair-excluded-modesnilmodes where typing does not pair
donkey-pair-delimiter-exceptions4 modesone delimiter dropped in one mode
donkey-pair-delimiter-inclusionsnilone delimiter added in one mode
donkey-pair-stand-downtstay out of smartparens’ way
donkey-self-insert-commands21 commandsmode commands that type, refused in NORMAL state
donkey-input-methodsnilyour input methods under SPC i
donkey-default-enter-rules-enabledtinstall the three built-in Enter rules
donkey-report-binding-changestsay once at startup what has taken DONKEY’s keys
donkey-digraph-line-spacing0air under each row of the digraph chart

M-x customize-group donkey shows them all with their documentation.

Version, changelog, and reporting a bug

M-x donkey-version prints the version running in this session. CHANGELOG.org in the repository is written for people using the package rather than from the commit log.

In a bug report, please include the output of M-x donkey-debug-platform and, for anything about a key, C-h l (view-lossage) taken right after reproducing it.

Developing

CI runs on every push to master and every pull request; the same checks by hand:

# the full suite
emacs -Q --batch -L . -L donkey-testing \
      $(for f in donkey-testing/*-test.el; do printf ' -l %s' "$f"; done) \
      -f ert-run-tests-batch-and-exit

# byte-compile, warnings as errors
emacs -Q --batch --eval '(setq byte-compile-error-on-warn t)' \
      -f batch-byte-compile donkey.el

# documentation conventions
emacs -Q --batch --eval "(progn (require 'checkdoc) (checkdoc-file \"donkey.el\"))"

# MELPA packaging conventions (needs package-lint)
emacs -Q --batch --eval "(package-initialize)" \
      -l package-lint -f package-lint-batch-and-exit donkey.el

Delete =donkey.elc= before running the suite. With a compiled file present -L . loads it in preference to the source, and byte-code open-codes delete-region, forward-char and forward-line, which defeats the mocks several tests use; seven tests then fail for reasons unrelated to any change. CI keeps compilation in a separate job for exactly this reason.

The test files are deliberately not compiled with warnings as errors: they carry unused lambda arguments (mock signatures that must match the real command) and references to optional third-party symbols.

Emacs versions

VersionWhat it is
29.1the minimum in Package-Requires
30.1the previous stable series
31.1the current stable release
release-snapshotthe branch upstream is preparing its next release from

A fifth job builds Emacs master and is allowed to fail, so churn in a version nobody runs yet does not turn the build red. The single-version jobs — each test file alone, Smartparens, shuffled order, the terminal frame, and lint — run on 31.1: they test properties of the suite rather than of Emacs.

About

An opinionated modal editing minor mode for Emacs, built on native commands, that leaves Emacs alone.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages