Start documenting new keyboard protocol

This commit is contained in:
Kovid Goyal 2021-01-11 11:28:42 +05:30
parent c8a9336160
commit b63ae10a09
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 291 additions and 89 deletions

263
docs/keyboard-protocol.rst Normal file
View File

@ -0,0 +1,263 @@
A protocol for comprehensive keyboard handling in terminals
=================================================================
There are various problems with the current state of keyboard handling. They
include:
* No way to use modifiers other than ``Ctrl`` and ``Alt``
* No way to reliably use multiple modifier keys, other than, ``Shift+Alt``.
* No way to handle different types of keyboard events, such as press, release or repeat
* No reliable way to distinguish single ``Esc`` keypresses from the start of a
escape sequence. Currently, client programs use fragile timing related hacks
for this, leading to bugs, for example:
`neovim #2035 <https://github.com/neovim/neovim/issues/2035>`_.
To solve these issues and others, kitty has created a new keyboard protocol,
that is backward compatible but allows applications to opt-in to support more
advanced usages. The protocol is based on initial work in `fixterms
<http://www.leonerd.org.uk/hacks/fixterms/>`_, however, it corrects various
issues in that proposal, namely:
* No way to disambiguate Esc keypresses, other than using 8-bit controls
which are undesirable for other reasons
* Incorrectly encoding shifted keys when shift modifier is used
* No way to not have :kbd:`Alt+letter` key presses generate escape codes that
conflict with other escape codes
* No way to specify both shifted and unshifted keys for robust shortcut
matching (think matching :kbd:`ctrl+shift+equal` and :kbd:`ctrl+plus`)
* No way to specify alternate layout key. This is useful for keyboard layouts
such as Cyrillic where you want the shortcut :kbd:`ctrl+c` to work when
pressing the :kbd:`ctrl+ц` on the keyboard.
* No way to report repeat and release key events, only key press events
* No way to report key events without text, useful for gaming. Think of using
the :kbd:`WASD` keys to control movement.
* A very small subset of all possible functional keys are specified.
A basic overview
------------------
Key events are divided into two types, those that produce text and those that
do not. When a key event produces text, the text is sent directly as UTF-8
encoded bytes. This is safe as UTF-8 contains no C0 control codes.
When the key event does not have text, the key event is encoded as an escape code. In
legacy compatibility mode (the default) this uses legacy escape codes, so old terminal
applications continue to work. Key events that could not be represented in
legacy mode are encoded using a ``CSI u`` escape code, that most terminal
programs should just ignore. For more advanced features, such as release/repeat
reporting etc., applications can tell the terminal they want this information by
sending an escape code to toggle the mode.
The central escape code used to encode key events is::
CSI unicode-key-code:alternate-key-codes ; modifiers:event-type u
Spaces in the above definition are present for clarity and should be ignored.
``CSI`` is the bytes ``0x1b 0x5b``. All parameters are decimal numbers. Fields
are separated by the semi-colon and sub-fields by the colon. Only the
``unicode-key-code`` field is mandatory, everything else is optional. The
escape code is terminated by the ``u`` character (the byte ``0x75``).
Key codes
~~~~~~~~~~~~~~
The ``unicode-key-code`` above is the Unicode codepoint representing the key, as a
decimal number. For example, the :kbd:`A` key is represented as ``97`` which is
the unicode code for lowercase ``a``. Note that the codepoint used is *always*
the lower-case (or more technically, un-shifted) version of the key. If the
user presses, for example, :kbd:`ctrl+shift+a` the escape code would be ``CSI
97;modifiers u``. It *must not* by ``CSI 65; modifiers u``.
If *alternate key reporting* is requested by the program running in the
terminal, the terminal can send two additional Unicode codepoints, the
*shifted key* and *base layout key*, separated by colons.
The shifted key is simply the upper-case version of ``unicode-codepoint``, or
more technically, the shifted version. So `a` becomes `A` and so on, based on
the current keyboard layout. This is needed to be able to match against a
shortcut such as :kbd:`ctrl+plus` which depending on the type of keyboard could
be either :kbd:`ctrl+shift+equal` or :kbd:`ctrl+plus`.
The *base layout key* is the key corresponding to the physical key in the
standard PC-101 key layout. So for example, if the user is using a Cyrillic
keyboard with a Cyrillic keyboard layout pressing the :kbd:`ctrl+ц` key will
be :kbd:`ctrl+c` in the standard layout. So the terminal should send the *base
layout key* as ``99`` corresponding to the ``c`` key.
If only one alternate key is present, it is the *shifted key* if the terminal
wants to send only a base layout key but no shifted key, it must use an empty
sub-field for the shifted key, like this::
CSI unicode-key-code::base-layout-key
Modifiers
~~~~~~~~~~~~~~
This protocol supports four modifier keys, :kbd:`shift, alt, ctrl and super`.
Here super is either the *Windows/Linux* key or the *Cmd* key on mac keyboards.
Modifiers are encoded as a bit field with::
shift 0b1 (1)
alt 0b10 (2)
ctrl 0b100 (4)
super 0b1000 (8)
In the escape code, the modifier value is encoded as a decimal number which is
``1 + actual modifiers``. So to represent :kbd:`shift` only, the value would be ``1 +
1 = 2``, to represent :kbd:`ctrl+shift` the value would be ``1 + 0b101 = 5``
and so on. If the modifier field is not present in the escape code, its default
value is ``1`` which means no modifiers.
Event types
~~~~~~~~~~~~~~~~
There are three key event types: ``press, repeat and release``. They are
reported (if requested) as a sub-field of the modifiers field (separated by a
colon). If no modifiers are present, the modifiers field must have the value
``1`` and the event type sub-field the type of event. The ``press`` event type
has value ``1`` and is the default if no event type sub field is present. The
``repeat`` type is ``2`` and the ``release`` type is ``3``. So for example::
CSI key-code;1 # this is a press event
CSI key-code;1:1 # this is a press event
CSI key-code;1:2 # this is a repeat event
CSI key-code:1:3 # this is a release event
.. note:: Key events that result in text are reported as plain UTF-8 text, so
events are not supported for them, unless the application requests key
report mode, see below.
Non-Unicode keys
~~~~~~~~~~~~~~~~~~~~~~~
There are many keys that don't correspond to letters from human languages, and
thus aren't represented in Unicode. Think of functional keys, such as
:kbd:`Escape, Play, Pause, F1, Home, etc`. These are encoded using Unicode code
points from the Private Use Area (``0xe000 - 0xf8ff``). The mapping of key
names to code points for these keys is in the
:ref:`Functional key definition table below <functional>`.
.. _functional:
Functional key definitions
----------------------------
.. start functional key table (auto generated by gen-key-constants.py do not edit)
.. csv-table:: Functional key codes
:header: "Name", "Codepoint (base-16)"
"ESCAPE", "E000"
"ENTER", "E001"
"TAB", "E002"
"BACKSPACE", "E003"
"INSERT", "E004"
"DELETE", "E005"
"LEFT", "E006"
"RIGHT", "E007"
"UP", "E008"
"DOWN", "E009"
"PAGE_UP", "E00A"
"PAGE_DOWN", "E00B"
"HOME", "E00C"
"END", "E00D"
"CAPS_LOCK", "E00E"
"SCROLL_LOCK", "E00F"
"NUM_LOCK", "E010"
"PRINT_SCREEN", "E011"
"PAUSE", "E012"
"MENU", "E013"
"F1", "E014"
"F2", "E015"
"F3", "E016"
"F4", "E017"
"F5", "E018"
"F6", "E019"
"F7", "E01A"
"F8", "E01B"
"F9", "E01C"
"F10", "E01D"
"F11", "E01E"
"F12", "E01F"
"F13", "E020"
"F14", "E021"
"F15", "E022"
"F16", "E023"
"F17", "E024"
"F18", "E025"
"F19", "E026"
"F20", "E027"
"F21", "E028"
"F22", "E029"
"F23", "E02A"
"F24", "E02B"
"F25", "E02C"
"F26", "E02D"
"F27", "E02E"
"F28", "E02F"
"F29", "E030"
"F30", "E031"
"F31", "E032"
"F32", "E033"
"F33", "E034"
"F34", "E035"
"F35", "E036"
"KP_0", "E037"
"KP_1", "E038"
"KP_2", "E039"
"KP_3", "E03A"
"KP_4", "E03B"
"KP_5", "E03C"
"KP_6", "E03D"
"KP_7", "E03E"
"KP_8", "E03F"
"KP_9", "E040"
"KP_DECIMAL", "E041"
"KP_DIVIDE", "E042"
"KP_MULTIPLY", "E043"
"KP_SUBTRACT", "E044"
"KP_ADD", "E045"
"KP_ENTER", "E046"
"KP_EQUAL", "E047"
"KP_SEPARATOR", "E048"
"KP_LEFT", "E049"
"KP_RIGHT", "E04A"
"KP_UP", "E04B"
"KP_DOWN", "E04C"
"KP_PAGE_UP", "E04D"
"KP_PAGE_DOWN", "E04E"
"KP_HOME", "E04F"
"KP_END", "E050"
"KP_INSERT", "E051"
"KP_DELETE", "E052"
"LEFT_SHIFT", "E053"
"LEFT_CONTROL", "E054"
"LEFT_ALT", "E055"
"LEFT_SUPER", "E056"
"RIGHT_SHIFT", "E057"
"RIGHT_CONTROL", "E058"
"RIGHT_ALT", "E059"
"RIGHT_SUPER", "E05A"
"MEDIA_PLAY", "E05B"
"MEDIA_PAUSE", "E05C"
"MEDIA_PLAY_PAUSE", "E05D"
"MEDIA_REVERSE", "E05E"
"MEDIA_STOP", "E05F"
"MEDIA_FAST_FORWARD", "E060"
"MEDIA_REWIND", "E061"
"MEDIA_TRACK_NEXT", "E062"
"MEDIA_TRACK_PREVIOUS", "E063"
"MEDIA_RECORD", "E064"
"LOWER_VOLUME", "E065"
"RAISE_VOLUME", "E066"
"MUTE_VOLUME", "E067"
.. end functional key table

View File

@ -69,91 +69,9 @@ of this protocol to enable drawing of arbitrary raster images in the terminal.
Keyboard handling Keyboard handling
------------------- -------------------
There are various problems with the current state of keyboard handling. They kitty has a :doc:`keyboard protocol <keyboard-protocol>` for reporting key
include: presses to terminal applications that solves all key handling issues in
terminal applications.
* No way to use modifiers other than ``Ctrl`` and ``Alt``
* No way to reliably use multiple modifier keys, other than, ``Shift+Alt``.
* No way to handle different types of keyboard events, such as press, release or repeat
* No reliable way to distinguish single ``Esc`` keypresses from the start of a
escape sequence. Currently, client programs use fragile timing related hacks
for this, leading to bugs, for example:
`neovim #2035 <https://github.com/neovim/neovim/issues/2035>`_.
There are already two distinct keyboard handling modes, *normal mode* and
*application mode*. These modes generate different escape sequences for the
various special keys (arrow keys, function keys, home/end etc.) Most terminals
start out in normal mode, however, most shell programs like ``bash`` switch them to
application mode. We propose adding a third mode, named *full mode* that addresses
the shortcomings listed above.
Switching to the new *full mode* is accomplished using the standard private
mode DECSET escape sequence::
<ESC>[?2017h
and to leave *full mode*, use DECRST::
<ESC>[?2017l
The number ``2017`` above is not used for any existing modes, as far as I know.
Client programs can query if the terminal emulator is in *full mode* by using
the standard `DECRQM <https://vt100.net/docs/vt510-rm/DECRQM.html>`_ escape sequence.
The new mode works as follows:
* All printable key presses without modifier keys are sent just as in the
*normal mode*. This means all printable ASCII characters and in addition,
``Enter``, ``Space`` and ``Backspace``. Also any unicode characters generated by
platform specific extended input modes, such as using the ``AltGr`` key. This
is done so that client programs that are not aware of this mode can still
handle basic text entry, so if a *full mode* using program crashes and does
not reset, the user can still issue a ``reset`` command in the shell to restore
normal key handling. Note that this includes pressing the ``Shift`` modifier
and printable keys. Note that this means there are no repeat and release
events for these keys and also for the left and right shift keys.
* For non printable keys and key combinations including one or more modifiers,
an escape sequence encoding the key event is sent. For details on the
escape sequence, see below.
The escape sequence encodes the following properties:
* Type of event: ``press,repeat,release``
* Modifiers pressed at the time of the event
* The actual key being pressed
Schematically::
<ESC>_K<type><modifiers><key><ESC>\
Where ``<type>`` is one of ``p`` -- press, ``r`` -- release and ``t`` -- repeat.
Modifiers is a bitmask represented as a single base64 digit. Shift -- ``0x1``,
Alt -- ``0x2``, Control -- ``0x4`` and Super -- ``0x8``. ``<key>`` is a number
(encoded in base85) corresponding to the key pressed. The key name to number
mapping is defined in :doc:`this table <key-encoding>`.
Client programs must ignore events for keys they do not know. The mapping in
the above table is stable and will never change, however, new codes might be
added to it in the future, for new keys.
For example::
<ESC>_KpGp<ESC>\ is <Ctrl>+<Alt>+x (press)
<ESC>_KrP8<ESC>\ is <Ctrl>+<Alt>+<Shift>+<Super>+PageUp (release)
This encoding means each key event is represented by 8 or 9 printable ascii
only bytes, for maximum robustness.
To see the full mode in action, run::
kitty +kitten key_demo
Support for this mode is indicated by the ``fullkbd`` boolean capability
in the terminfo database, in case querying for it via DECQRM is inconvenient.
.. _ext_styles: .. _ext_styles:

View File

@ -5,7 +5,7 @@
from typing import Dict, List from typing import Dict, List
functional_key_defs = '''\ functional_key_defs = '''# {{{
# kitty XKB macOS # kitty XKB macOS
escape Escape - escape Escape -
enter Return - enter Return -
@ -111,7 +111,8 @@ media_record XF86AudioRecord -
lower_volume XF86AudioLowerVolume - lower_volume XF86AudioLowerVolume -
raise_volume XF86AudioRaiseVolume - raise_volume XF86AudioRaiseVolume -
mute_volume XF86AudioMute - mute_volume XF86AudioMute -
''' ''' # }}}
functional_key_names: List[str] = [] functional_key_names: List[str] = []
name_to_code: Dict[str, int] = {} name_to_code: Dict[str, int] = {}
name_to_xkb: Dict[str, str] = {} name_to_xkb: Dict[str, str] = {}
@ -135,8 +136,14 @@ def patch_file(path: str, what: str, text: str, start_marker: str = '/* ', end_m
with open(path, 'r+') as f: with open(path, 'r+') as f:
raw = f.read() raw = f.read()
try:
start = raw.index(start_q) start = raw.index(start_q)
except ValueError:
raise SystemExit(f'Failed to find "{start_q}" in {path}')
try:
end = raw.index(end_q) end = raw.index(end_q)
except ValueError:
raise SystemExit(f'Failed to find "{end_q}" in {path}')
raw = raw[:start] + start_q + '\n' + text + '\n' + raw[end:] raw = raw[:start] + start_q + '\n' + text + '\n' + raw[end:]
f.seek(0) f.seek(0)
f.truncate(0) f.truncate(0)
@ -164,9 +171,23 @@ def generate_xkb_mapping() -> None:
patch_file('glfw/xkb_glfw.c', 'glfw to xkb', '\n'.join(rlines)) patch_file('glfw/xkb_glfw.c', 'glfw to xkb', '\n'.join(rlines))
def generate_functional_table() -> None:
lines = [
'',
'.. csv-table:: Functional key codes',
' :header: "Name", "Codepoint (base-16)"',
''
]
for name, code in name_to_code.items():
lines.append(f' "{name.upper()}", "{code:X}"')
lines.append('')
patch_file('docs/keyboard-protocol.rst', 'functional key table', '\n'.join(lines), start_marker='.. ', end_marker='')
def main() -> None: def main() -> None:
generate_glfw_header() generate_glfw_header()
generate_xkb_mapping() generate_xkb_mapping()
generate_functional_table()
if __name__ == '__main__': if __name__ == '__main__':