From 75cb580e36d2f79d78f0df0d0d95e6f2411fe0a8 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Mon, 14 Sep 2026 14:14:28 +0100 Subject: [PATCH] Add Kitty keyboard protocol support --- Makefile.am | 2 + format.c | 10 + input-keys.c | 35 +++- input-kitty.c | 327 +++++++++++++++++++++++++++++++ input.c | 51 ++++- key-string.c | 82 +++++++- options-table.c | 2 +- popup.c | 3 +- regress/kitty-keys.sh | 107 ++++++++++ screen-write.c | 5 +- screen.c | 10 +- server-client.c | 1 + tmux.1 | 37 +++- tmux.h | 117 ++++++++++- tty-keys.c | 29 +++ tty-kitty.c | 440 ++++++++++++++++++++++++++++++++++++++++++ tty.c | 15 +- window.c | 11 +- 18 files changed, 1258 insertions(+), 26 deletions(-) create mode 100644 input-kitty.c create mode 100644 regress/kitty-keys.sh create mode 100644 tty-kitty.c diff --git a/Makefile.am b/Makefile.am index 60770fd1f..8c98a0bb2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -171,6 +171,7 @@ dist_tmux_SOURCES = \ grid.c \ hooks.c \ hyperlinks.c \ + input-kitty.c \ input-keys.c \ input.c \ job.c \ @@ -212,6 +213,7 @@ dist_tmux_SOURCES = \ tty-acs.c \ tty-draw.c \ tty-features.c \ + tty-kitty.c \ tty-keys.c \ tty-term.c \ tty.c \ diff --git a/format.c b/format.c index d7a9d75b2..c36e58f51 100644 --- a/format.c +++ b/format.c @@ -2487,7 +2487,17 @@ format_cb_pane_unseen_changes(struct format_tree *ft) static void * format_cb_pane_key_mode(struct format_tree *ft) { + char *s; + if (ft->wp != NULL && ft->wp->screen != NULL) { + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) { + if (ft->wp->screen->kitty_keys.flags == 0) + return (xstrdup("VT10x")); + xasprintf(&s, "Kitty %u", + ft->wp->screen->kitty_keys.flags); + return (s); + } switch (ft->wp->screen->mode & EXTENDED_KEY_MODES) { case MODE_KEYS_EXTENDED: return (xstrdup("Ext 1")); diff --git a/input-keys.c b/input-keys.c index 4cade4e53..765e948d0 100644 --- a/input-keys.c +++ b/input-keys.c @@ -395,7 +395,8 @@ input_key_build(void) /* Translate a key code into an output key sequence for a pane. */ int -input_key_pane(struct window_pane *wp, key_code key, struct mouse_event *m) +input_key_pane(struct window_pane *wp, key_code key, struct mouse_event *m, + int extended_encoding) { if (log_get_level() != 0) { log_debug("writing key 0x%llx (%s) to %%%u", key, @@ -407,7 +408,21 @@ input_key_pane(struct window_pane *wp, key_code key, struct mouse_event *m) input_key_mouse(wp, m); return (0); } - return (input_key(wp->screen, wp->event, key)); + return (input_key(wp->screen, wp->event, key, extended_encoding)); +} + +/* Return whether a key may use the configured extended encoding. */ +int +input_key_client_supports_extended(struct client *c, key_code key) +{ + if (c == NULL || (key & KEYC_SENT)) + return (1); + if (options_get_number(global_options, "extended-keys") == 0) + return (0); + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + return ((c->tty.flags & TTY_KKBPUSHED) != 0); + return (tty_term_has(c->tty.term, TTYC_ENEKS)); } static void @@ -464,7 +479,8 @@ input_key_extended(struct bufferevent *bev, key_code key) } else key &= KEYC_MASK_KEY; - if (options_get_number(global_options, "extended-keys-format") == 1) + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_XTERM) xsnprintf(tmp, sizeof tmp, "\033[27;%c;%llu~", modifier, key); else xsnprintf(tmp, sizeof tmp, "\033[%llu;%cu", key, modifier); @@ -571,7 +587,8 @@ input_key_mode1(struct bufferevent *bev, key_code key) /* Translate a key code into an output key sequence. */ int -input_key(struct screen *s, struct bufferevent *bev, key_code key) +input_key(struct screen *s, struct bufferevent *bev, key_code key, + int extended_encoding) { struct input_key_entry *ike = NULL; key_code newkey; @@ -588,6 +605,11 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key) return (0); } + if (extended_encoding && + options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY && input_key_kitty(s, bev, key) == 0) + return (0); + /* Is this backspace? */ if ((key & KEYC_MASK_KEY) == KEYC_BSPACE) { newkey = options_get_number(global_options, "backspace"); @@ -677,6 +699,11 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key) log_debug("%s: ignoring key 0x%llx", __func__, key); return (0); } + if (key & (KEYC_SUPER|KEYC_HYPER)) + return (input_key_vt10x(bev, key)); + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + return (input_key_vt10x(bev, key)); /* * No builtin key sequence; construct an extended key sequence diff --git a/input-kitty.c b/input-kitty.c new file mode 100644 index 000000000..8879f8de9 --- /dev/null +++ b/input-kitty.c @@ -0,0 +1,327 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Track pane keyboard state and encode keys for applications. */ + +struct input_kitty_key { + key_code key; + u_int number; + char final; +}; + +static const struct input_kitty_key input_kitty_keys[] = { + { C0_ESC, 27, 'u' }, + { C0_CR, 13, 'u' }, + { C0_HT, 9, 'u' }, + { KEYC_BSPACE, 127, 'u' }, + { KEYC_IC, 2, '~' }, + { KEYC_DC, 3, '~' }, + { KEYC_LEFT, 1, 'D' }, + { KEYC_RIGHT, 1, 'C' }, + { KEYC_UP, 1, 'A' }, + { KEYC_DOWN, 1, 'B' }, + { KEYC_PPAGE, 5, '~' }, + { KEYC_NPAGE, 6, '~' }, + { KEYC_HOME, 1, 'H' }, + { KEYC_END, 1, 'F' }, + { KEYC_F1, 1, 'P' }, + { KEYC_F2, 1, 'Q' }, + { KEYC_F3, 13, '~' }, + { KEYC_F4, 1, 'S' }, + { KEYC_F5, 15, '~' }, + { KEYC_F6, 17, '~' }, + { KEYC_F7, 18, '~' }, + { KEYC_F8, 19, '~' }, + { KEYC_F9, 20, '~' }, + { KEYC_F10, 21, '~' }, + { KEYC_F11, 23, '~' }, + { KEYC_F12, 24, '~' }, + { KEYC_CAPSLOCK, 57358, 'u' }, + { KEYC_SCROLLLOCK, 57359, 'u' }, + { KEYC_NUMLOCK, 57360, 'u' }, + { KEYC_PRINTSCREEN, 57361, 'u' }, + { KEYC_PAUSE, 57362, 'u' }, + { KEYC_MENU, 57363, 'u' }, + { KEYC_F13, 57376, 'u' }, + { KEYC_F14, 57377, 'u' }, + { KEYC_F15, 57378, 'u' }, + { KEYC_F16, 57379, 'u' }, + { KEYC_F17, 57380, 'u' }, + { KEYC_F18, 57381, 'u' }, + { KEYC_F19, 57382, 'u' }, + { KEYC_F20, 57383, 'u' }, + { KEYC_F21, 57384, 'u' }, + { KEYC_F22, 57385, 'u' }, + { KEYC_F23, 57386, 'u' }, + { KEYC_F24, 57387, 'u' }, + { KEYC_F25, 57388, 'u' }, + { KEYC_F26, 57389, 'u' }, + { KEYC_F27, 57390, 'u' }, + { KEYC_F28, 57391, 'u' }, + { KEYC_F29, 57392, 'u' }, + { KEYC_F30, 57393, 'u' }, + { KEYC_F31, 57394, 'u' }, + { KEYC_F32, 57395, 'u' }, + { KEYC_F33, 57396, 'u' }, + { KEYC_F34, 57397, 'u' }, + { KEYC_F35, 57398, 'u' }, + { KEYC_KP_ZERO|KEYC_KEYPAD, 57399, 'u' }, + { KEYC_KP_ONE|KEYC_KEYPAD, 57400, 'u' }, + { KEYC_KP_TWO|KEYC_KEYPAD, 57401, 'u' }, + { KEYC_KP_THREE|KEYC_KEYPAD, 57402, 'u' }, + { KEYC_KP_FOUR|KEYC_KEYPAD, 57403, 'u' }, + { KEYC_KP_FIVE|KEYC_KEYPAD, 57404, 'u' }, + { KEYC_KP_SIX|KEYC_KEYPAD, 57405, 'u' }, + { KEYC_KP_SEVEN|KEYC_KEYPAD, 57406, 'u' }, + { KEYC_KP_EIGHT|KEYC_KEYPAD, 57407, 'u' }, + { KEYC_KP_NINE|KEYC_KEYPAD, 57408, 'u' }, + { KEYC_KP_PERIOD|KEYC_KEYPAD, 57409, 'u' }, + { KEYC_KP_SLASH|KEYC_KEYPAD, 57410, 'u' }, + { KEYC_KP_STAR|KEYC_KEYPAD, 57411, 'u' }, + { KEYC_KP_MINUS|KEYC_KEYPAD, 57412, 'u' }, + { KEYC_KP_PLUS|KEYC_KEYPAD, 57413, 'u' }, + { KEYC_KP_ENTER|KEYC_KEYPAD, 57414, 'u' }, + { KEYC_KP_EQUAL|KEYC_KEYPAD, 57415, 'u' }, + { KEYC_KP_SEPARATOR|KEYC_KEYPAD, 57416, 'u' }, + { KEYC_KP_LEFT|KEYC_KEYPAD, 57417, 'u' }, + { KEYC_KP_RIGHT|KEYC_KEYPAD, 57418, 'u' }, + { KEYC_KP_UP|KEYC_KEYPAD, 57419, 'u' }, + { KEYC_KP_DOWN|KEYC_KEYPAD, 57420, 'u' }, + { KEYC_KP_PPAGE|KEYC_KEYPAD, 57421, 'u' }, + { KEYC_KP_NPAGE|KEYC_KEYPAD, 57422, 'u' }, + { KEYC_KP_HOME|KEYC_KEYPAD, 57423, 'u' }, + { KEYC_KP_END|KEYC_KEYPAD, 57424, 'u' }, + { KEYC_KP_IC|KEYC_KEYPAD, 57425, 'u' }, + { KEYC_KP_DC|KEYC_KEYPAD, 57426, 'u' }, + { KEYC_KP_BEGIN|KEYC_KEYPAD, 57427, '~' }, + { KEYC_MEDIA_PLAY, 57428, 'u' }, + { KEYC_MEDIA_PAUSE, 57429, 'u' }, + { KEYC_MEDIA_PLAYPAUSE, 57430, 'u' }, + { KEYC_MEDIA_REVERSE, 57431, 'u' }, + { KEYC_MEDIA_STOP, 57432, 'u' }, + { KEYC_MEDIA_FASTFORWARD, 57433, 'u' }, + { KEYC_MEDIA_REWIND, 57434, 'u' }, + { KEYC_MEDIA_NEXT, 57435, 'u' }, + { KEYC_MEDIA_PREVIOUS, 57436, 'u' }, + { KEYC_MEDIA_RECORD, 57437, 'u' }, + { KEYC_VOLUME_DOWN, 57438, 'u' }, + { KEYC_VOLUME_UP, 57439, 'u' }, + { KEYC_VOLUME_MUTE, 57440, 'u' }, + { KEYC_LEFT_SHIFT, 57441, 'u' }, + { KEYC_LEFT_CTRL, 57442, 'u' }, + { KEYC_LEFT_ALT, 57443, 'u' }, + { KEYC_LEFT_SUPER, 57444, 'u' }, + { KEYC_LEFT_HYPER, 57445, 'u' }, + { KEYC_LEFT_META, 57446, 'u' }, + { KEYC_RIGHT_SHIFT, 57447, 'u' }, + { KEYC_RIGHT_CTRL, 57448, 'u' }, + { KEYC_RIGHT_ALT, 57449, 'u' }, + { KEYC_RIGHT_SUPER, 57450, 'u' }, + { KEYC_RIGHT_HYPER, 57451, 'u' }, + { KEYC_RIGHT_META, 57452, 'u' }, + { KEYC_ISO_LEVEL3_SHIFT, 57453, 'u' }, + { KEYC_ISO_LEVEL5_SHIFT, 57454, 'u' } +}; + +static u_int +input_kitty_default_flags(void) +{ + if (options_get_number(global_options, "extended-keys") == 2 && + options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + return (KITTY_KEY_DISAMBIGUATE); + return (0); +} + +void +input_kitty_reset(struct screen *s) +{ + memset(&s->kitty_keys, 0, sizeof s->kitty_keys); + s->kitty_keys.flags = input_kitty_default_flags(); +} + +void +input_kitty_alternate_on(struct screen *s) +{ + s->saved_kitty_keys = s->kitty_keys; + input_kitty_reset(s); +} + +void +input_kitty_alternate_off(struct screen *s) +{ + s->kitty_keys = s->saved_kitty_keys; + memset(&s->saved_kitty_keys, 0, sizeof s->saved_kitty_keys); +} + +void +input_kitty_set(struct screen *s, u_int flags, int mode) +{ + flags &= KITTY_KEY_SUPPORTED; + if (mode == 2) + s->kitty_keys.flags |= flags; + else if (mode == 3) + s->kitty_keys.flags &= ~flags; + else + s->kitty_keys.flags = flags; + s->kitty_keys.flags |= input_kitty_default_flags(); +} + +void +input_kitty_push(struct screen *s, u_int flags) +{ + s->kitty_keys.saved_flags = s->kitty_keys.flags; + s->kitty_keys.have_saved = 1; + s->kitty_keys.flags = (flags & KITTY_KEY_SUPPORTED)| + input_kitty_default_flags(); +} + +void +input_kitty_pop(struct screen *s, u_int count) +{ + if (count == 0) + return; + if (s->kitty_keys.have_saved && count == 1) + s->kitty_keys.flags = s->kitty_keys.saved_flags; + else + s->kitty_keys.flags = input_kitty_default_flags(); + s->kitty_keys.have_saved = 0; + s->kitty_keys.flags |= input_kitty_default_flags(); +} + +static u_int +input_kitty_modifiers(key_code key) +{ + u_int modifiers = 1; + + if (key & KEYC_SHIFT) + modifiers += 1; + if (key & KEYC_META) + modifiers += 2; + if (key & KEYC_CTRL) + modifiers += 4; + if (key & KEYC_SUPER) + modifiers += 8; + if (key & KEYC_HYPER) + modifiers += 16; + return (modifiers); +} + +static const struct input_kitty_key * +input_kitty_lookup(key_code key) +{ + key_code lookup; + u_int i; + + lookup = key & (KEYC_MASK_KEY|KEYC_KEYPAD); + for (i = 0; i < nitems(input_kitty_keys); i++) { + if (input_kitty_keys[i].key == lookup) + return (&input_kitty_keys[i]); + } + return (NULL); +} + +int +input_key_kitty(struct screen *s, struct bufferevent *bev, key_code key) +{ + const struct input_kitty_key *ikk; + struct utf8_data ud; + wchar_t wc; + key_code modifiers, onlykey; + u_int flags, number, modifier; + char tmp[64]; + size_t size; + + flags = s->kitty_keys.flags & KITTY_KEY_SUPPORTED; + if (flags == 0) + return (-1); + if ((key & KEYC_MASK_KEY) == KEYC_BTAB) + key = C0_HT|(key & ~KEYC_MASK_KEY)|KEYC_SHIFT; + + modifiers = key & (KEYC_SHIFT|KEYC_META|KEYC_CTRL|KEYC_SUPER|KEYC_HYPER); + ikk = input_kitty_lookup(key); + if (ikk != NULL) { + if ((flags & KITTY_KEY_REPORT_ALL) == 0) { + if (ikk->key == C0_CR || ikk->key == C0_HT || + ikk->key == KEYC_BSPACE) + return (-1); + if (ikk->final != 'u' && + (modifiers & (KEYC_SUPER|KEYC_HYPER)) == 0) + return (-1); + } + number = ikk->number; + modifier = input_kitty_modifiers(key); + if (ikk->final == 'u') { + if (modifier == 1) + size = xsnprintf(tmp, sizeof tmp, "\033[%uu", number); + else + size = xsnprintf(tmp, sizeof tmp, "\033[%u;%uu", + number, modifier); + } else if (ikk->final == '~') { + if (modifier == 1) + size = xsnprintf(tmp, sizeof tmp, "\033[%u~", number); + else + size = xsnprintf(tmp, sizeof tmp, "\033[%u;%u~", + number, modifier); + } else { + if (modifier == 1) + size = xsnprintf(tmp, sizeof tmp, "\033[%c", + ikk->final); + else + size = xsnprintf(tmp, sizeof tmp, "\033[1;%u%c", + modifier, ikk->final); + } + goto write; + } + + if ((flags & KITTY_KEY_REPORT_ALL) == 0 && + (modifiers & (KEYC_META|KEYC_CTRL|KEYC_SUPER|KEYC_HYPER)) == 0) + return (-1); + + onlykey = key & KEYC_MASK_KEY; + if (KEYC_IS_UNICODE(key)) { + utf8_to_data(onlykey, &ud); + if (utf8_towc(&ud, &wc) != UTF8_DONE) + return (-1); + number = wc; + } else if (onlykey <= 0x7f) + number = onlykey; + else + return (-1); + if ((key & KEYC_SHIFT) && number >= 'A' && number <= 'Z') + number += 'a' - 'A'; + + modifier = input_kitty_modifiers(key); + if (modifier == 1) + size = xsnprintf(tmp, sizeof tmp, "\033[%uu", number); + else + size = xsnprintf(tmp, sizeof tmp, "\033[%u;%uu", number, + modifier); + +write: + log_debug("%s: %.*s", __func__, (int)size, tmp); + bufferevent_write(bev, tmp, size); + return (0); +} diff --git a/input.c b/input.c index 2fef558cb..97427c2be 100644 --- a/input.c +++ b/input.c @@ -279,6 +279,10 @@ enum input_csi_type { INPUT_CSI_HPA, INPUT_CSI_ICH, INPUT_CSI_IL, + INPUT_CSI_KITTY_POP, + INPUT_CSI_KITTY_PUSH, + INPUT_CSI_KITTY_QUERY, + INPUT_CSI_KITTY_SET, INPUT_CSI_MODOFF, INPUT_CSI_MODSET, INPUT_CSI_QUERY, @@ -344,7 +348,11 @@ static const struct input_table_entry input_csi_table[] = { { 'r', "", INPUT_CSI_DECSTBM }, { 's', "", INPUT_CSI_SCP }, { 't', "", INPUT_CSI_WINOPS }, - { 'u', "", INPUT_CSI_RCP } + { 'u', "", INPUT_CSI_RCP }, + { 'u', "<", INPUT_CSI_KITTY_POP }, + { 'u', "=", INPUT_CSI_KITTY_SET }, + { 'u', ">", INPUT_CSI_KITTY_PUSH }, + { 'u', "?", INPUT_CSI_KITTY_QUERY } }; /* Input transition. */ @@ -1525,6 +1533,9 @@ input_csi_dispatch(struct input_ctx *ictx) screen_write_cursormove(sctx, m - 1, n - 1, 1); break; case INPUT_CSI_MODSET: + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + break; n = input_get(ictx, 0, 0, 0); if (n != 4) break; @@ -1544,6 +1555,9 @@ input_csi_dispatch(struct input_ctx *ictx) screen_write_mode_set(sctx, MODE_KEYS_EXTENDED); break; case INPUT_CSI_MODOFF: + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + break; n = input_get(ictx, 0, 0, 0); if (n != 4) break; @@ -1557,6 +1571,41 @@ input_csi_dispatch(struct input_ctx *ictx) if (options_get_number(global_options, "extended-keys") == 2) screen_write_mode_set(sctx, MODE_KEYS_EXTENDED); break; + case INPUT_CSI_KITTY_QUERY: + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + break; + input_reply(ictx, 1, "\033[?%uu", s->kitty_keys.flags); + break; + case INPUT_CSI_KITTY_SET: + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + break; + n = input_get(ictx, 0, 0, 0); + m = input_get(ictx, 1, 1, 1); + if (n >= 0 && m >= 1 && m <= 3) + input_kitty_set(s, n, m); + break; + case INPUT_CSI_KITTY_PUSH: + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + break; + n = input_get(ictx, 0, 0, 0); + if (n >= 0) + input_kitty_push(s, n); + break; + case INPUT_CSI_KITTY_POP: + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + break; + n = input_get(ictx, 0, 1, 1); + if (n >= 0) + input_kitty_pop(s, n); + break; case INPUT_CSI_WINOPS: input_csi_dispatch_winops(ictx); break; diff --git a/key-string.c b/key-string.c index ac7a1bf89..6cfa09f8b 100644 --- a/key-string.c +++ b/key-string.c @@ -45,6 +45,29 @@ static const struct { { "F10", KEYC_F10|KEYC_IMPLIED_META }, { "F11", KEYC_F11|KEYC_IMPLIED_META }, { "F12", KEYC_F12|KEYC_IMPLIED_META }, + { "F13", KEYC_F13|KEYC_IMPLIED_META }, + { "F14", KEYC_F14|KEYC_IMPLIED_META }, + { "F15", KEYC_F15|KEYC_IMPLIED_META }, + { "F16", KEYC_F16|KEYC_IMPLIED_META }, + { "F17", KEYC_F17|KEYC_IMPLIED_META }, + { "F18", KEYC_F18|KEYC_IMPLIED_META }, + { "F19", KEYC_F19|KEYC_IMPLIED_META }, + { "F20", KEYC_F20|KEYC_IMPLIED_META }, + { "F21", KEYC_F21|KEYC_IMPLIED_META }, + { "F22", KEYC_F22|KEYC_IMPLIED_META }, + { "F23", KEYC_F23|KEYC_IMPLIED_META }, + { "F24", KEYC_F24|KEYC_IMPLIED_META }, + { "F25", KEYC_F25|KEYC_IMPLIED_META }, + { "F26", KEYC_F26|KEYC_IMPLIED_META }, + { "F27", KEYC_F27|KEYC_IMPLIED_META }, + { "F28", KEYC_F28|KEYC_IMPLIED_META }, + { "F29", KEYC_F29|KEYC_IMPLIED_META }, + { "F30", KEYC_F30|KEYC_IMPLIED_META }, + { "F31", KEYC_F31|KEYC_IMPLIED_META }, + { "F32", KEYC_F32|KEYC_IMPLIED_META }, + { "F33", KEYC_F33|KEYC_IMPLIED_META }, + { "F34", KEYC_F34|KEYC_IMPLIED_META }, + { "F35", KEYC_F35|KEYC_IMPLIED_META }, { "IC", KEYC_IC|KEYC_IMPLIED_META }, { "Insert", KEYC_IC|KEYC_IMPLIED_META }, { "DC", KEYC_DC|KEYC_IMPLIED_META }, @@ -122,6 +145,54 @@ static const struct { { "KPEnter", KEYC_KP_ENTER|KEYC_KEYPAD }, { "KP0", KEYC_KP_ZERO|KEYC_KEYPAD }, { "KP.", KEYC_KP_PERIOD|KEYC_KEYPAD }, + { "KP=", KEYC_KP_EQUAL|KEYC_KEYPAD }, + { "KP,", KEYC_KP_SEPARATOR|KEYC_KEYPAD }, + { "KPLeft", KEYC_KP_LEFT|KEYC_KEYPAD }, + { "KPRight", KEYC_KP_RIGHT|KEYC_KEYPAD }, + { "KPUp", KEYC_KP_UP|KEYC_KEYPAD }, + { "KPDown", KEYC_KP_DOWN|KEYC_KEYPAD }, + { "KPPageUp", KEYC_KP_PPAGE|KEYC_KEYPAD }, + { "KPPageDown", KEYC_KP_NPAGE|KEYC_KEYPAD }, + { "KPHome", KEYC_KP_HOME|KEYC_KEYPAD }, + { "KPEnd", KEYC_KP_END|KEYC_KEYPAD }, + { "KPInsert", KEYC_KP_IC|KEYC_KEYPAD }, + { "KPDelete", KEYC_KP_DC|KEYC_KEYPAD }, + { "KPBegin", KEYC_KP_BEGIN|KEYC_KEYPAD }, + + /* Other function keys. */ + { "CapsLock", KEYC_CAPSLOCK }, + { "ScrollLock", KEYC_SCROLLLOCK }, + { "NumLock", KEYC_NUMLOCK }, + { "PrintScreen", KEYC_PRINTSCREEN }, + { "Pause", KEYC_PAUSE }, + { "Menu", KEYC_MENU }, + { "MediaPlay", KEYC_MEDIA_PLAY }, + { "MediaPause", KEYC_MEDIA_PAUSE }, + { "MediaPlayPause", KEYC_MEDIA_PLAYPAUSE }, + { "MediaReverse", KEYC_MEDIA_REVERSE }, + { "MediaStop", KEYC_MEDIA_STOP }, + { "MediaFastForward", KEYC_MEDIA_FASTFORWARD }, + { "MediaRewind", KEYC_MEDIA_REWIND }, + { "MediaNext", KEYC_MEDIA_NEXT }, + { "MediaPrevious", KEYC_MEDIA_PREVIOUS }, + { "MediaRecord", KEYC_MEDIA_RECORD }, + { "VolumeDown", KEYC_VOLUME_DOWN }, + { "VolumeUp", KEYC_VOLUME_UP }, + { "VolumeMute", KEYC_VOLUME_MUTE }, + { "LeftShift", KEYC_LEFT_SHIFT }, + { "LeftControl", KEYC_LEFT_CTRL }, + { "LeftAlt", KEYC_LEFT_ALT }, + { "LeftSuper", KEYC_LEFT_SUPER }, + { "LeftHyper", KEYC_LEFT_HYPER }, + { "LeftMeta", KEYC_LEFT_META }, + { "RightShift", KEYC_RIGHT_SHIFT }, + { "RightControl", KEYC_RIGHT_CTRL }, + { "RightAlt", KEYC_RIGHT_ALT }, + { "RightSuper", KEYC_RIGHT_SUPER }, + { "RightHyper", KEYC_RIGHT_HYPER }, + { "RightMeta", KEYC_RIGHT_META }, + { "ISOLevel3Shift", KEYC_ISO_LEVEL3_SHIFT }, + { "ISOLevel5Shift", KEYC_ISO_LEVEL5_SHIFT }, /* Mouse keys. */ KEYC_MOUSE_STRING(MOUSEDOWN1, MouseDown1), @@ -226,9 +297,14 @@ key_string_get_modifiers(const char **string) modifiers |= KEYC_META; break; case 'S': - case 's': modifiers |= KEYC_SHIFT; break; + case 's': + modifiers |= KEYC_SUPER; + break; + case 'H': + modifiers |= KEYC_HYPER; + break; default: *string = NULL; return (0); @@ -349,6 +425,10 @@ key_string_lookup_key(key_code key, int with_flags) strlcat(out, "M-", sizeof out); if (key & KEYC_SHIFT) strlcat(out, "S-", sizeof out); + if (key & KEYC_SUPER) + strlcat(out, "s-", sizeof out); + if (key & KEYC_HYPER) + strlcat(out, "H-", sizeof out); key &= KEYC_MASK_KEY; /* Handle no key. */ diff --git a/options-table.c b/options-table.c index 97c0840df..e89b83411 100644 --- a/options-table.c +++ b/options-table.c @@ -104,7 +104,7 @@ static const char *options_table_extended_keys_list[] = { "off", "on", "always", NULL }; static const char *options_table_extended_keys_format_list[] = { - "csi-u", "xterm", NULL + "csi-u", "xterm", "kitty", NULL }; static const char *options_table_allow_passthrough_list[] = { "off", "on", "all", NULL diff --git a/popup.c b/popup.c index 0345fa367..c54a614e4 100644 --- a/popup.c +++ b/popup.c @@ -449,7 +449,8 @@ popup_key_cb(struct client *c, void *data, struct key_event *event) bufferevent_write(job_get_event(pd->job), buf, len); return (0); } - input_key(&pd->s, job_get_event(pd->job), event->key); + input_key(&pd->s, job_get_event(pd->job), event->key, + input_key_client_supports_extended(pd->c, event->key)); } return (0); diff --git a/regress/kitty-keys.sh b/regress/kitty-keys.sh new file mode 100644 index 000000000..7353c3af1 --- /dev/null +++ b/regress/kitty-keys.sh @@ -0,0 +1,107 @@ +#!/bin/sh + +# Test pane-side Kitty keyboard protocol state and key encoding. This uses +# synthetic protocol sequences and does not require a particular terminal. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +CONF=$(mktemp) +OUT=$(mktemp) +TMUX="$TEST_TMUX -LtestK$$ -f$CONF" + +trap 'rm -f "$CONF" "$OUT"; $TMUX kill-server 2>/dev/null' 0 1 15 + +printf '%s\n' 'set -g extended-keys on' \ + 'set -g extended-keys-format kitty' >"$CONF" + +wait_for_output() +{ + i=0 + while [ ! -s "$OUT" ] && [ "$i" -lt 50 ]; do + sleep 0.1 + i=$((i + 1)) + done + [ -s "$OUT" ] +} + +wait_for_mode() +{ + wanted=$1 + i=0 + while [ "$($TMUX display-message -pt: '#{pane_key_mode}')" != "$wanted" ] && + [ "$i" -lt 50 ]; do + sleep 0.1 + i=$((i + 1)) + done + [ "$($TMUX display-message -pt: '#{pane_key_mode}')" = "$wanted" ] +} + +check_output() +{ + expected=$1 + actual=$(tr -d ' \n' <"$OUT") + if [ "$actual" != "$expected" ]; then + echo "expected $expected, got $actual" + exit 1 + fi +} + +$TMUX new-session -d -x80 -y24 \ + "stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" || exit 1 +wait_for_mode 'Kitty 1' +$TMUX send-keys -t: C-S-a +wait_for_output +check_output 1b5b39373b3675 + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_mode 'Kitty 1' +$TMUX send-keys -t: s-a +wait_for_output +check_output 1b5b39373b3975 + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>8u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_mode 'Kitty 8' +$TMUX send-keys -t: a +wait_for_output +check_output 1b5b393775 + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>1u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_output +check_output 1b5b3f3175 + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>4;2m\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_output +check_output 1b5b3f3075 + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>1u\033[>8u\033[8u\033[<2u\033[?u'; dd bs=1 count=10 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_output +check_output 1b5b3f31751b5b3f3075 + +: >"$OUT" +$TMUX set-option -g extended-keys always +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>0u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_output +check_output 1b5b3f3175 +$TMUX set-option -g extended-keys on + +: >"$OUT" +$TMUX respawn-pane -k -t: \ + "stty raw -echo; printf '\033[>1u\033[?u\033[?1049h\033[?u\033[>8u\033[?u\033[?1049l\033[?u'; dd bs=1 count=20 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" +wait_for_output +check_output 1b5b3f31751b5b3f30751b5b3f38751b5b3f3175 + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/screen-write.c b/screen-write.c index 1b101ed94..44bfbc567 100644 --- a/screen-write.c +++ b/screen-write.c @@ -437,8 +437,11 @@ screen_write_reset(struct screen_write_ctx *ctx) s->mode = MODE_CURSOR|MODE_WRAP; - if (options_get_number(global_options, "extended-keys") == 2) + if (options_get_number(global_options, "extended-keys") == 2 && + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED; + input_kitty_reset(s); screen_write_clearscreen(ctx, 8); screen_write_set_cursor(ctx, 0, 0); diff --git a/screen.c b/screen.c index f9ca89af5..4ec75dab7 100644 --- a/screen.c +++ b/screen.c @@ -88,6 +88,8 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->default_cstyle = SCREEN_CURSOR_DEFAULT; s->mode = MODE_CURSOR; s->default_mode = 0; + memset(&s->kitty_keys, 0, sizeof s->kitty_keys); + memset(&s->saved_kitty_keys, 0, sizeof s->saved_kitty_keys); s->ccolour = -1; s->default_ccolour = -1; s->tabs = NULL; @@ -116,11 +118,13 @@ screen_reinit(struct screen *s, int check) s->mode = MODE_CURSOR|MODE_WRAP|(s->mode & MODE_CRLF); - if (options_get_number(global_options, "extended-keys") == 2) + if (options_get_number(global_options, "extended-keys") == 2 && + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED; - if (SCREEN_IS_ALTERNATE(s)) screen_alternate_off(s, NULL, 0); + input_kitty_reset(s); s->saved_cx = UINT_MAX; s->saved_cy = UINT_MAX; @@ -698,6 +702,7 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) if (SCREEN_IS_ALTERNATE(s)) return 0; + input_kitty_alternate_on(s); sx = screen_size_x(s); sy = screen_size_y(s); @@ -758,6 +763,7 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) s->cy = screen_size_y(s) - 1; return 0; } + input_kitty_alternate_off(s); /* Restore the saved grid. */ grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0, diff --git a/server-client.c b/server-client.c index 6aa0b562b..10ff1e169 100644 --- a/server-client.c +++ b/server-client.c @@ -2194,6 +2194,7 @@ server_client_reset_state(struct client *c) log_debug("%s: client %s mode %s", __func__, c->name, screen_mode_to_string(mode)); } + tty_update_kitty(tty, s); /* Reset region and margin. */ tty_region_off(tty); diff --git a/tmux.1 b/tmux.1 index 56790f9f0..b04c652d4 100644 --- a/tmux.1 +++ b/tmux.1 @@ -4907,12 +4907,18 @@ If enabled, the server will exit when there are no attached clients. .It Xo Ic extended\-keys .Op Ic on | off | always .Xc -Controls how modified keys (keys pressed together with Control, Meta, or Shift) -are reported. -This is the equivalent of the +Controls how modified keys are reported. +For the +.Ic csi\-u +and +.Ic xterm +formats, this controls the .Ic modifyOtherKeys .Xr xterm 1 resource. +For the +.Ic kitty +format, this controls the progressive keyboard protocol enhancements. .Pp When set to .Ic on , @@ -4939,11 +4945,24 @@ option and the .Ic pane_key_mode variable. .It Xo Ic extended\-keys\-format -.Op Ic csi\-u | xterm +.Op Ic csi\-u | xterm | kitty .Xc -Selects one of the two possible formats for reporting modified keys to -applications. -This is the equivalent of the +Selects the format for reporting modified keys to applications. +The +.Ic kitty +format uses the Kitty keyboard protocol and supports the Super and Hyper +modifiers. +Their key name prefixes are +.Ql s- +and +.Ql H- . +The disambiguate and report-all enhancements are supported; event types, +alternate keys and associated text are not requested. +For the +.Ic csi\-u +and +.Ic xterm +formats, this is the equivalent of the .Ic formatOtherKeys .Xr xterm 1 resource. @@ -4955,6 +4974,10 @@ and as .Ql \[ha][[65;6u when set to .Ic csi\-u . +With +.Ic kitty , +it will be reported as +.Ql \[ha][[97;6u . .It Xo Ic focus\-events .Op Ic on | off .Xc diff --git a/tmux.h b/tmux.h index 6635280e9..b247b9cbe 100644 --- a/tmux.h +++ b/tmux.h @@ -142,6 +142,8 @@ struct winlink; #define VISUAL_BOTH 2 /* Key modifier bits. */ +#define KEYC_SUPER 0x00010000000000ULL +#define KEYC_HYPER 0x00020000000000ULL #define KEYC_META 0x00100000000000ULL #define KEYC_CTRL 0x00200000000000ULL #define KEYC_SHIFT 0x00400000000000ULL @@ -374,6 +376,29 @@ enum { KEYC_F10, KEYC_F11, KEYC_F12, + KEYC_F13, + KEYC_F14, + KEYC_F15, + KEYC_F16, + KEYC_F17, + KEYC_F18, + KEYC_F19, + KEYC_F20, + KEYC_F21, + KEYC_F22, + KEYC_F23, + KEYC_F24, + KEYC_F25, + KEYC_F26, + KEYC_F27, + KEYC_F28, + KEYC_F29, + KEYC_F30, + KEYC_F31, + KEYC_F32, + KEYC_F33, + KEYC_F34, + KEYC_F35, KEYC_IC, KEYC_DC, KEYC_HOME, @@ -405,6 +430,54 @@ enum { KEYC_KP_ENTER, KEYC_KP_ZERO, KEYC_KP_PERIOD, + KEYC_KP_EQUAL, + KEYC_KP_SEPARATOR, + KEYC_KP_LEFT, + KEYC_KP_RIGHT, + KEYC_KP_UP, + KEYC_KP_DOWN, + KEYC_KP_PPAGE, + KEYC_KP_NPAGE, + KEYC_KP_HOME, + KEYC_KP_END, + KEYC_KP_IC, + KEYC_KP_DC, + KEYC_KP_BEGIN, + + /* Other function keys. */ + KEYC_CAPSLOCK, + KEYC_SCROLLLOCK, + KEYC_NUMLOCK, + KEYC_PRINTSCREEN, + KEYC_PAUSE, + KEYC_MENU, + KEYC_MEDIA_PLAY, + KEYC_MEDIA_PAUSE, + KEYC_MEDIA_PLAYPAUSE, + KEYC_MEDIA_REVERSE, + KEYC_MEDIA_STOP, + KEYC_MEDIA_FASTFORWARD, + KEYC_MEDIA_REWIND, + KEYC_MEDIA_NEXT, + KEYC_MEDIA_PREVIOUS, + KEYC_MEDIA_RECORD, + KEYC_VOLUME_DOWN, + KEYC_VOLUME_UP, + KEYC_VOLUME_MUTE, + KEYC_LEFT_SHIFT, + KEYC_LEFT_CTRL, + KEYC_LEFT_ALT, + KEYC_LEFT_SUPER, + KEYC_LEFT_HYPER, + KEYC_LEFT_META, + KEYC_RIGHT_SHIFT, + KEYC_RIGHT_CTRL, + KEYC_RIGHT_ALT, + KEYC_RIGHT_SUPER, + KEYC_RIGHT_HYPER, + KEYC_RIGHT_META, + KEYC_ISO_LEVEL3_SHIFT, + KEYC_ISO_LEVEL5_SHIFT, /* Theme reporting. */ KEYC_REPORT_DARK_THEME, @@ -702,6 +775,16 @@ enum tty_code_code { #define CURSOR_MODES (MODE_CURSOR|MODE_CURSOR_BLINKING|MODE_CURSOR_VERY_VISIBLE) #define EXTENDED_KEY_MODES (MODE_KEYS_EXTENDED|MODE_KEYS_EXTENDED_2) +/* Extended key output formats. */ +#define EXTENDED_KEYS_CSI_U 0 +#define EXTENDED_KEYS_XTERM 1 +#define EXTENDED_KEYS_KITTY 2 + +/* Supported Kitty keyboard protocol flags. */ +#define KITTY_KEY_DISAMBIGUATE 0x1 +#define KITTY_KEY_REPORT_ALL 0x8 +#define KITTY_KEY_SUPPORTED (KITTY_KEY_DISAMBIGUATE|KITTY_KEY_REPORT_ALL) + /* Mouse protocol constants. */ #define MOUSE_PARAM_MAX 0xff #define MOUSE_PARAM_UTF8_MAX 0x7ff @@ -1068,6 +1151,11 @@ struct progress_bar { /* Virtual screen. */ struct screen_sel; struct screen_titles; +struct kitty_key_state { + u_int flags; + u_int saved_flags; + int have_saved; +}; struct screen { char *title; char *path; @@ -1089,6 +1177,8 @@ struct screen { int mode; int default_mode; + struct kitty_key_state kitty_keys; + struct kitty_key_state saved_kitty_keys; u_int saved_cx; u_int saved_cy; @@ -1819,9 +1909,13 @@ struct tty { #define TTY_WAITBG 0x4000 #define TTY_BRACKETPASTE 0x8000 #define TTY_HAVESYNC 0x10000 +#define TTY_HAVEKKB 0x20000 +#define TTY_KKBPUSHED 0x40000 +#define TTY_KKBSUPPORT 0x80000 #define TTY_ALL_REQUEST_FLAGS \ - (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC) + (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC|TTY_HAVEKKB) int flags; + u_int kitty_keys; struct tty_term *term; @@ -2974,6 +3068,7 @@ int tty_open(struct tty *, char **); void tty_close(struct tty *); void tty_free(struct tty *); void tty_update_features(struct tty *); +void tty_update_kitty(struct tty *, struct screen *); void tty_set_selection(struct tty *, const char *, const char *, size_t); void tty_write(void (*)(struct tty *, const struct tty_ctx *), struct tty_ctx *); @@ -3450,10 +3545,26 @@ void input_cancel_requests(struct client *); /* input-key.c */ void input_key_build(void); -int input_key_pane(struct window_pane *, key_code, struct mouse_event *); -int input_key(struct screen *, struct bufferevent *, key_code); +int input_key_pane(struct window_pane *, key_code, struct mouse_event *, + int); +int input_key(struct screen *, struct bufferevent *, key_code, int); int input_key_get_mouse(struct screen *, struct mouse_event *, u_int, u_int, const char **, size_t *); +int input_key_client_supports_extended(struct client *, key_code); + +/* input-kitty.c */ +void input_kitty_reset(struct screen *); +void input_kitty_alternate_on(struct screen *); +void input_kitty_alternate_off(struct screen *); +void input_kitty_set(struct screen *, u_int, int); +void input_kitty_push(struct screen *, u_int); +void input_kitty_pop(struct screen *, u_int); +int input_key_kitty(struct screen *, struct bufferevent *, key_code); + +/* tty-kitty.c */ +int tty_keys_kitty(struct tty *, const char *, size_t, size_t *, + key_code *); +int tty_keys_kitty_query(struct tty *, const char *, size_t, size_t *); /* colour.c */ int colour_find_rgb(u_char, u_char, u_char); diff --git a/tty-keys.c b/tty-keys.c index 8462e0d9b..57a9cf941 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -772,6 +772,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a Kitty keyboard protocol response? */ + switch (tty_keys_kitty_query(tty, buf, len, &size)) { + case 0: /* yes */ + key = KEYC_UNKNOWN; + goto complete_key; + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + goto partial_key; + } + /* Is this a synchronized update mode response? */ switch (tty_keys_sync(tty, buf, len, &size)) { case 0: /* yes */ @@ -858,6 +869,18 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this an extended key press? */ + switch (tty_keys_kitty(tty, buf, len, &size, &key)) { + case 0: /* yes */ + goto complete_key; + case -1: /* no, or not valid */ + break; + case -2: /* yes, but not representable by tmux */ + goto discard_key; + case 1: /* partial */ + goto partial_key; + } + /* Is this an extended key press? */ switch (tty_keys_extended_key(tty, buf, len, &size, &key)) { case 0: /* yes */ @@ -1089,6 +1112,9 @@ tty_keys_extended_key(struct tty *tty, const char *buf, size_t len, utf8_char uc; *size = 0; + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + return (-1); /* First two bytes are always \033[. */ if (buf[0] != '\033') @@ -1527,6 +1553,9 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, } log_debug("%s: received primary DA %.*s", c->name, (int)*size, buf); + /* A DA response without a preceding keyboard response means no support. */ + if (~tty->flags & TTY_HAVEKKB) + tty->flags |= TTY_HAVEKKB; tty_update_features(tty); tty->flags |= TTY_HAVEDA; diff --git a/tty-kitty.c b/tty-kitty.c new file mode 100644 index 000000000..30f86a379 --- /dev/null +++ b/tty-kitty.c @@ -0,0 +1,440 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include + +#include "tmux.h" + +/* Detect support in the outside terminal and decode its key sequences. */ + +struct tty_kitty_key { + u_int number; + key_code key; +}; + +static const struct tty_kitty_key tty_kitty_keys[] = { + { 57358, KEYC_CAPSLOCK }, + { 57359, KEYC_SCROLLLOCK }, + { 57360, KEYC_NUMLOCK }, + { 57361, KEYC_PRINTSCREEN }, + { 57362, KEYC_PAUSE }, + { 57363, KEYC_MENU }, + { 57376, KEYC_F13 }, + { 57377, KEYC_F14 }, + { 57378, KEYC_F15 }, + { 57379, KEYC_F16 }, + { 57380, KEYC_F17 }, + { 57381, KEYC_F18 }, + { 57382, KEYC_F19 }, + { 57383, KEYC_F20 }, + { 57384, KEYC_F21 }, + { 57385, KEYC_F22 }, + { 57386, KEYC_F23 }, + { 57387, KEYC_F24 }, + { 57388, KEYC_F25 }, + { 57389, KEYC_F26 }, + { 57390, KEYC_F27 }, + { 57391, KEYC_F28 }, + { 57392, KEYC_F29 }, + { 57393, KEYC_F30 }, + { 57394, KEYC_F31 }, + { 57395, KEYC_F32 }, + { 57396, KEYC_F33 }, + { 57397, KEYC_F34 }, + { 57398, KEYC_F35 }, + { 57399, KEYC_KP_ZERO|KEYC_KEYPAD }, + { 57400, KEYC_KP_ONE|KEYC_KEYPAD }, + { 57401, KEYC_KP_TWO|KEYC_KEYPAD }, + { 57402, KEYC_KP_THREE|KEYC_KEYPAD }, + { 57403, KEYC_KP_FOUR|KEYC_KEYPAD }, + { 57404, KEYC_KP_FIVE|KEYC_KEYPAD }, + { 57405, KEYC_KP_SIX|KEYC_KEYPAD }, + { 57406, KEYC_KP_SEVEN|KEYC_KEYPAD }, + { 57407, KEYC_KP_EIGHT|KEYC_KEYPAD }, + { 57408, KEYC_KP_NINE|KEYC_KEYPAD }, + { 57409, KEYC_KP_PERIOD|KEYC_KEYPAD }, + { 57410, KEYC_KP_SLASH|KEYC_KEYPAD }, + { 57411, KEYC_KP_STAR|KEYC_KEYPAD }, + { 57412, KEYC_KP_MINUS|KEYC_KEYPAD }, + { 57413, KEYC_KP_PLUS|KEYC_KEYPAD }, + { 57414, KEYC_KP_ENTER|KEYC_KEYPAD }, + { 57415, KEYC_KP_EQUAL|KEYC_KEYPAD }, + { 57416, KEYC_KP_SEPARATOR|KEYC_KEYPAD }, + { 57417, KEYC_KP_LEFT|KEYC_KEYPAD }, + { 57418, KEYC_KP_RIGHT|KEYC_KEYPAD }, + { 57419, KEYC_KP_UP|KEYC_KEYPAD }, + { 57420, KEYC_KP_DOWN|KEYC_KEYPAD }, + { 57421, KEYC_KP_PPAGE|KEYC_KEYPAD }, + { 57422, KEYC_KP_NPAGE|KEYC_KEYPAD }, + { 57423, KEYC_KP_HOME|KEYC_KEYPAD }, + { 57424, KEYC_KP_END|KEYC_KEYPAD }, + { 57425, KEYC_KP_IC|KEYC_KEYPAD }, + { 57426, KEYC_KP_DC|KEYC_KEYPAD }, + { 57427, KEYC_KP_BEGIN|KEYC_KEYPAD }, + { 57428, KEYC_MEDIA_PLAY }, + { 57429, KEYC_MEDIA_PAUSE }, + { 57430, KEYC_MEDIA_PLAYPAUSE }, + { 57431, KEYC_MEDIA_REVERSE }, + { 57432, KEYC_MEDIA_STOP }, + { 57433, KEYC_MEDIA_FASTFORWARD }, + { 57434, KEYC_MEDIA_REWIND }, + { 57435, KEYC_MEDIA_NEXT }, + { 57436, KEYC_MEDIA_PREVIOUS }, + { 57437, KEYC_MEDIA_RECORD }, + { 57438, KEYC_VOLUME_DOWN }, + { 57439, KEYC_VOLUME_UP }, + { 57440, KEYC_VOLUME_MUTE }, + { 57441, KEYC_LEFT_SHIFT }, + { 57442, KEYC_LEFT_CTRL }, + { 57443, KEYC_LEFT_ALT }, + { 57444, KEYC_LEFT_SUPER }, + { 57445, KEYC_LEFT_HYPER }, + { 57446, KEYC_LEFT_META }, + { 57447, KEYC_RIGHT_SHIFT }, + { 57448, KEYC_RIGHT_CTRL }, + { 57449, KEYC_RIGHT_ALT }, + { 57450, KEYC_RIGHT_SUPER }, + { 57451, KEYC_RIGHT_HYPER }, + { 57452, KEYC_RIGHT_META }, + { 57453, KEYC_ISO_LEVEL3_SHIFT }, + { 57454, KEYC_ISO_LEVEL5_SHIFT } +}; + +static int +tty_kitty_parse_number(const char *s, u_int *value, int empty) +{ + char *end; + unsigned long number; + + if (*s == '\0') { + if (!empty) + return (-1); + return (0); + } + if (!isdigit((u_char)*s)) + return (-1); + number = strtoul(s, &end, 10); + if (*end != '\0' || number > UINT_MAX) + return (-1); + *value = number; + return (0); +} + +static int +tty_kitty_parse_list(char *s, u_int *first, u_int *second, int empty, + u_int maximum) +{ + char *copy, *item; + u_int n = 0, value; + int result = 0; + + copy = s; + while ((item = strsep(©, ":")) != NULL) { + if (maximum != 0 && n == maximum) + return (-1); + value = 0; + if (tty_kitty_parse_number(item, &value, empty) != 0) { + result = -1; + break; + } + if (n == 0 && *item != '\0') + *first = value; + else if (n == 1 && *item != '\0') + *second = value; + n++; + } + return (result); +} + +static int +tty_kitty_find_end(const char *buf, size_t len, size_t *end) +{ + size_t i; + + for (i = 2; i < len; i++) { + if (buf[i] == 'u' || buf[i] == '~' || buf[i] == 'A' || + buf[i] == 'B' || buf[i] == 'C' || buf[i] == 'D' || + buf[i] == 'E' || buf[i] == 'F' || buf[i] == 'H' || + buf[i] == 'P' || buf[i] == 'Q' || buf[i] == 'S') { + *end = i; + return (0); + } + if (!isdigit((u_char)buf[i]) && buf[i] != ';' && buf[i] != ':') + return (-1); + } + return (1); +} + +static key_code +tty_kitty_function(u_int number, char final) +{ + u_int i; + + if (final == 'u') { + switch (number) { + case 9: + return (C0_HT); + case 13: + return (C0_CR); + case 27: + return (C0_ESC); + case 127: + return (KEYC_BSPACE); + } + for (i = 0; i < nitems(tty_kitty_keys); i++) { + if (tty_kitty_keys[i].number == number) + return (tty_kitty_keys[i].key); + } + return (KEYC_NONE); + } + + if (final == '~') { + switch (number) { + case 2: return (KEYC_IC); + case 3: return (KEYC_DC); + case 5: return (KEYC_PPAGE); + case 6: return (KEYC_NPAGE); + case 7: return (KEYC_HOME); + case 8: return (KEYC_END); + case 11: return (KEYC_F1); + case 12: return (KEYC_F2); + case 13: return (KEYC_F3); + case 14: return (KEYC_F4); + case 15: return (KEYC_F5); + case 17: return (KEYC_F6); + case 18: return (KEYC_F7); + case 19: return (KEYC_F8); + case 20: return (KEYC_F9); + case 21: return (KEYC_F10); + case 23: return (KEYC_F11); + case 24: return (KEYC_F12); + case 29: return (KEYC_MENU); + case 57427: return (KEYC_KP_BEGIN|KEYC_KEYPAD); + } + return (KEYC_UNKNOWN); + } + if (number != 1) + return (KEYC_UNKNOWN); + switch (final) { + case 'A': return (KEYC_UP|KEYC_CURSOR); + case 'B': return (KEYC_DOWN|KEYC_CURSOR); + case 'C': return (KEYC_RIGHT|KEYC_CURSOR); + case 'D': return (KEYC_LEFT|KEYC_CURSOR); + case 'E': return (KEYC_KP_BEGIN|KEYC_KEYPAD); + case 'F': return (KEYC_END); + case 'H': return (KEYC_HOME); + case 'P': return (KEYC_F1); + case 'Q': return (KEYC_F2); + case 'S': return (KEYC_F4); + } + return (KEYC_UNKNOWN); +} + +int +tty_keys_kitty_query(struct tty *tty, const char *buf, size_t len, + size_t *size) +{ + struct client *c = tty->client; + size_t i; + u_int flags = 0; + + *size = 0; + if (tty->flags & TTY_HAVEKKB) + return (-1); + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + return (-1); + + if (buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != '[') + return (-1); + if (len == 2) + return (1); + if (buf[2] != '?') + return (-1); + if (len == 3) + return (1); + + for (i = 3; i < len && isdigit((u_char)buf[i]); i++) { + if (flags > (UINT_MAX - (buf[i] - '0')) / 10) + return (-1); + flags = (flags * 10) + (buf[i] - '0'); + } + if (i == len) + return (1); + if (i == 3 || buf[i] != 'u') + return (-1); + + *size = i + 1; + tty->kitty_keys = flags; + tty->flags |= (TTY_HAVEKKB|TTY_KKBSUPPORT); + log_debug("%s: received Kitty keyboard flags %u", c->name, flags); + tty_update_features(tty); + return (0); +} + +void +tty_update_kitty(struct tty *tty, struct screen *s) +{ + u_int flags; + char buf[32]; + + if (options_get_number(global_options, "extended-keys") == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) { + if (tty->flags & TTY_KKBPUSHED) { + tty_puts(tty, "\033[flags &= ~TTY_KKBPUSHED; + } + return; + } + if ((tty->flags & (TTY_HAVEKKB|TTY_KKBSUPPORT)) != + (TTY_HAVEKKB|TTY_KKBSUPPORT)) + return; + + flags = KITTY_KEY_DISAMBIGUATE; + if (s != NULL && (s->kitty_keys.flags & KITTY_KEY_SUPPORTED) != 0) + flags = s->kitty_keys.flags & KITTY_KEY_SUPPORTED; + if ((tty->flags & TTY_KKBPUSHED) == 0) { + xsnprintf(buf, sizeof buf, "\033[>%uu", flags); + tty_puts(tty, buf); + tty->flags |= TTY_KKBPUSHED; + } else if (tty->kitty_keys != flags) { + xsnprintf(buf, sizeof buf, "\033[=%uu", flags); + tty_puts(tty, buf); + } + tty->kitty_keys = flags; +} + +int +tty_keys_kitty(struct tty *tty, const char *buf, size_t len, size_t *size, + key_code *key) +{ + struct client *c = tty->client; + struct utf8_data ud; + utf8_char uc; + size_t end; + char tmp[128], *copy, *field, final; + u_int number = 0, modifiers = 1, event = 1, value; + u_int fields = 0; + key_code nkey, onlykey; + int result; + + *size = 0; + if ((tty->flags & TTY_KKBPUSHED) == 0 || + options_get_number(global_options, "extended-keys-format") != + EXTENDED_KEYS_KITTY) + return (-1); + if (len == 0 || buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != '[') + return (-1); + if (len == 2) + return (1); + if (buf[2] == '?') + return (-1); + + result = tty_kitty_find_end(buf, len, &end); + if (result != 0) + return (result); + if (end - 2 >= sizeof tmp) + return (-1); + memcpy(tmp, buf + 2, end - 2); + tmp[end - 2] = '\0'; + final = buf[end]; + *size = end + 1; + + copy = tmp; + while ((field = strsep(©, ";")) != NULL) { + if (fields == 3) + return (-1); + value = 0; + if (fields == 0) { + if (*field == '\0') { + if (final == 'u') + return (-1); + number = 1; + } else if (tty_kitty_parse_list(field, &number, &value, + 1, 3) != 0) + return (-1); + } else if (fields == 1) { + if (tty_kitty_parse_list(field, &modifiers, &event, 1, + 2) != 0) + return (-1); + } else if (tty_kitty_parse_list(field, &value, &value, 1, 0) != 0) + return (-1); + fields++; + } + if (fields == 0 || number == 0 || modifiers == 0) + return (-1); + if (event == 0 || event == 3 || event > 3) + return (-2); + + modifiers--; + if (modifiers & 32) + return (-2); + nkey = tty_kitty_function(number, final); + if (nkey == KEYC_NONE) { + if (final != 'u') + return (-1); + if (number >= 57344 && number <= 63743) + return (-2); + if (number <= 0x7f) + nkey = number; + else if (utf8_fromwc(number, &ud) != UTF8_DONE || + utf8_from_data(&ud, &uc) != UTF8_DONE) + return (-2); + else + nkey = uc; + } else if (nkey == KEYC_UNKNOWN) + return (-1); + + if (modifiers & 1) + nkey |= KEYC_SHIFT; + if (modifiers & 2) + nkey |= KEYC_META|KEYC_IMPLIED_META; + if (modifiers & 4) + nkey |= KEYC_CTRL; + if (modifiers & 8) + nkey |= KEYC_SUPER; + if (modifiers & 16) + nkey |= KEYC_HYPER; + + /* Shift-Tab has a dedicated tmux key code. */ + onlykey = nkey & KEYC_MASK_KEY; + if (onlykey == C0_HT && (nkey & KEYC_SHIFT)) + nkey = KEYC_BTAB|(nkey & ~KEYC_MASK_KEY & ~KEYC_SHIFT); + + if (log_get_level() != 0) { + log_debug("%s: Kitty key %.*s is %llx (%s)", c->name, + (int)*size, buf, nkey, key_string_lookup_key(nkey, 1)); + } + *key = nkey; + return (0); +} diff --git a/tty.c b/tty.c index 9bac687eb..635582524 100644 --- a/tty.c +++ b/tty.c @@ -409,6 +409,11 @@ tty_send_requests(struct tty *tty) return; if (tty->term->flags & TERM_VT100LIKE) { + if (~tty->flags & TTY_HAVEKKB && + options_get_number(global_options, "extended-keys") != 0 && + options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + tty_puts(tty, "\033[?u"); if (~tty->flags & TTY_HAVEDA) tty_puts(tty, "\033[c"); if (~tty->flags & TTY_HAVEDA2) @@ -507,6 +512,10 @@ tty_stop_tty(struct tty *tty) tty_raw(tty, "\033[?7727l"); tty_raw(tty, tty_term_string(tty->term, TTYC_DSFCS)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSEKS)); + if (tty->flags & TTY_KKBPUSHED) { + tty_raw(tty, "\033[flags &= ~TTY_KKBPUSHED; + } if (tty_use_margin(tty)) tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); @@ -559,8 +568,12 @@ tty_update_features(struct tty *tty) if (tty_use_margin(tty)) tty_putcode(tty, TTYC_ENMG); - if (options_get_number(global_options, "extended-keys")) + if (options_get_number(global_options, "extended-keys-format") == + EXTENDED_KEYS_KITTY) + tty_puts(tty, tty_term_string(tty->term, TTYC_DSEKS)); + else if (options_get_number(global_options, "extended-keys")) tty_puts(tty, tty_term_string(tty->term, TTYC_ENEKS)); + tty_update_kitty(tty, NULL); if (options_get_number(global_options, "focus-events")) tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS)); if (tty->term->flags & TERM_VT100LIKE) diff --git a/window.c b/window.c index e0ab51f19..36854969e 100644 --- a/window.c +++ b/window.c @@ -1980,7 +1980,8 @@ window_pane_copy_paste(struct window_pane *wp, char *buf, size_t len) } static void -window_pane_copy_key(struct window_pane *wp, key_code key) +window_pane_copy_key(struct window_pane *wp, key_code key, + int extended_encoding) { struct window_pane *loop; @@ -1991,7 +1992,7 @@ window_pane_copy_key(struct window_pane *wp, key_code key) (~loop->flags & PANE_INPUTOFF) && window_pane_is_visible(loop) && options_get_number(loop->options, "synchronize-panes")) - input_key_pane(loop, key, NULL); + input_key_pane(loop, key, NULL, extended_encoding); } } @@ -2019,6 +2020,7 @@ window_pane_key(struct window_pane *wp, struct client *c, struct session *s, struct winlink *wl, key_code key, struct mouse_event *m) { struct window_mode_entry *wme; + int extended_encoding; if (KEYC_IS_MOUSE(key) && m == NULL) return (-1); @@ -2041,13 +2043,14 @@ window_pane_key(struct window_pane *wp, struct client *c, struct session *s, if (wp->fd == -1 || wp->flags & PANE_INPUTOFF) return (0); - if (input_key_pane(wp, key, m) != 0) + extended_encoding = input_key_client_supports_extended(c, key); + if (input_key_pane(wp, key, m, extended_encoding) != 0) return (-1); if (KEYC_IS_MOUSE(key)) return (0); if (options_get_number(wp->options, "synchronize-panes")) - window_pane_copy_key(wp, key); + window_pane_copy_key(wp, key, extended_encoding); return (0); }