Add tests for legacy letter key encodings

This commit is contained in:
Kovid Goyal 2021-01-13 08:31:52 +05:30
parent 78d45eb161
commit c519013b20
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 466 additions and 21 deletions

View File

@ -1,12 +1,16 @@
A protocol for comprehensive keyboard handling in terminals
=================================================================
There are various problems with the current state of keyboard handling. They
include:
There are various problems with the current state of keyboard handling in
terminals. They include:
* No way to use modifiers other than ``Ctrl`` and ``Alt``
* 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 reliably use multiple modifier keys, other than, ``shift+alt`` and
``ctrl+alt``.
* Many of the existing escape codes used to encode these events are ambiguous
with different key presses mapping to the same escape code.
* No way to handle different types of keyboard events, such as press, release or repeat
@ -22,6 +26,8 @@ advanced usages. The protocol is based on initial work in `fixterms
issues in that proposal, listed at the :ref:`bottom of this document
<fixterms_bugs>`.
.. versionadded:: 0.20.0
A basic overview
------------------
@ -302,6 +308,34 @@ distinguish these, use the :ref:`disambiguate <disambiguate>` flag.
Legacy text keys
~~~~~~~~~~~~~~~~~~~
For legacy compatibility, the keys
:kbd:`a-z 0-9 \` - = [ ] \ ; ' , . /` with the modifiers
:kbd:`shift, alt, ctrl, shift+alt, ctrl+alt` are output using the
following algorithm:
#. If the :kbd:`alt` key is pressed output the byte for ``ESC (0x1b)``
#. If the :kbd:`ctrl` modifier is pressed mask the seventh bit ``(& 0b111111)`` in the key's ASCII code number and output that
#. Otherwise, if the :kbd:`shift` modifier is pressed, output the shifted key, for example, ``A`` for ``a`` and ``$`` for ``4``.
#. Otherwise, output the key unmodified
Additionally, :kbd:`ctrl+space` is output as the NULL byte ``(0x0)``.
Any other combination of modifiers with these keys is output as the appropriate
``CSI u`` escape code.
.. csv-table:: Example encodings
:header: "Key", "Plain", "shift", "alt", "ctrl", "shift+alt", "alt+ctrl", "ctrl+shift"
"i", "i (105)", "I (73)", "ESC i", ") (41)", "ESC I", "ESC )", "CSI 105; 6 u"
"3", "3 (51)", "# (35)", "ESC 3", "3 (51)", "ESC #", "ESC 3", "CSI 51; 6 u"
";", "; (59)", ": (58)", "ESC ;", "; (59)", "ESC :", "ESC ;", "CSI 59; 6 u"
.. note::
Many of the legacy escape codes are ambiguous with multiple different key
presses yielding the same escape code(s), for example, :kbd:`ctrl+i` is the
same as :kbd:`tab`, :kbd:`ctrl+m` is the same as :kbd:`Enter`, :kbd:`alt+[ 2
shift+\`` is the same :kbd:`Insert`, etc. To resolve these use the
:ref:`disambiguate progressive enhancement <disambiguate>`.
.. _functional:

View File

@ -2,9 +2,9 @@
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import string
from typing import Dict, List
functional_key_defs = '''# {{{
# kitty XKB macOS
escape Escape -
@ -113,6 +113,8 @@ raise_volume XF86AudioRaiseVolume -
mute_volume XF86AudioMute -
''' # }}}
shift_map = {x[0]: x[1] for x in '`~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ [{ ]} \\| ;: \'" ,< .> /?'.split()}
shift_map.update({x: x.upper() for x in string.ascii_lowercase})
functional_encoding_overrides = {
'insert': 2, 'delete': 3, 'page_up': 5, 'page_down': 6,
'home': 7, 'end': 8, 'tab': 9, 'f1': 11, 'f2': 12, 'enter': 13, 'f4': 14,
@ -220,10 +222,46 @@ def generate_functional_table() -> None:
patch_file('kitty/key_encoding.c', 'special numbers', '\n'.join(enc_lines))
def generate_legacy_text_key_maps() -> None:
tests = []
tp = ' ' * 8
shift, alt, ctrl = 1, 2, 4
lines = []
for c, s in shift_map.items():
if c in '\\\'':
c = '\\' + c
lines.append(f" case '{c}': return '{s}';")
patch_file('kitty/key_encoding.c', 'shifted key map', '\n'.join(lines))
def simple(c: str) -> None:
shifted = shift_map.get(c, c)
ctrled = chr(ord(c) & 0b111111)
for m in range(16):
if m == 0:
tests.append(f'{tp}ae(enc(ord({c!r})), {c!r})')
elif m == shift:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=shift), {shifted!r})')
elif m == alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=alt), "\\x1b" + {c!r})')
elif m == ctrl:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=ctrl), {ctrled!r})')
elif m == shift | alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=shift | alt), "\\x1b" + {shifted!r})')
elif m == ctrl | alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=ctrl | alt), "\\x1b" + {ctrled!r})')
for k in shift_map:
simple(k)
patch_file('kitty_tests/keys.py', 'legacy letter tests', '\n'.join(tests), start_marker='# ', end_marker='')
def main() -> None:
generate_glfw_header()
generate_xkb_mapping()
generate_functional_table()
generate_legacy_text_key_maps()
if __name__ == '__main__':

View File

@ -138,6 +138,7 @@ encode_function_key(const KeyEvent *ev, char *output) {
}
}
if (ev->mods.value == SHIFT && key_number == GLFW_FKEY_TAB) { SIMPLE("\x1b[Z"); }
if (ev->mods.value == CTRL && key_number == GLFW_FKEY_BACKSPACE) { SIMPLE("\x08"); }
#undef SIMPLE
#define S(number, trailer) key_number = number; csi_trailer = trailer; break
@ -180,29 +181,105 @@ encode_function_key(const KeyEvent *ev, char *output) {
return serialize(&ed, output, csi_trailer);
}
static inline char
shifted_ascii_key(const uint32_t key) {
switch(key) {
/* start shifted key map (auto generated by gen-key-constants.py do not edit) */
case '`': return '~';
case '1': return '!';
case '2': return '@';
case '3': return '#';
case '4': return '$';
case '5': return '%';
case '6': return '^';
case '7': return '&';
case '8': return '*';
case '9': return '(';
case '0': return ')';
case '-': return '_';
case '=': return '+';
case '[': return '{';
case ']': return '}';
case '\\': return '|';
case ';': return ':';
case '\'': return '"';
case ',': return '<';
case '.': return '>';
case '/': return '?';
case 'a': return 'A';
case 'b': return 'B';
case 'c': return 'C';
case 'd': return 'D';
case 'e': return 'E';
case 'f': return 'F';
case 'g': return 'G';
case 'h': return 'H';
case 'i': return 'I';
case 'j': return 'J';
case 'k': return 'K';
case 'l': return 'L';
case 'm': return 'M';
case 'n': return 'N';
case 'o': return 'O';
case 'p': return 'P';
case 'q': return 'Q';
case 'r': return 'R';
case 's': return 'S';
case 't': return 'T';
case 'u': return 'U';
case 'v': return 'V';
case 'w': return 'W';
case 'x': return 'X';
case 'y': return 'Y';
case 'z': return 'Z';
/* end shifted key map */
default:
return 0;
}
}
static int
encode_printable_ascii_key_legacy(const KeyEvent *ev, char *output) {
if (!ev->mods.value) return snprintf(output, KEY_BUFFER_SIZE, "%c", (char)ev->key);
if (ev->disambiguate) return 0;
char shifted_key = 0;
if ('a' <= ev->key && ev->key <= 'z') shifted_key = ev->key + ('A' - 'a');
switch(ev->key) {
#define S(which, val) case which: shifted_key = val; break;
S('0', ')') S('9', '(') S('8', '*') S('7', '&') S('6', '^') S('5', '%') S('4', '$') S('3', '#') S('2', '@') S('1', '!')
S('`', '~') S('-', '_') S('=', '+') S('[', '{') S(']', '}') S('\\', '|') S(';', ':') S('\'', '"') S(',', '<') S('.', '>') S('/', '?')
#undef S
}
char shifted_key = shifted_ascii_key(ev->key);
shifted_key = (shifted_key && ev->mods.shift) ? shifted_key : (char)ev->key;
if (ev->mods.value == SHIFT)
return snprintf(output, KEY_BUFFER_SIZE, "%c", shifted_key);
if ((ev->mods.value == ALT || ev->mods.value == (SHIFT | ALT)))
return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", shifted_key);
if (ev->mods.value == CTRL && ev->key && 0x1f != ev->key)
return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x1f);
if (ev->mods.value == CTRL)
return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x3f);
if (ev->mods.value == (CTRL | ALT))
return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", ev->key & 0x1f);
return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", ev->key & 0x3f);
return 0;
}
static inline bool
is_legacy_ascii_key(uint32_t key) {
START_ALLOW_CASE_RANGE
switch (key) {
case 'a' ... 'z':
case '0' ... '9':
case '`':
case '-':
case '=':
case '[':
case ']':
case '\\':
case ';':
case '\'':
case ',':
case '.':
case '/':
return true;
default:
return false;
}
END_ALLOW_CASE_RANGE
}
static int
encode_key(const KeyEvent *ev, char *output) {
if (!ev->report_all_event_types && ev->action == RELEASE) return 0;
@ -211,9 +288,14 @@ encode_key(const KeyEvent *ev, char *output) {
init_encoding_data(&ed, ev);
bool simple_encoding_ok = !ed.add_actions && !ed.add_alternates;
if (32 <= ev->key && ev->key <= 126 && simple_encoding_ok) {
if (simple_encoding_ok) {
if (is_legacy_ascii_key(ev->key)) {
int ret = encode_printable_ascii_key_legacy(ev, output);
if (ret > 0) return ret;
} else if (ev->key == ' ' && ev->mods.value == CTRL) {
output[0] = 0;
return 1;
}
}
if (simple_encoding_ok && !ed.has_mods) return encode_utf8(ev->key, output);

View File

@ -169,7 +169,7 @@ pyencode_key_for_tty(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
char output[KEY_BUFFER_SIZE+1] = {0};
int num = encode_glfw_key_event(&ev, cursor_key_mode, key_encoding_flags, output);
if (num == SEND_TEXT_TO_CHILD) return PyUnicode_FromString(text);
return PyUnicode_FromString(output);
return PyUnicode_FromStringAndSize(output, MAX(0, num));
}
static PyMethodDef module_methods[] = {

View File

@ -48,7 +48,7 @@ class TestKeys(BaseTest):
mods_test(defines.GLFW_FKEY_ENTER, '\x0d', alt='\033\x0d', csi_num=ord('\r'))
mods_test(defines.GLFW_FKEY_KP_ENTER, '\x0d', alt='\033\x0d', csi_num=ord('\r'))
mods_test(defines.GLFW_FKEY_ESCAPE, '\x1b', alt='\033\033', csi_num=27)
mods_test(defines.GLFW_FKEY_BACKSPACE, '\x7f', alt='\033\x7f', csi_num=127)
mods_test(defines.GLFW_FKEY_BACKSPACE, '\x7f', alt='\033\x7f', ctrl='\x08', csi_num=127)
mods_test(defines.GLFW_FKEY_TAB, '\t', alt='\033\t', shift='\x1b[Z', csi_num=ord('\t'))
mods_test(defines.GLFW_FKEY_INSERT, csi_num=2, trailer='~')
mods_test(defines.GLFW_FKEY_KP_INSERT, csi_num=2, trailer='~')
@ -82,6 +82,297 @@ class TestKeys(BaseTest):
mods_test(defines.GLFW_FKEY_LEFT, csi_num=1, trailer='D')
mods_test(defines.GLFW_FKEY_KP_LEFT, csi_num=1, trailer='D')
# legacy key tests {{{
# start legacy letter tests (auto generated by gen-key-constants.py do not edit)
ae(enc(ord('`')), '`')
ae(enc(ord('`'), mods=shift), '~')
ae(enc(ord('`'), mods=alt), "\x1b" + '`')
ae(enc(ord('`'), mods=shift | alt), "\x1b" + '~')
ae(enc(ord('`'), mods=ctrl), ' ')
ae(enc(ord('`'), mods=ctrl | alt), "\x1b" + ' ')
ae(enc(ord('1')), '1')
ae(enc(ord('1'), mods=shift), '!')
ae(enc(ord('1'), mods=alt), "\x1b" + '1')
ae(enc(ord('1'), mods=shift | alt), "\x1b" + '!')
ae(enc(ord('1'), mods=ctrl), '1')
ae(enc(ord('1'), mods=ctrl | alt), "\x1b" + '1')
ae(enc(ord('2')), '2')
ae(enc(ord('2'), mods=shift), '@')
ae(enc(ord('2'), mods=alt), "\x1b" + '2')
ae(enc(ord('2'), mods=shift | alt), "\x1b" + '@')
ae(enc(ord('2'), mods=ctrl), '2')
ae(enc(ord('2'), mods=ctrl | alt), "\x1b" + '2')
ae(enc(ord('3')), '3')
ae(enc(ord('3'), mods=shift), '#')
ae(enc(ord('3'), mods=alt), "\x1b" + '3')
ae(enc(ord('3'), mods=shift | alt), "\x1b" + '#')
ae(enc(ord('3'), mods=ctrl), '3')
ae(enc(ord('3'), mods=ctrl | alt), "\x1b" + '3')
ae(enc(ord('4')), '4')
ae(enc(ord('4'), mods=shift), '$')
ae(enc(ord('4'), mods=alt), "\x1b" + '4')
ae(enc(ord('4'), mods=shift | alt), "\x1b" + '$')
ae(enc(ord('4'), mods=ctrl), '4')
ae(enc(ord('4'), mods=ctrl | alt), "\x1b" + '4')
ae(enc(ord('5')), '5')
ae(enc(ord('5'), mods=shift), '%')
ae(enc(ord('5'), mods=alt), "\x1b" + '5')
ae(enc(ord('5'), mods=shift | alt), "\x1b" + '%')
ae(enc(ord('5'), mods=ctrl), '5')
ae(enc(ord('5'), mods=ctrl | alt), "\x1b" + '5')
ae(enc(ord('6')), '6')
ae(enc(ord('6'), mods=shift), '^')
ae(enc(ord('6'), mods=alt), "\x1b" + '6')
ae(enc(ord('6'), mods=shift | alt), "\x1b" + '^')
ae(enc(ord('6'), mods=ctrl), '6')
ae(enc(ord('6'), mods=ctrl | alt), "\x1b" + '6')
ae(enc(ord('7')), '7')
ae(enc(ord('7'), mods=shift), '&')
ae(enc(ord('7'), mods=alt), "\x1b" + '7')
ae(enc(ord('7'), mods=shift | alt), "\x1b" + '&')
ae(enc(ord('7'), mods=ctrl), '7')
ae(enc(ord('7'), mods=ctrl | alt), "\x1b" + '7')
ae(enc(ord('8')), '8')
ae(enc(ord('8'), mods=shift), '*')
ae(enc(ord('8'), mods=alt), "\x1b" + '8')
ae(enc(ord('8'), mods=shift | alt), "\x1b" + '*')
ae(enc(ord('8'), mods=ctrl), '8')
ae(enc(ord('8'), mods=ctrl | alt), "\x1b" + '8')
ae(enc(ord('9')), '9')
ae(enc(ord('9'), mods=shift), '(')
ae(enc(ord('9'), mods=alt), "\x1b" + '9')
ae(enc(ord('9'), mods=shift | alt), "\x1b" + '(')
ae(enc(ord('9'), mods=ctrl), '9')
ae(enc(ord('9'), mods=ctrl | alt), "\x1b" + '9')
ae(enc(ord('0')), '0')
ae(enc(ord('0'), mods=shift), ')')
ae(enc(ord('0'), mods=alt), "\x1b" + '0')
ae(enc(ord('0'), mods=shift | alt), "\x1b" + ')')
ae(enc(ord('0'), mods=ctrl), '0')
ae(enc(ord('0'), mods=ctrl | alt), "\x1b" + '0')
ae(enc(ord('-')), '-')
ae(enc(ord('-'), mods=shift), '_')
ae(enc(ord('-'), mods=alt), "\x1b" + '-')
ae(enc(ord('-'), mods=shift | alt), "\x1b" + '_')
ae(enc(ord('-'), mods=ctrl), '-')
ae(enc(ord('-'), mods=ctrl | alt), "\x1b" + '-')
ae(enc(ord('=')), '=')
ae(enc(ord('='), mods=shift), '+')
ae(enc(ord('='), mods=alt), "\x1b" + '=')
ae(enc(ord('='), mods=shift | alt), "\x1b" + '+')
ae(enc(ord('='), mods=ctrl), '=')
ae(enc(ord('='), mods=ctrl | alt), "\x1b" + '=')
ae(enc(ord('[')), '[')
ae(enc(ord('['), mods=shift), '{')
ae(enc(ord('['), mods=alt), "\x1b" + '[')
ae(enc(ord('['), mods=shift | alt), "\x1b" + '{')
ae(enc(ord('['), mods=ctrl), '\x1b')
ae(enc(ord('['), mods=ctrl | alt), "\x1b" + '\x1b')
ae(enc(ord(']')), ']')
ae(enc(ord(']'), mods=shift), '}')
ae(enc(ord(']'), mods=alt), "\x1b" + ']')
ae(enc(ord(']'), mods=shift | alt), "\x1b" + '}')
ae(enc(ord(']'), mods=ctrl), '\x1d')
ae(enc(ord(']'), mods=ctrl | alt), "\x1b" + '\x1d')
ae(enc(ord('\\')), '\\')
ae(enc(ord('\\'), mods=shift), '|')
ae(enc(ord('\\'), mods=alt), "\x1b" + '\\')
ae(enc(ord('\\'), mods=shift | alt), "\x1b" + '|')
ae(enc(ord('\\'), mods=ctrl), '\x1c')
ae(enc(ord('\\'), mods=ctrl | alt), "\x1b" + '\x1c')
ae(enc(ord(';')), ';')
ae(enc(ord(';'), mods=shift), ':')
ae(enc(ord(';'), mods=alt), "\x1b" + ';')
ae(enc(ord(';'), mods=shift | alt), "\x1b" + ':')
ae(enc(ord(';'), mods=ctrl), ';')
ae(enc(ord(';'), mods=ctrl | alt), "\x1b" + ';')
ae(enc(ord("'")), "'")
ae(enc(ord("'"), mods=shift), '"')
ae(enc(ord("'"), mods=alt), "\x1b" + "'")
ae(enc(ord("'"), mods=shift | alt), "\x1b" + '"')
ae(enc(ord("'"), mods=ctrl), "'")
ae(enc(ord("'"), mods=ctrl | alt), "\x1b" + "'")
ae(enc(ord(',')), ',')
ae(enc(ord(','), mods=shift), '<')
ae(enc(ord(','), mods=alt), "\x1b" + ',')
ae(enc(ord(','), mods=shift | alt), "\x1b" + '<')
ae(enc(ord(','), mods=ctrl), ',')
ae(enc(ord(','), mods=ctrl | alt), "\x1b" + ',')
ae(enc(ord('.')), '.')
ae(enc(ord('.'), mods=shift), '>')
ae(enc(ord('.'), mods=alt), "\x1b" + '.')
ae(enc(ord('.'), mods=shift | alt), "\x1b" + '>')
ae(enc(ord('.'), mods=ctrl), '.')
ae(enc(ord('.'), mods=ctrl | alt), "\x1b" + '.')
ae(enc(ord('/')), '/')
ae(enc(ord('/'), mods=shift), '?')
ae(enc(ord('/'), mods=alt), "\x1b" + '/')
ae(enc(ord('/'), mods=shift | alt), "\x1b" + '?')
ae(enc(ord('/'), mods=ctrl), '/')
ae(enc(ord('/'), mods=ctrl | alt), "\x1b" + '/')
ae(enc(ord('a')), 'a')
ae(enc(ord('a'), mods=shift), 'A')
ae(enc(ord('a'), mods=alt), "\x1b" + 'a')
ae(enc(ord('a'), mods=shift | alt), "\x1b" + 'A')
ae(enc(ord('a'), mods=ctrl), '!')
ae(enc(ord('a'), mods=ctrl | alt), "\x1b" + '!')
ae(enc(ord('b')), 'b')
ae(enc(ord('b'), mods=shift), 'B')
ae(enc(ord('b'), mods=alt), "\x1b" + 'b')
ae(enc(ord('b'), mods=shift | alt), "\x1b" + 'B')
ae(enc(ord('b'), mods=ctrl), '"')
ae(enc(ord('b'), mods=ctrl | alt), "\x1b" + '"')
ae(enc(ord('c')), 'c')
ae(enc(ord('c'), mods=shift), 'C')
ae(enc(ord('c'), mods=alt), "\x1b" + 'c')
ae(enc(ord('c'), mods=shift | alt), "\x1b" + 'C')
ae(enc(ord('c'), mods=ctrl), '#')
ae(enc(ord('c'), mods=ctrl | alt), "\x1b" + '#')
ae(enc(ord('d')), 'd')
ae(enc(ord('d'), mods=shift), 'D')
ae(enc(ord('d'), mods=alt), "\x1b" + 'd')
ae(enc(ord('d'), mods=shift | alt), "\x1b" + 'D')
ae(enc(ord('d'), mods=ctrl), '$')
ae(enc(ord('d'), mods=ctrl | alt), "\x1b" + '$')
ae(enc(ord('e')), 'e')
ae(enc(ord('e'), mods=shift), 'E')
ae(enc(ord('e'), mods=alt), "\x1b" + 'e')
ae(enc(ord('e'), mods=shift | alt), "\x1b" + 'E')
ae(enc(ord('e'), mods=ctrl), '%')
ae(enc(ord('e'), mods=ctrl | alt), "\x1b" + '%')
ae(enc(ord('f')), 'f')
ae(enc(ord('f'), mods=shift), 'F')
ae(enc(ord('f'), mods=alt), "\x1b" + 'f')
ae(enc(ord('f'), mods=shift | alt), "\x1b" + 'F')
ae(enc(ord('f'), mods=ctrl), '&')
ae(enc(ord('f'), mods=ctrl | alt), "\x1b" + '&')
ae(enc(ord('g')), 'g')
ae(enc(ord('g'), mods=shift), 'G')
ae(enc(ord('g'), mods=alt), "\x1b" + 'g')
ae(enc(ord('g'), mods=shift | alt), "\x1b" + 'G')
ae(enc(ord('g'), mods=ctrl), "'")
ae(enc(ord('g'), mods=ctrl | alt), "\x1b" + "'")
ae(enc(ord('h')), 'h')
ae(enc(ord('h'), mods=shift), 'H')
ae(enc(ord('h'), mods=alt), "\x1b" + 'h')
ae(enc(ord('h'), mods=shift | alt), "\x1b" + 'H')
ae(enc(ord('h'), mods=ctrl), '(')
ae(enc(ord('h'), mods=ctrl | alt), "\x1b" + '(')
ae(enc(ord('i')), 'i')
ae(enc(ord('i'), mods=shift), 'I')
ae(enc(ord('i'), mods=alt), "\x1b" + 'i')
ae(enc(ord('i'), mods=shift | alt), "\x1b" + 'I')
ae(enc(ord('i'), mods=ctrl), ')')
ae(enc(ord('i'), mods=ctrl | alt), "\x1b" + ')')
ae(enc(ord('j')), 'j')
ae(enc(ord('j'), mods=shift), 'J')
ae(enc(ord('j'), mods=alt), "\x1b" + 'j')
ae(enc(ord('j'), mods=shift | alt), "\x1b" + 'J')
ae(enc(ord('j'), mods=ctrl), '*')
ae(enc(ord('j'), mods=ctrl | alt), "\x1b" + '*')
ae(enc(ord('k')), 'k')
ae(enc(ord('k'), mods=shift), 'K')
ae(enc(ord('k'), mods=alt), "\x1b" + 'k')
ae(enc(ord('k'), mods=shift | alt), "\x1b" + 'K')
ae(enc(ord('k'), mods=ctrl), '+')
ae(enc(ord('k'), mods=ctrl | alt), "\x1b" + '+')
ae(enc(ord('l')), 'l')
ae(enc(ord('l'), mods=shift), 'L')
ae(enc(ord('l'), mods=alt), "\x1b" + 'l')
ae(enc(ord('l'), mods=shift | alt), "\x1b" + 'L')
ae(enc(ord('l'), mods=ctrl), ',')
ae(enc(ord('l'), mods=ctrl | alt), "\x1b" + ',')
ae(enc(ord('m')), 'm')
ae(enc(ord('m'), mods=shift), 'M')
ae(enc(ord('m'), mods=alt), "\x1b" + 'm')
ae(enc(ord('m'), mods=shift | alt), "\x1b" + 'M')
ae(enc(ord('m'), mods=ctrl), '-')
ae(enc(ord('m'), mods=ctrl | alt), "\x1b" + '-')
ae(enc(ord('n')), 'n')
ae(enc(ord('n'), mods=shift), 'N')
ae(enc(ord('n'), mods=alt), "\x1b" + 'n')
ae(enc(ord('n'), mods=shift | alt), "\x1b" + 'N')
ae(enc(ord('n'), mods=ctrl), '.')
ae(enc(ord('n'), mods=ctrl | alt), "\x1b" + '.')
ae(enc(ord('o')), 'o')
ae(enc(ord('o'), mods=shift), 'O')
ae(enc(ord('o'), mods=alt), "\x1b" + 'o')
ae(enc(ord('o'), mods=shift | alt), "\x1b" + 'O')
ae(enc(ord('o'), mods=ctrl), '/')
ae(enc(ord('o'), mods=ctrl | alt), "\x1b" + '/')
ae(enc(ord('p')), 'p')
ae(enc(ord('p'), mods=shift), 'P')
ae(enc(ord('p'), mods=alt), "\x1b" + 'p')
ae(enc(ord('p'), mods=shift | alt), "\x1b" + 'P')
ae(enc(ord('p'), mods=ctrl), '0')
ae(enc(ord('p'), mods=ctrl | alt), "\x1b" + '0')
ae(enc(ord('q')), 'q')
ae(enc(ord('q'), mods=shift), 'Q')
ae(enc(ord('q'), mods=alt), "\x1b" + 'q')
ae(enc(ord('q'), mods=shift | alt), "\x1b" + 'Q')
ae(enc(ord('q'), mods=ctrl), '1')
ae(enc(ord('q'), mods=ctrl | alt), "\x1b" + '1')
ae(enc(ord('r')), 'r')
ae(enc(ord('r'), mods=shift), 'R')
ae(enc(ord('r'), mods=alt), "\x1b" + 'r')
ae(enc(ord('r'), mods=shift | alt), "\x1b" + 'R')
ae(enc(ord('r'), mods=ctrl), '2')
ae(enc(ord('r'), mods=ctrl | alt), "\x1b" + '2')
ae(enc(ord('s')), 's')
ae(enc(ord('s'), mods=shift), 'S')
ae(enc(ord('s'), mods=alt), "\x1b" + 's')
ae(enc(ord('s'), mods=shift | alt), "\x1b" + 'S')
ae(enc(ord('s'), mods=ctrl), '3')
ae(enc(ord('s'), mods=ctrl | alt), "\x1b" + '3')
ae(enc(ord('t')), 't')
ae(enc(ord('t'), mods=shift), 'T')
ae(enc(ord('t'), mods=alt), "\x1b" + 't')
ae(enc(ord('t'), mods=shift | alt), "\x1b" + 'T')
ae(enc(ord('t'), mods=ctrl), '4')
ae(enc(ord('t'), mods=ctrl | alt), "\x1b" + '4')
ae(enc(ord('u')), 'u')
ae(enc(ord('u'), mods=shift), 'U')
ae(enc(ord('u'), mods=alt), "\x1b" + 'u')
ae(enc(ord('u'), mods=shift | alt), "\x1b" + 'U')
ae(enc(ord('u'), mods=ctrl), '5')
ae(enc(ord('u'), mods=ctrl | alt), "\x1b" + '5')
ae(enc(ord('v')), 'v')
ae(enc(ord('v'), mods=shift), 'V')
ae(enc(ord('v'), mods=alt), "\x1b" + 'v')
ae(enc(ord('v'), mods=shift | alt), "\x1b" + 'V')
ae(enc(ord('v'), mods=ctrl), '6')
ae(enc(ord('v'), mods=ctrl | alt), "\x1b" + '6')
ae(enc(ord('w')), 'w')
ae(enc(ord('w'), mods=shift), 'W')
ae(enc(ord('w'), mods=alt), "\x1b" + 'w')
ae(enc(ord('w'), mods=shift | alt), "\x1b" + 'W')
ae(enc(ord('w'), mods=ctrl), '7')
ae(enc(ord('w'), mods=ctrl | alt), "\x1b" + '7')
ae(enc(ord('x')), 'x')
ae(enc(ord('x'), mods=shift), 'X')
ae(enc(ord('x'), mods=alt), "\x1b" + 'x')
ae(enc(ord('x'), mods=shift | alt), "\x1b" + 'X')
ae(enc(ord('x'), mods=ctrl), '8')
ae(enc(ord('x'), mods=ctrl | alt), "\x1b" + '8')
ae(enc(ord('y')), 'y')
ae(enc(ord('y'), mods=shift), 'Y')
ae(enc(ord('y'), mods=alt), "\x1b" + 'y')
ae(enc(ord('y'), mods=shift | alt), "\x1b" + 'Y')
ae(enc(ord('y'), mods=ctrl), '9')
ae(enc(ord('y'), mods=ctrl | alt), "\x1b" + '9')
ae(enc(ord('z')), 'z')
ae(enc(ord('z'), mods=shift), 'Z')
ae(enc(ord('z'), mods=alt), "\x1b" + 'z')
ae(enc(ord('z'), mods=shift | alt), "\x1b" + 'Z')
ae(enc(ord('z'), mods=ctrl), ':')
ae(enc(ord('z'), mods=ctrl | alt), "\x1b" + ':')
# end legacy letter tests
# }}}
ae(enc(key=ord(' ')), ' ')
ae(enc(key=ord(' '), mods=ctrl), '\0')
ae(enc(key=ord('i'), mods=ctrl | shift), csi(ctrl | shift, ord('i')))
q = partial(enc, key=ord('a'))
ae(q(), 'a')
ae(q(text='a'), 'a')