diff --git a/docs/keyboard-protocol.rst b/docs/keyboard-protocol.rst index d1ca90272..66de95305 100644 --- a/docs/keyboard-protocol.rst +++ b/docs/keyboard-protocol.rst @@ -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 `. +.. versionadded:: 0.20.0 + A basic overview ------------------ @@ -302,6 +308,34 @@ distinguish these, use the :ref:`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 `. .. _functional: diff --git a/gen-key-constants.py b/gen-key-constants.py index 1e746daa5..d5e27cd35 100644 --- a/gen-key-constants.py +++ b/gen-key-constants.py @@ -2,9 +2,9 @@ # vim:fileencoding=utf-8 # License: GPLv3 Copyright: 2021, Kovid Goyal +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__': diff --git a/kitty/key_encoding.c b/kitty/key_encoding.c index 9180ded9b..e46d468b5 100644 --- a/kitty/key_encoding.c +++ b/kitty/key_encoding.c @@ -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) { - int ret = encode_printable_ascii_key_legacy(ev, output); - if (ret > 0) return ret; + 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); diff --git a/kitty/keys.c b/kitty/keys.c index dc1256a8c..0cbbbe4de 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -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[] = { diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py index a9f7a12f4..bd305a878 100644 --- a/kitty_tests/keys.py +++ b/kitty_tests/keys.py @@ -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')