Skip to content

Latest commit

 

History

625 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.

Thirteen lessons, ending with the two the keyboard itself cannot give you — wrapping a selection, and the characters that are not on it:

  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)

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.

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 and V

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.

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 — see Long lines above — also serves as a line-wise selection when a selection is what you are after.

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 Pstill 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. Rectangles work with the wrap keys and with banking, below.

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.

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 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.

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.

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.

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.

Giving a key back to the buffer

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.

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-modevisit the buffer
speedbar-modeup a directoryopen the file or enter the directory
Man-modeprevious sectionnext section
woman-modefollow
apropos-modefollow
image-modeprevious filenext file
doc-view-modeprevious pagenext page
tar-modeextract the entry
archive-modeextract the entry
Custom-modeup to the parent group
occur-modego to the occurrence
compilation-modego to the error (covers grep-mode)
emacs-lisp-compilation-modego to the error
package-menu-modedescribe the package
Buffer-menu-modevisit the buffer
org-agenda-modea week earliera week later
bookmark-bmenu-modejump to the bookmark
vc-dir-modevisit the file
proced-moderefine by the field at point
profiler-report-modeexpand or collapse the entry
xref--xref-buffer-modego to the reference
finder-modeselect
flymake-diagnostics-buffer-modego to the diagnostic
ebrowse-member-modego 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-redisplayg, 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.

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. 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

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:

  1. An org-agenda buffer — whatever the agenda binds RET to.
  2. An Org buffer — the rules below: follow the link, toggle the checkbox, flip the TODO.
  3. A Markdown buffer — follow the link at point.
  4. Anything else — 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.

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.

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

  ;;; 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))

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 screen row
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-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-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-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