diff --git a/docs/keyboard-protocol.rst b/docs/keyboard-protocol.rst
index db4accb75..44c61b2a7 100644
--- a/docs/keyboard-protocol.rst
+++ b/docs/keyboard-protocol.rst
@@ -21,11 +21,11 @@ advanced usages. The protocol is based on initial work in `fixterms
`_, however, it corrects various
issues in that proposal, namely:
- * No way to disambiguate Esc keypresses, other than using 8-bit controls
+ * No way to disambiguate :kbd:`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 have non-conflicting escape codes for :kbd:`alt+letter,
+ ctrl+letter, ctrl+alt+letter` key presses
* 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
@@ -62,6 +62,8 @@ are separated by the semi-colon and sub-fields by the colon. Only the
escape code is terminated by the ``u`` character (the byte ``0x75``).
+.. _key_codes:
+
Key codes
~~~~~~~~~~~~~~
@@ -113,6 +115,8 @@ 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:
+
Event types
~~~~~~~~~~~~~~~~
@@ -130,8 +134,8 @@ has value ``1`` and is the default if no event type sub field is present. The
.. 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.
+ events are not supported for them, unless the application requests *key
+ report mode*, see below.
Non-Unicode keys
@@ -145,11 +149,90 @@ names to code points for these keys is in the
:ref:`Functional key definition table below `.
+Progressive enhancement
+--------------------------
+
+While, in theory, every key event could be completely represented by this
+protocol and all would be hunk-dory, in reality there is a vast universe of
+existing terminal programs that expect legacy control codes for key events and
+that are not likely to ever be updated. To support these, in default mode,
+the terminal will emit legacy escape codes for compatibility. If a terminal
+program wants more robust key handling, it can request it from the terminal,
+via the mechanism described here. Each enhancement is described in detail
+below. The escape code for requesting enhancements is::
+
+ CSI = flags ; mode u
+
+Here ``flags`` is a decimal encoded integer to specify a set of bit-flags. The
+meanings of the flags are given below. The second, ``mode`` parameter is
+optional (defaulting to ``1``) and specifies how the flags are applied.
+The value ``1`` means all set bits are set and all unset bits are reset.
+The value ``2`` means all set bits are set, unset bits are left unchanged.
+The value ``3`` means all set bits are reset, unset bits are left unchanged.
+
+.. csv-table:: The progressive enhancement flags
+ :header: "Bit", "Meaning"
+
+ "0b1 (1)", "Disambiguate escape codes"
+ "0b10 (2)", "Report key event types"
+ "0b100 (4)", "Report alternate keys"
+ "0b1000 (8)", "Report all keys as CSIu escape codes"
+
+The program running in the terminal can query the terminal for the
+current values of the flags by sending::
+
+ CSI ? u
+
+The terminal will reply with::
+
+ CSI ? flags u
+
+The program can also push/pop the current flags onto a stack in the
+terminal with::
+
+ CSI > flags u # for push, if flags ommitted default to zero
+ CSI < number u # to pop number entries, defaulting to 1 if unspecified
+
+Terminals should limit the size of the stack as appropriate, to prevent
+Denial-of-Service attacks. Terminals must maintain separate stacks for the main
+and alternate screens. If a pop request is received that empties the stack,
+all flags are reset. If a push request is received and the stack is full, the
+oldest entry from the stack must be evicted.
+
+Disambiguate escape codes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This type of progressive enhancement fixes the problem of some legacy key
+press encodings overlapping with other control codes. For instance, pressing
+the :kbd:`Esc` key generates the byte ``0x1b`` which also is used to indicate
+the start of an escape code. Similarly pressing the key :kbd:`alt+[` will
+generate the bytes used for CSI control codes. Turning on this flag will cause
+the terminal to report the :kbd:`Esc, alt+letter, ctrl+letter, ctrl+alt+letter`
+keys using CSIu sequences instead of legacy ones. Here letter is any printable
+ASCII letter (from 32 (i.e. space) to 126 (i.e. ~)).
+
+Report event types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This type of progressive enhancement causes the terminal to report key repeat
+and key release events. Normally only key press events are reported and key
+repeat events are treated as key press events. See :ref:`event_types` for
+details on how these are reported.
+
+
+Report alternate keys
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This type of progressive enhancement causes the terminal to report alternate
+key values in addition to the main value, to aid in shortcut matching. See
+:ref:`key_codes` for details on how these are reported.
+
.. _functional:
Functional key definitions
----------------------------
+.. {{{
.. start functional key table (auto generated by gen-key-constants.py do not edit)
.. csv-table:: Functional key codes
@@ -261,3 +344,4 @@ Functional key definitions
"MUTE_VOLUME", "E067"
.. end functional key table
+.. }}}
diff --git a/kitty/key_encoding.c b/kitty/key_encoding.c
index 7ea805b9e..0d0924f36 100644
--- a/kitty/key_encoding.c
+++ b/kitty/key_encoding.c
@@ -160,8 +160,10 @@ encode_function_key(const KeyEvent *ev, char *output) {
static int
encode_printable_ascii_key_legacy(const KeyEvent *ev, char *output) {
- char shifted_key = 0;
+ 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;
@@ -169,14 +171,13 @@ encode_printable_ascii_key_legacy(const KeyEvent *ev, char *output) {
S('`', '~') S('-', '_') S('=', '+') S('[', '{') S(']', '}') S('\\', '|') S(';', ':') S('\'', '"') S(',', '<') S('.', '>') S('/', '?')
#undef S
}
-
- if (!ev->mods.value) return snprintf(output, KEY_BUFFER_SIZE, "%c", (char)ev->key);
- if (!ev->disambiguate) {
- if ((ev->mods.value == ALT || ev->mods.value == (SHIFT | ALT)))
- return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", (shifted_key && ev->mods.shift) ? shifted_key : (char)ev->key);
- }
- if (ev->mods.value == CTRL && (ev->key != 'i' && ev->key != 'm' && ev->key != '[' && ev->key != '@'))
- return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x7f);
+ shifted_key = (shifted_key && ev->mods.shift) ? shifted_key : (char)ev->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)
+ return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x1f);
+ if (ev->mods.value == (CTRL | ALT))
+ return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", ev->key & 0x1f);
return 0;
}
diff --git a/kitty/keys.c b/kitty/keys.c
index f47c67144..065815511 100644
--- a/kitty/keys.c
+++ b/kitty/keys.c
@@ -111,7 +111,7 @@ on_key_input(GLFWkeyevent *ev) {
screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom
}
char encoded_key[KEY_BUFFER_SIZE] = {0};
- int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key);
+ int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen_current_key_encoding_flags(screen), encoded_key);
if (size == SEND_TEXT_TO_CHILD) {
schedule_write_to_child(w->id, 1, text, strlen(text));
debug("sent text to child\n");
@@ -130,19 +130,20 @@ fake_scroll(Window *w, int amount, bool upwards) {
GLFWkeyevent ev = {.key = key };
char encoded_key[KEY_BUFFER_SIZE] = {0};
Screen *screen = w->render_data.screen;
+ uint8_t flags = screen_current_key_encoding_flags(screen);
while (amount-- > 0) {
ev.action = GLFW_PRESS;
- int size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key);
+ int size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, flags, encoded_key);
if (size > 0) schedule_write_to_child(w->id, 1, encoded_key, size);
ev.action = GLFW_RELEASE;
- size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key);
+ size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, flags, encoded_key);
if (size > 0) schedule_write_to_child(w->id, 1, encoded_key, size);
}
}
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
-#define M(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL}
+#define M(name, arg_type) {#name, (PyCFunction)(void (*) (void))(py##name), arg_type, NULL}
PYWRAP1(key_for_native_key_name) {
const char *name;
diff --git a/kitty/parser.c b/kitty/parser.c
index c8476f250..91785af11 100644
--- a/kitty/parser.c
+++ b/kitty/parser.c
@@ -683,7 +683,7 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
unsigned int num = screen->parser_buf_pos, start, i, num_params=0, p1, p2;
static unsigned int params[MAX_PARAMS] = {0};
bool private;
- if (buf[0] == '>' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=' || buf[0] == '-') {
+ if (buf[0] == '>' || buf[0] == '<' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=' || buf[0] == '-') {
start_modifier = (char)screen->parser_buf[0];
buf++; num--;
}
@@ -835,6 +835,23 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
screen_restore_cursor(screen);
break;
}
+ if (!end_modifier && start_modifier == '?') {
+ REPORT_COMMAND(screen_report_key_encoding_flags);
+ screen_report_key_encoding_flags(screen);
+ break;
+ }
+ if (!end_modifier && start_modifier == '=') {
+ CALL_CSI_HANDLER2(screen_set_key_encoding_flags, 0, 1);
+ break;
+ }
+ if (!end_modifier && start_modifier == '>') {
+ CALL_CSI_HANDLER1(screen_push_key_encoding_flags, 0);
+ break;
+ }
+ if (!end_modifier && start_modifier == '<') {
+ CALL_CSI_HANDLER1(screen_pop_key_encoding_flags, 1);
+ break;
+ }
REPORT_ERROR("Unknown CSI u sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case 'r':
@@ -1090,6 +1107,7 @@ accumulate_csi(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback)
break;
case '?':
case '>':
+ case '<':
case '!':
case '=':
case '-':
diff --git a/kitty/screen.c b/kitty/screen.c
index e2c59b414..2ba485cea 100644
--- a/kitty/screen.c
+++ b/kitty/screen.c
@@ -135,6 +135,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->tabstops = self->main_tabstops;
init_tabstops(self->main_tabstops, self->columns);
init_tabstops(self->alt_tabstops, self->columns);
+ self->key_encoding_flags = self->main_key_encoding_flags;
if (!init_overlay_line(self, self->columns)) { Py_CLEAR(self); return NULL; }
self->hyperlink_pool = alloc_hyperlink_pool();
if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); }
@@ -150,7 +151,8 @@ void
screen_reset(Screen *self) {
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true);
if (self->overlay_line.is_active) deactivate_overlay_line(self);
- self->key_encoding_flags = 0;
+ memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags));
+ memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags));
self->last_graphic_char = 0;
self->main_savepoint.is_valid = false;
self->alt_savepoint.is_valid = false;
@@ -718,12 +720,14 @@ screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_scree
if (save_cursor) screen_save_cursor(self);
self->linebuf = self->alt_linebuf;
self->tabstops = self->alt_tabstops;
+ self->key_encoding_flags = self->alt_key_encoding_flags;
self->grman = self->alt_grman;
screen_cursor_position(self, 1, 1);
cursor_reset(self->cursor);
} else {
self->linebuf = self->main_linebuf;
self->tabstops = self->main_tabstops;
+ self->key_encoding_flags = self->main_key_encoding_flags;
if (save_cursor) screen_restore_cursor(self);
self->grman = self->main_grman;
}
@@ -833,6 +837,54 @@ screen_set_8bit_controls(Screen *self, bool yes) {
self->modes.eight_bit_controls = yes;
}
+uint8_t
+screen_current_key_encoding_flags(Screen *self) {
+ for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
+ if (self->key_encoding_flags[i] & 0x80) return self->key_encoding_flags[i] & 0xf;
+ }
+ return 0;
+}
+
+void
+screen_report_key_encoding_flags(Screen *self) {
+ char buf[16] = {0};
+ snprintf(buf, sizeof(buf), "?%uu", screen_current_key_encoding_flags(self));
+ write_escape_code_to_child(self, CSI, buf);
+}
+
+void
+screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how) {
+ unsigned idx = 0;
+ for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
+ if (self->key_encoding_flags[i] & 0x80) { idx = i; break; }
+ }
+ uint8_t q = val & 0xf;
+ if (how == 1) self->key_encoding_flags[idx] = q;
+ else if (how == 2) self->key_encoding_flags[idx] |= q;
+ else if (how == 3) self->key_encoding_flags[idx] &= ~q;
+ self->key_encoding_flags[idx] |= 0x80;
+}
+
+void
+screen_push_key_encoding_flags(Screen *self, uint32_t val) {
+ uint8_t q = val & 0xf;
+ const unsigned sz = arraysz(self->main_key_encoding_flags);
+ unsigned current_idx = 0;
+ for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) {
+ if (self->key_encoding_flags[i] & 0x80) { current_idx = i; break; }
+ }
+ if (current_idx == sz - 1) memmove(self->key_encoding_flags, self->key_encoding_flags + 1, (sz - 1) * sizeof(self->main_key_encoding_flags[0]));
+ else self->key_encoding_flags[current_idx++] |= 0x80;
+ self->key_encoding_flags[current_idx] = 0x80 | q;
+}
+
+void
+screen_pop_key_encoding_flags(Screen *self, uint32_t num) {
+ for (unsigned i = arraysz(self->main_key_encoding_flags); num && i-- > 0; ) {
+ if (self->key_encoding_flags[i] & 0x80) { num--; self->key_encoding_flags[i] = 0; }
+ }
+}
+
// }}}
// Cursor {{{
diff --git a/kitty/screen.h b/kitty/screen.h
index 8dbb65fbb..f1c06486c 100644
--- a/kitty/screen.h
+++ b/kitty/screen.h
@@ -129,7 +129,7 @@ typedef struct {
HYPERLINK_POOL_HANDLE hyperlink_pool;
ANSIBuf as_ansi_buf;
char_type last_graphic_char;
- unsigned key_encoding_flags;
+ uint8_t main_key_encoding_flags[8], alt_key_encoding_flags[8], *key_encoding_flags;
} Screen;
@@ -223,9 +223,15 @@ void screen_rescale_images(Screen *self);
void screen_report_size(Screen *, unsigned int which);
void screen_manipulate_title_stack(Screen *, unsigned int op, unsigned int which);
void screen_draw_overlay_text(Screen *self, const char *utf8_text);
+void screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how);
+void screen_push_key_encoding_flags(Screen *self, uint32_t val);
+void screen_pop_key_encoding_flags(Screen *self, uint32_t num);
+uint8_t screen_current_key_encoding_flags(Screen *self);
+void screen_report_key_encoding_flags(Screen *self);
#define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen);
DECLARE_CH_SCREEN_HANDLER(bell)
DECLARE_CH_SCREEN_HANDLER(backspace)
DECLARE_CH_SCREEN_HANDLER(tab)
DECLARE_CH_SCREEN_HANDLER(linefeed)
DECLARE_CH_SCREEN_HANDLER(carriage_return)
+#undef DECLARE_CH_SCREEN_HANDLER
diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py
index a4eacdeda..c0c3d24cc 100644
--- a/kitty_tests/keys.py
+++ b/kitty_tests/keys.py
@@ -2,82 +2,11 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal
-from functools import partial
-
import kitty.fast_data_types as defines
-from kitty.keys import (
- interpret_key_event, modify_complex_key, modify_key_bytes, smkx_key_map
-)
-
from . import BaseTest
-class DummyWindow:
-
- def __init__(self):
- self.screen = self
- self.extended_keyboard = False
- self.cursor_key_mode = True
-
-
-class TestParser(BaseTest):
-
- def test_modify_complex_key(self):
- self.ae(modify_complex_key('kcuu1', 4), b'\033[1;4A')
- self.ae(modify_complex_key('kcuu1', 3), b'\033[1;3A')
- self.ae(modify_complex_key('kf5', 3), b'\033[15;3~')
- self.assertRaises(ValueError, modify_complex_key, 'kri', 3)
-
- def test_interpret_key_event(self):
- # test rmkx/smkx
- w = DummyWindow()
-
- def k(expected, key, mods=0):
- actual = interpret_key_event(
- getattr(defines, 'GLFW_KEY_' + key),
- 0,
- mods,
- w,
- defines.GLFW_PRESS,
- )
- self.ae(b'\033' + expected.encode('ascii'), actual)
-
- for ckm, mch in {True: 'O', False: '['}.items():
- w.cursor_key_mode = ckm
- for name, ch in {
- 'UP': 'A',
- 'DOWN': 'B',
- 'RIGHT': 'C',
- 'LEFT': 'D',
- 'HOME': 'H',
- 'END': 'F',
- }.items():
- k(mch + ch, name)
- w.cursor_key_mode = True
-
- # test remaining special keys
- for key, num in zip('INSERT DELETE PAGE_UP PAGE_DOWN'.split(), '2356'):
- k('[' + num + '~', key)
- for key, num in zip('1234', 'PQRS'):
- k('O' + num, 'F' + key)
- for key, num in zip(range(5, 13), (15, 17, 18, 19, 20, 21, 23, 24)):
- k('[' + str(num) + '~', 'F{}'.format(key))
-
- # test modifiers
- SPECIAL_KEYS = 'UP DOWN RIGHT LEFT HOME END INSERT DELETE PAGE_UP PAGE_DOWN '
- for i in range(1, 13):
- SPECIAL_KEYS += 'F{} '.format(i)
- SPECIAL_KEYS = SPECIAL_KEYS.strip().split()
- for mods, num in zip(('CONTROL', 'ALT', 'SHIFT+ALT'), '534'):
- fmods = 0
- num = int(num)
- for m in mods.split('+'):
- fmods |= getattr(defines, 'GLFW_MOD_' + m)
- km = partial(k, mods=fmods)
- for key in SPECIAL_KEYS:
- keycode = getattr(defines, 'GLFW_KEY_' + key)
- base_key = smkx_key_map[keycode]
- km(modify_key_bytes(base_key, num).decode('ascii')[1:], key)
+class TestKeys(BaseTest):
def test_encode_mouse_event(self):
NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL = range(4)
diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py
index d9f3385db..0ac19d0b3 100644
--- a/kitty_tests/screen.py
+++ b/kitty_tests/screen.py
@@ -728,6 +728,49 @@ class TestScreen(BaseTest):
self.ae(str(s.linebuf), '0\n5\n6\n7\n\n')
self.ae(str(s.historybuf), '')
+ def test_key_encoding_flags_stack(self):
+ s = self.create_screen()
+ c = s.callbacks
+
+ def w(code, p1='', p2=''):
+ p = f'{p1}'
+ if p2:
+ p += f';{p2}'
+ return parse_bytes(s, f'\033[{code}{p}u'.encode('ascii'))
+
+ def ac(flags):
+ parse_bytes(s, '\033[?u'.encode('ascii'))
+ self.ae(c.wtcbuf, f'\033[?{flags}u'.encode('ascii'))
+ c.clear()
+
+ ac(0)
+ w('=', 0b1001)
+ ac(0b1001)
+ w('=', 0b0011, 2)
+ ac(0b1011)
+ w('=', 0b0110, 3)
+ ac(0b1001)
+ s.reset()
+ ac(0)
+
+ w('>', 0b0011)
+ ac(0b0011)
+ w('=', 0b1111)
+ ac(0b1111)
+ w('>', 0b10)
+ ac(0b10)
+ w('<')
+ ac(0b1111)
+ for i in range(10):
+ w('<')
+ ac(0)
+ s.reset()
+
+ for i in range(1, 16):
+ w('>', i)
+ ac(15)
+ w('<'), ac(14), w('<'), ac(13)
+
def test_color_stack(self):
s = self.create_screen()
c = s.callbacks