Start here
Everyday use
- Moving
- Entering INSERT state
- Editing
- Selecting — objects, rectangles, banks, the mark run
- Wrapping a selection in a pair
- The Enter key
- Search and replace
- Typing characters your keyboard lacks
- Other languages
- Suppressed keys
Making it yours
- Your own keys — one key, the leader, giving a key back
- If a key is not doing what you want
- Modes where DONKEY steps aside
- Modes DONKEY supports rather than takes over —
hjkl, and the mode keeps the rest - The cursor
- The clipboard
- Configuring the Enter key
- Choosing your own pairs
- Keys a major mode types with
- Why those two modes ship excluded — the reasoning, for readers who want it
- The digraph chart
- A complete configuration
When something is not right
Reference
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.
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:
- Letters run commands instead of typing.
- Digits are not counts —
C-u 3is, as in stock Emacs. RETdoes nothing in a buffer you are editing, and is handed back to the mode in buffers you are not.BACKSPACEandDELETEdo 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.
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.
DONKEY is one file with no dependencies beyond Emacs 29.1.
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))
: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.
M-x donkey-mode toggles DONKEY everywhere. Turning it off clears
every trace of it from every buffer and gives you stock Emacs back.
M-x donkey-mode. The modeline showsDONKEY[N]: you are in NORMAL state.- Press
i. The modeline showsDONKEY[I]. Type a sentence — this is ordinary Emacs. - Press
C-g. Back toDONKEY[N]. h j k lmove left, down, up, right.wandbmove by words.vstarts a selection,jandlgrow it,ycopies it,ppastes.ddeletes the character under the cursor, or the selection if you have one.uundoes.C-x C-ssaves, as always.
That is enough to work. ? shows every key DONKEY binds, in a buffer
you can read.
M-x donkey-tutor opens a buffer you learn in by editing it, the way
M-x help-with-tutorial and vimtutor do.
Thirteen lessons, ending with the two the keyboard itself cannot give you — wrapping a selection, and the characters that are not on it:
- Moving around (including
Sfor undoing a mis-keyed jump) - Counts
- Typing
- Deleting and changing
- Selecting things (words, sentences, delimiters, levels)
- Whole lines
- Copy and paste
- Banking, which is DONKEY’s own idea
- Columns (rectangles)
- Changing your mind (undo, and the exchange key)
- When two selections disagree
- Wrapping a selection
- Characters your keyboard does not have (digraphs)
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.
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.
| Key | Does |
|---|---|
h | left one character |
j | down one line |
k | up one line |
l | right one character |
w | forward one word |
b | back one word |
W | forward one balanced expression |
B | back one balanced expression |
g g | beginning of the buffer |
G | end of the buffer |
g e | end of the buffer (Helix’s spelling) |
g h | beginning of the line |
g l | end of the line |
: | go to a line by number |
z z | recentre the screen |
On a wrapped line, j moves to the next line, which may be off the
screen. J and K move by what you can see instead — the next and
previous screen row.
V turns that around: it switches the buffer into visual-line mode,
where j and k move by screen rows and J and K move by logical
lines. Press V again to switch back.
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 linesC-u 3 d— delete three charactersC-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.
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)
| Key | Where you end up |
|---|---|
i | before the cursor |
a | after the cursor |
I | at the first non-blank character of the line |
A | at the end of the line |
o | on a new line below |
O | on a new line above |
c | replacing the selection, or the character at point |
C-g brings you back from any of them.
| Key | Does |
|---|---|
d | delete the selection, or the character under the cursor |
x | the same as d (Vim’s spelling; d is Helix’s) |
D | delete to the end of the line |
c | change: delete, then INSERT state |
y | copy the selection |
p | paste |
P | paste as a rectangle |
u | undo |
U | redo — including undo-tree’s own redo, where that is on |
C | comment or uncomment |
g j | join this line and the next |
> | indent the selection, or this line |
g q | fill the selection |
g Q | fill 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.
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.
V — see Long lines above — also serves as a line-wise selection
when a selection is what you are after.
| Key | Selects |
|---|---|
m w | the word at point |
m W | the symbol at point |
m b | the word before point |
m B | the symbol before point |
m s | the sentence |
m S | the sentence before |
m p | the paragraph |
m P | the paragraph before |
m i | inside a pair — quotes, brackets, anything |
m a | the pair as well as what it holds |
m I | inside the balanced expression |
m A | the balanced expression as well |
m v | a 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.
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.
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:
| Keys | Do |
|---|---|
w W b B s S | mark 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, K | continue a visible run by character, line and buffer |
u, U | step back and forward through the run’s history |
. | repeat |
* | swap which end of the selection grows |
p, P | stay the paste keys — M p replaces the marked word |
d, y, x, c | act on the run’s selection, as everywhere else |
v, V | refused: neither can take a selection the run owns |
M | end the run |
| anything else | a 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.
m v starts a rectangle. P pastes one back. Rectangles work with the
wrap keys and with banking, below.
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.
| Key | Does |
|---|---|
m l | bank the selection |
m u | take back the last bank |
m U | take back the whole run of banks |
m DEL | clear 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.
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 goes on; select the pair as well and it comes off.
Characters your keyboard cannot reach are available too — see Typing characters your keyboard lacks — and rectangles can be wrapped line by line.
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.
- 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.
Press a delimiter on a selection that already sits inside that delimiter and the three engines part company:
| Your selection | DONKEY | electric-pair-mode | Smartparens |
|---|---|---|---|
word in say "word" now | say word now | say ""word"" now | say ""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.
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.)
| Key | Runs |
|---|---|
r r | replace-regexp |
r q | query-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.
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.
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.
These do nothing in NORMAL state, on purpose:
| Key | Why |
|---|---|
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.
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.
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.
(with-eval-after-load 'donkey
(keymap-set donkey-normal-mode-map "F" #'delete-other-windows))
That key is yours in every NORMAL-state buffer.
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.
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.
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.
A key bound to undefined is refused. Unbind it and it falls through
to whatever the major mode has:
(with-eval-after-load 'donkey
(keymap-unset donkey-normal-mode-map ";")) ; ; is the mode's again
That works for any printable key, not only the ones DONKEY binds:
keymap-unset leaves a nil in DONKEY’s map, and a nil there shadows
what answers the key underneath.
Its REMOVE argument does the opposite, and is worth knowing so you do
not reach for it by mistake. It takes the binding out rather than
nilling it, so the key goes back to being refused:
(with-eval-after-load 'donkey
(keymap-unset donkey-normal-mode-map ";" t)) ; ; is refused again
Be aware of what you are letting through: some major modes bind punctuation to a command that types, which is why these keys are refused rather than left open in the first place.
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.
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.
dandxare bothdonkey-delete, so remapping that moves both; every wrap key isdonkey-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.
See Modes Where DONKEY Steps Aside, just below. One line, and the mode has all of its keys back.
If it is your key rather than the mode’s, bind it in
donkey-normal-mode-map as above — that wins everywhere.
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:
| Mode | h | l |
|---|---|---|
dired-mode | up a directory | open the file or enter the directory |
ibuffer-mode | — | visit the buffer |
speedbar-mode | up a directory | open the file or enter the directory |
Man-mode | previous section | next section |
woman-mode | — | follow |
apropos-mode | — | follow |
image-mode | previous file | next file |
doc-view-mode | previous page | next page |
tar-mode | — | extract the entry |
archive-mode | — | extract the entry |
Custom-mode | up 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-mode | a week earlier | a 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-mode | a day earlier | a 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.
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.
Where h, j, k or l displaces something the mode had, the section
usually gives it the shift of the key that took it:
| Mode | Key | Runs |
|---|---|---|
dired-mode | J K | go to file, kill lines |
ibuffer-mode | J K | jump to buffer, kill lines |
speedbar-mode | K | kill the buffer on the line |
help-mode | H L R | back, forward, revert |
eww-mode | L | back a page |
dictionary-mode | H L | previous, match words |
shortdoc-mode | C | copy the function as kill |
log-view-mode | T | toggle the mark on the entry |
bookmark-bmenu-mode | L | load a bookmark file |
ert-results-mode | J | jump between summary and result |
calendar-mode | H | the holidays here |
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.
Four keys are taken, so four commands lose their key. Two of them were never really lost:
| Mode | Key | Was | Still reachable |
|---|---|---|---|
dired-mode | h | describe-mode | yes — C-h m |
dired-mode | l | dired-do-redisplay | ≈ g, which reverts |
ibuffer-mode | h | (unbound) | nothing was there |
ibuffer-mode | l | ibuffer-redisplay | g, 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:
| Mode | Key | Runs |
|---|---|---|
dired-mode | J | dired-goto-file |
dired-mode | K | dired-do-kill-lines |
ibuffer-mode | J | ibuffer-jump-to-buffer |
ibuffer-mode | K | ibuffer-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.
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.
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.
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.
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.
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 kstill tells the truth about it. - Remap the command DONKEY’s map holds, not the one the mode already
reached.
jisnext-linethere, and Dired’s own remap turns that intodired-next-line; a remap ofdired-next-linewould be a second hop, and Emacs does not take it. - A remap follows the command everywhere it is reachable. Remapping
dired-find-filemovesl,RETandetogether. That is usually what you want — it is howdired-openworks — but it is not a way to change one key.
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.
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.
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.
Three kinds of mode, for three different reasons.
| Mode | Why |
|---|---|
comint-mode | every 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-mode | a full terminal — character at a time, raw mode |
vterm-mode | the same, faster |
eshell-mode | its own command line and input loop |
eat-mode | a terminal that derives from none of the above |
mistty-mode | the same, wrapping a shell |
slime-repl-mode | Common Lisp REPL, not a comint derivative |
cider-repl-mode | Clojure REPL, the same |
racket-repl-mode | Racket REPL, the same |
haskell-interactive-mode | GHCi 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.
| Mode | What NORMAL state would take |
|---|---|
magit-mode | commit, log, branch, diff, push, stash. Covers every magit and forge buffer |
git-rebase-mode | pick, 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.
| Mode | Why |
|---|---|
Info-mode | n, 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.
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))
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.)
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.
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 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.
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.
y and p use the system clipboard where there is one, falling back
to the kill ring. 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.
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))
RET does nothing in the modes listed in donkey-editing-modes:
;; 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. Add a mode to make RET inert there; remove
one to let RET fall through to whatever that mode binds.
Everywhere else, RET tries these in order and stops at the first
that answers:
- An
org-agendabuffer — whatever the agenda bindsRETto. - An Org buffer — the rules below: follow the link, toggle the checkbox, flip the TODO.
- A Markdown buffer — follow the link at point.
- Anything else — whatever the mode itself binds
RETto, asked at the moment you press it. A command that would type or break a line is refused, soRETstays inert rather than inserting a newline in a buffer you are reading.
;; (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.
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.
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.
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.
If you use smartparens, one call improves C-g inside its overlays,
especially in a terminal:
(with-eval-after-load 'smartparens
(donkey-setup-smartparens))
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:
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.
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.
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.
! # & 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.
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:
| Key | Dired’s own binding | What you get with NORMAL state on |
|---|---|---|
j | dired-goto-file | dired-next-line |
k | dired-do-kill-lines | dired-previous-line |
u | dired-unmark | dired-undo |
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
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.
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:
iandaput you in INSERT state in a buffer you cannot type into — no error, no message, justDONKEY[I]in the modeline.C-gcomes back out.oandDmove point before they fail, so a mistyped key can lose your place in a long listing.
| Mode | Its letters are | So NORMAL state answered with |
|---|---|---|
magit-mode | commit, log, branch, diff, push, stash | change, forward-char, backward-word |
git-rebase-mode | pick, reword, drop, exec, break | yank, 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.
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 mode | On the list? | What matched it |
|---|---|---|
magit-log-mode | no | magit-mode |
shell-mode | no | comint-mode |
package-menu-mode | no | tabulated-list-mode |
magit-mode | yes | itself |
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.
Three reasons, in the order they matter.
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.
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.
;; 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.
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.
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
;;; 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
;; 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 on
(donkey-mode 1))
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.
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)
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.
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.
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:
xdeletes 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.
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.
h j k l | left, down, up, right |
w b | forward, back one word |
W B | forward, back one balanced expression |
J K | down, up one screen row |
S | back through where you have been |
: | go to line |
% | select the whole buffer |
z z | recentre |
g g | beginning of buffer | g e | end of buffer |
g h | beginning of line | g l | end of line |
g j | join lines | g q | fill region |
g Q | fill paragraph | G | end of buffer |
i | before point | a | after point |
I | line start | A | line end |
o | line below | O | line above |
c | change |
d x | delete selection or character | D | to end of line |
c | change | C | comment |
y | copy | p | paste |
P | paste rectangle | > | indent |
u | undo | U | redo |
. | repeat |
v | selection | V | visual line / line selection |
M | mark run | m v | rectangle |
m w | word | m W | symbol |
m b | word backward | m B | symbol backward |
m s | sentence | m S | sentence backward |
m p | paragraph | m P | paragraph backward |
m i | inside a pair | m a | pair and contents |
m I | inside a sexp | m A | sexp and contents |
m l | bank selection | m u | unbank last |
m U | unbank the run | m DEL | clear the bank |
r r | replace-regexp |
r q | query-replace |
z z | recentre |
SPC | your leader |
SPC i & | insert one digraph |
SPC i . | digraph input method |
SPC i - | input method off |
Nineteen keys — " $ ' ( ) * + / < = [ \ ] _
` { | } ~ — plus « » ‘ ’ “ ” ‹ ›. With a
selection they wrap it; press again with the pair around the selection
and it comes off.
, - ; answer “is undefined”. BACKSPACE and DELETE (under all
four of their key names) do nothing at all.
80 commands. These have no key and are reached with M-x:
| Command | Does |
|---|---|
donkey-mode | turn DONKEY on or off everywhere |
donkey-normal-mode | NORMAL state in this buffer |
donkey-insert-mode | INSERT state in this buffer |
donkey-enter-normal | enter NORMAL state |
donkey-tutor | the tutor buffer |
donkey-digraph | the digraph chart |
donkey-check-bindings | what has taken DONKEY’s keys |
donkey-debug-platform | what DONKEY sees on this machine |
donkey-version | the loaded version |
donkey-org-scratch | an *org-scratch* buffer |
donkey-org-todo | toggle a headline’s TODO state |
donkey-switch-other-buffer | back to the previous buffer |
donkey-mark-run-adopt | adopt the current selection into a mark run |
donkey-refresh-wrap-keys | claim wrap keys after changing the pair table |
donkey-refresh-suppressed-commands | re-apply donkey-self-insert-commands |
donkey-toggle-wrap-engine | switch between DONKEY and your pairing package |
donkey-setup-smartparens | improve C-g inside smartparens overlays |
donkey-add-denylist-entry | stop sending cursor escapes to a terminal |
donkey-remove-denylist-entry | undo 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:
| Key | Command |
|---|---|
m l | donkey-bank-selection |
c | donkey-change |
m <delete> | donkey-clear-banked-selection |
m <deletechar> | donkey-clear-banked-selection |
m DEL | donkey-clear-banked-selection |
C | donkey-comment-dwim |
y | donkey-copy |
d | donkey-delete |
x | donkey-delete |
? | donkey-describe-bindings |
SPC i - | donkey-disable-input-method |
<enter> | donkey-enter-dwim |
RET | donkey-enter-dwim |
: | donkey-goto-line |
> | donkey-indent-region-or-line |
SPC i . | donkey-input-method-digraphs |
a | donkey-insert-after |
I | donkey-insert-beginning-of-line |
SPC i & | donkey-insert-digraph |
A | donkey-insert-end-of-line |
i | donkey-insert-here |
g j | donkey-join-line |
S | donkey-jump-back |
m i | donkey-mark-inner |
m a | donkey-mark-outer |
m p | donkey-mark-paragraph |
m P | donkey-mark-paragraph-backward |
M | donkey-mark-run-toggle |
m s | donkey-mark-sentence |
m S | donkey-mark-sentence-backward |
m I | donkey-mark-sexp-inner |
m A | donkey-mark-sexp-outer |
m W | donkey-mark-symbol |
m B | donkey-mark-symbol-backward |
% | donkey-mark-whole-buffer |
m w | donkey-mark-word |
m b | donkey-mark-word-backward |
O | donkey-open-above |
o | donkey-open-below |
m v | donkey-rectangle-mark-mode |
U | donkey-redo |
v | donkey-set-mark |
m u | donkey-unbank-line |
m U | donkey-unbank-section |
V | donkey-visual-line-toggle |
J | donkey-visual-next-line |
K | donkey-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 |
p | donkey-yank |
P | donkey-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:
| Command | Key in a run |
|---|---|
donkey-mark-run-buffer-end | G |
donkey-mark-run-buffer-end | g e |
donkey-mark-run-buffer-start | g g |
donkey-mark-run-cancel | M |
donkey-mark-run-down | j |
donkey-mark-run-exchange | * |
donkey-mark-run-left | h |
donkey-mark-run-line-backward | K |
donkey-mark-run-line-end | g l |
donkey-mark-run-line-forward | J |
donkey-mark-run-line-start | g h |
donkey-mark-run-refuse | V |
donkey-mark-run-refuse | v |
donkey-mark-run-right | l |
donkey-mark-run-step-back | u |
donkey-mark-run-step-forward | U |
donkey-mark-run-up | k |
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.
| Option | Default | What it does |
|---|---|---|
donkey-excluded-modes | 13 modes, see above | where DONKEY steps aside completely |
donkey-support-modes | 41 sections, see above | what h and l do in a support mode |
donkey-key-packages | 1 package, prose | the keys a section takes by naming it |
donkey-support-mode-exceptions | occur-edit-mode | program-looking buffers you type in |
donkey-excluded-mode-exceptions | nil | modes that stay on under an excluded parent |
donkey-editing-modes | 7 broad modes | where RET does nothing |
donkey-position-ring-max | 10 | how far S remembers |
donkey-cursor-normal | box | cursor in NORMAL state |
donkey-cursor-insert | (bar . 2) | cursor in INSERT state |
donkey-cursor-support | box | cursor 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-mark-pair-delimiters | 21 pairs | what m i=/=m a select and the keys wrap |
donkey-wrap-delimiters | all | which of those pairs get a key |
donkey-wrap-region-engine | donkey | who puts the pair on: DONKEY or your pairing package |
donkey-self-insert-commands | 21 commands | mode commands that type, refused in NORMAL state |
donkey-input-methods | nil | your input methods under SPC i |
donkey-default-enter-rules-enabled | t | install the three built-in Enter rules |
donkey-report-binding-changes | t | say once at startup what has taken DONKEY’s keys |
donkey-digraph-line-spacing | 0 | air under each row of the digraph chart |
M-x customize-group donkey shows them all with their
documentation.
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.
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.
| Version | What it is |
|---|---|
29.1 | the minimum in Package-Requires |
30.1 | the previous stable series |
31.1 | the current stable release |
release-snapshot | the 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.