Terminals are weird. If you type read -r
in a shell then hold Alt
and tap a, you may be surprised to see ^[a
glaring at you.
Why?
Your terminal encodes Alt-a (referred to as <m-a>
in vim) as two bytes:
Escape (referred to as <esc>
in vim) followed by a
. This encoding scheme has
been used for decades and allows your terminal to send escape sequences like
this to remote machines.
Therefore, when you type <m-a>
in vim, things do not work as expected. If you
try to map <m-a>
, it will not work out of the box. This is exactly what Anna,
the expert vim user, discovered in her Vim journey.
Anna saw mention of meta sequences on some stackoverflow post, and she has the
perfect use-case. She wants to map <m-m>
(Meta m) to run :sil make | redraw!
.
So, she does just that to test:
:nnoremap <m-m> :sil make <bar> redraw!<cr>
Pleased that she remembered |
needed to be <bar>
in the map and that she
needed to add <cr>
at the end so vim would execute her mapping, she held
Alt and tapped m to issue Meta m, and ...
It didn't work.
Confused, Anna tried mapping another meta sequence.
:inoremap <m-a> <esc>:echo 'Meta a'<cr>gi
she typed furiously and pressed Alta forcibly after entering INSERT mode only to find that, once again, her efforts were futile.
Suddenly, an idea came to Anna. "Meta a and Escape a are basically the same thing!" she exclaimed aloud. "If I map Escape a, when I type Alt a, the mapping will be called!"
:inoremap <esc>a <esc>:echo 'Meta a! Ha!'<cr>gi
she wrote and was extremely happy when she saw Meta a! Ha!
printed at the
bottom of the screen when she pressed Alta
However, it didn't take her long to notice the elephant in the room: every time she pressed Escape in INSERT mode, vim seemed to stay in INSERT mode for an entire second more!
"Why?" she exclaimed in a frustrated tone.
After some reading, Anna found something about 'timeout' and 'timeoutlen' on
stackoverflow and promptly opened :help 'timeout'
in vim, as any good vim user
would do. There, she discovered the table staring ominously at her:
'timeout' | 'ttimeout' | action |
---|---|---|
off | off | do not time out |
on | on or off | time out on :mappings and key codes |
off | on | time out on key codes |
She realized the issue. Because she mapped <esc>a
, vim was waiting for
'timeout' milliseconds before timing out her map and issuing <esc>
like she
typed. Indeed, if she typed Esc then a fast enough in
INSERT mode, then she saw the once happy Meta a! Ha!
message and realized
that this would cause serious harm to her workflow.
After some experimentation, she concluded that this was not a good idea. If she
decreased 'timeoutlen' then it was difficult to issue multi-key builtin maps or
her own maps that began with <leader>
, and Anna had many of these maps that
sped up her daily chores.
At a loss, Anna decided not to use any meta maps and told a few of her friends, "Vim sucks at mapping meta keys. It just isn't possible."
No one knows what happened to Anna. Maybe she still has a meta-free vimrc. Maybe she switched to neovim, which has "first class" meta support.
However, you can map meta sequences in Vim without any drawbacks! Keep reading to see how.
First, you need to understand that vim can receive multiple bytes but treat this
as a single "key". When you type Alta, the terminal
encodes this as <esc>a
(two bytes, 27 and 97), then Vim receives these two
bytes. If you don't tell vim that <esc>a
is a normal key, it will treat this
as two separate keys. In order to tell vim that <esc>
followed rapidly by a
is a normal key, just do the following:
:execute "set <m-a>=\<esc>a"
This tells vim "Escape followed by 'a' is a special code that should be
interpreted as the normal key, Meta a". See :help :set-termcap
for more
information.
Note that "\<esc>"
(need double quotes) is translated into the byte for
<esc>
(ASCII 27).
Now, we can map <m-a>
without any consequences:
:nnoremap <m-a> :echo 'Meta a works PERFECTLY!'<cr>
:inoremap <m-a> <esc>:echo 'Meta a works PERFECTLY!'<cr>gi
After issuing Alta, you can see that Meta a works
perfectly, and there is no longer the issue with pressing <esc>
in INESRT
mode.
Hooray!
Note that if you only map <esc>{key}
instead of <m-{key}>
in NORMAL mode,
then there should not be an issue, but mapping <m-{key}>
is the proper way to
achieve this, and it won't break elsewhere.
In order to map any meta sequence, you can put this anywhere in your vimrc:
if !has('nvim') && !has('gui_running')
" set up Meta to work properly for most keys in terminal vim
" NOTE: these do not work: <m-space>,<m->>,<m-[>,<m-{up,down,left,right}>
" NOTE: <m-@>,<m-O> only work in xterm and gvim - not st, urxvt, etc
" NOTE: map <m-\|> or <m-bar>
for ord in range(33,61)+range(63,90)+range(92,126)
let char = ord is 34 ? '\"' : ord is 124 ? '\|' : nr2char(ord)
exec printf("set <m-%s>=\<esc>%s", char, char)
if exists(':tnoremap') " fix terminal control sequences
exec printf("tnoremap <silent> <m-%s> <esc>%s", char, char)
endif
endfor
" set up <c-left> and <c-right> properly
" NOTE: if below don't work, compare with ctrl-v + CTRL-{LEFT,RIGHT} in INSERT mode
" NOTE: <c-up>,<c-down> do not work in any terminal
exe "set <c-right>=\<esc>[1;5C"
exe "set <c-left>=\<esc>[1;5D"
endif
There is only one sequence that requires special notation when mapping. In order
to map |
, you need to use something like :nnoremap <m-bar> :echo 'M-bar!'<cr>
or :nnoremap <m-\|> :echo 'M-bar!'<cr>
. Every other meta
sequence (even <m-">
) works as it is.
Note that some keys just do not work at all in a terminal; the broken keys are as follows:
<m-@>
(not possible toset
)<m-O>
(capital "Oh"; why doesn't this work? I don't know)<m->>
or<m-\>>
(not possible toset
)<m-[>
(causes issues)<m-space>
(breaks all other meta sequences)<m-up>
<m-down>
<m-right>
<m-left>
You can also test the above code with some vim-script:
sil update
let [s:lhs,s:rhs] = [{'|':'bar'}, {'|':'bar', "'":"''"}]
for ord in range(33,61)+range(63,90)+range(92,126)
let char = nr2char(ord)
let [l,r] = [get(s:lhs, char, char), get(s:rhs, char, char)]
exe printf("nnore <m-%s> :echo 'M-%s PRESSED'<cr>", l, r)
exe printf("nnore <f20><m-%s> :let g:lastmap = '%s'<cr>", l, r)
endfor
for ord in range(33,61)+range(63,90)+range(92,126)
let char = nr2char(ord)
sil call feedkeys("\<f20>\<esc>".char, 'Tx')
if get(g:, 'lastmap') != get(s:lhs, char, char)
echom 'Broken' '<m-'.char.'>'
endif
endfor
if &mod | sil earlier 1f | endif
Paste that into, for example, /tmp/test_meta.vim
, run :source %
, and then
type :messages
. It will show you which keys that could be set
do not work
properly by injecting keys in a low-level way using feedkeys()
.
You can also issues meta sequences manually to test. For example, if you press
Alt#, you should see M-# PRESSED
echoed to the
command-line.
If you get lost or don't feel like testing this out but are dubious, here is a gif:
When you press <m-a>
, the terminal encodes it as <esc>a
(two bytes).
Tell vim that <esc>a
is the "key sequence" <m-a>
by issuing:
:execute "set <m-a>=\<esc>a"
To map any meta sequence (except <m-space>
, <m-@>
, <m-[
, and <m-O
), add
this to your vimrc:
if !has('nvim') && !has('gui_running')
" set up Meta to work properly for most keys in terminal vim
" NOTE: these do not work: <m-space>,<m->>,<m-[>,<m-{up,down,left,right}>
" NOTE: <m-@>,<m-O> only work in xterm and gvim - not st, urxvt, etc
" NOTE: map <m-\|> or <m-bar>
for ord in range(33,61)+range(63,90)+range(92,126)
let char = ord is 34 ? '\"' : ord is 124 ? '\|' : nr2char(ord)
exec printf("set <m-%s>=\<esc>%s", char, char)
if exists(':tnoremap') " fix terminal control sequences
exec printf("tnoremap <silent> <m-%s> <esc>%s", char, char)
endif
endfor
" set up <c-left> and <c-right> properly
" NOTE: if below don't work, compare with ctrl-v + CTRL-{LEFT,RIGHT} in INSERT mode
" NOTE: <c-up>,<c-down> do not work in any terminal
exe "set <c-right>=\<esc>[1;5C"
exe "set <c-left>=\<esc>[1;5D"
endif
Example map: nnoremap <m-a> :echo 'M-a works!'<cr>
Note that you need to use <m-bar>
(or <m-\|>
) for <bar>
but everything
else is normal (<m-">
, <m-#>
, etc).
Read :help 'timeout'
and below sections yourself to better understand how
these options work.
Read :help :set-termcap
to read more about translating "special codes" into
"normal keys".