Compare commits

..

6 Commits

Author SHA1 Message Date
Michael Grant
1abf9f7023 Add a test for Kitty keyboard protocol input parsing
Also note that kitty-keys-terminal.sh cannot run under the regress
Makefile, which uses env -i and so removes DISPLAY and HOME, and make the
Kitty test file modes match the rest of regress.
2026-09-17 06:16:11 +01:00
Michael Grant
cb80f01460 Cleanup. 2026-09-17 06:00:13 +01:00
Michael Grant
e2334c3205 Document Kitty query snapshot semantics 2026-09-15 06:14:43 +01:00
Michael Grant
9e88490a05 Simplify Kitty query completion flag 2026-09-15 06:11:25 +01:00
Michael Grant
10ea823da6 Make Kitty keyboard negotiation per client 2026-09-15 05:45:58 +01:00
Michael Grant
75cb580e36 Add Kitty keyboard protocol support 2026-09-14 14:14:28 +01:00
23 changed files with 1709 additions and 42 deletions

12
CHANGES
View File

@@ -111,6 +111,18 @@ CHANGES FROM 3.7c TO 3.8
* Change show-options and show-hooks to use formats and add a -F flag to each. * Change show-options and show-hooks to use formats and add a -F flag to each.
* Add support for the Kitty keyboard protocol, selected with a new kitty
value for the extended-keys-format option. This can report the Super and
Hyper modifiers (key name prefixes s- and H-) as well as Control, Meta
and Shift. The protocol is negotiated separately with each client, so
terminals with different capabilities may be attached at the same time;
clients without it use standard extended keys, or VT10x keys if those
are also unavailable, and their input is translated for applications
which have asked for Kitty encoding.
* Add a client_key_mode format giving the extended key reporting mode in
use for a client, alongside the existing pane_key_mode for a pane.
* Allow individual terminal features to be disabled by adding @ to their names * Allow individual terminal features to be disabled by adding @ to their names
in terminal-features, and add a utf8 feature for terminals which support in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output. UTF-8 output.

View File

@@ -171,6 +171,7 @@ dist_tmux_SOURCES = \
grid.c \ grid.c \
hooks.c \ hooks.c \
hyperlinks.c \ hyperlinks.c \
input-kitty.c \
input-keys.c \ input-keys.c \
input.c \ input.c \
job.c \ job.c \
@@ -212,6 +213,7 @@ dist_tmux_SOURCES = \
tty-acs.c \ tty-acs.c \
tty-draw.c \ tty-draw.c \
tty-features.c \ tty-features.c \
tty-kitty.c \
tty-keys.c \ tty-keys.c \
tty-term.c \ tty-term.c \
tty.c \ tty.c \

View File

@@ -115,15 +115,11 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
/* /*
* -c is intended to be the client where the message should be * -c is intended to be the client where the message should be
* displayed if -p is not given, but it makes sense to use it for the * displayed if -p is not given. But it makes sense to use it for the
* client formats too. If it was given explicitly, use it even when it * formats too, assuming it matches the session. If it doesn't, use the
* is not in the target session, since the formats should describe the * best client for the session.
* client which was asked for; the session, window and pane formats
* still come from the target. Otherwise it is just the current client,
* so only use it if it matches the session and fall back to the best
* client for the session if not.
*/ */
if (tc != NULL && (args_has(args, 'c') || tc->session == s)) if (tc != NULL && tc->session == s)
c = tc; c = tc;
else if (s != NULL) else if (s != NULL)
c = cmd_find_best_client(s); c = cmd_find_best_client(s);

View File

@@ -1617,6 +1617,43 @@ format_cb_client_height(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for client_key_mode. */
static void *
format_cb_client_key_mode(struct format_tree *ft)
{
struct client *c = ft->c;
const char *mode;
char *s;
int kitty, eks;
if (c == NULL || (~c->tty.flags & TTY_STARTED))
return (NULL);
if (c->tty.flags & TTY_KKBPUSHED) {
xasprintf(&s, "Kitty %u", c->tty.kitty_keys);
return (s);
}
/* This mirrors the choice made in tty_update_features(). */
kitty = ((c->tty.flags & (TTY_HAVEKKB|TTY_KKBSUPPORT)) ==
(TTY_HAVEKKB|TTY_KKBSUPPORT));
eks = (options_get_number(global_options, "extended-keys") != 0);
if (kitty && options_get_number(global_options,
"extended-keys-format") == EXTENDED_KEYS_KITTY)
eks = 0;
if (eks && tty_term_has(c->tty.term, TTYC_ENEKS))
mode = "Ext";
else
mode = "VT10x";
/* The terminal supports Kitty keys but they are not in use. */
if (kitty) {
xasprintf(&s, "%s (Kitty)", mode);
return (s);
}
return (xstrdup(mode));
}
/* Callback for client_key_table. */ /* Callback for client_key_table. */
static void * static void *
format_cb_client_key_table(struct format_tree *ft) format_cb_client_key_table(struct format_tree *ft)
@@ -2487,7 +2524,17 @@ format_cb_pane_unseen_changes(struct format_tree *ft)
static void * static void *
format_cb_pane_key_mode(struct format_tree *ft) format_cb_pane_key_mode(struct format_tree *ft)
{ {
char *s;
if (ft->wp != NULL && ft->wp->screen != NULL) { 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) { switch (ft->wp->screen->mode & EXTENDED_KEY_MODES) {
case MODE_KEYS_EXTENDED: case MODE_KEYS_EXTENDED:
return (xstrdup("Ext 1")); return (xstrdup("Ext 1"));
@@ -3642,6 +3689,9 @@ static const struct format_table_entry format_table[] = {
{ "client_height", FORMAT_TABLE_STRING, { "client_height", FORMAT_TABLE_STRING,
format_cb_client_height format_cb_client_height
}, },
{ "client_key_mode", FORMAT_TABLE_STRING,
format_cb_client_key_mode
},
{ "client_key_table", FORMAT_TABLE_STRING, { "client_key_table", FORMAT_TABLE_STRING,
format_cb_client_key_table format_cb_client_key_table
}, },

View File

@@ -395,7 +395,8 @@ input_key_build(void)
/* Translate a key code into an output key sequence for a pane. */ /* Translate a key code into an output key sequence for a pane. */
int 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) { if (log_get_level() != 0) {
log_debug("writing key 0x%llx (%s) to %%%u", key, log_debug("writing key 0x%llx (%s) to %%%u", key,
@@ -407,7 +408,20 @@ input_key_pane(struct window_pane *wp, key_code key, struct mouse_event *m)
input_key_mouse(wp, m); input_key_mouse(wp, m);
return (0); 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 (c->tty.flags & TTY_KKBPUSHED)
return (1);
return (tty_term_has(c->tty.term, TTYC_ENEKS));
} }
static void static void
@@ -464,7 +478,8 @@ input_key_extended(struct bufferevent *bev, key_code key)
} else } else
key &= KEYC_MASK_KEY; 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); xsnprintf(tmp, sizeof tmp, "\033[27;%c;%llu~", modifier, key);
else else
xsnprintf(tmp, sizeof tmp, "\033[%llu;%cu", key, modifier); xsnprintf(tmp, sizeof tmp, "\033[%llu;%cu", key, modifier);
@@ -571,7 +586,8 @@ input_key_mode1(struct bufferevent *bev, key_code key)
/* Translate a key code into an output key sequence. */ /* Translate a key code into an output key sequence. */
int 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; struct input_key_entry *ike = NULL;
key_code newkey; key_code newkey;
@@ -588,6 +604,11 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
return (0); 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? */ /* Is this backspace? */
if ((key & KEYC_MASK_KEY) == KEYC_BSPACE) { if ((key & KEYC_MASK_KEY) == KEYC_BSPACE) {
newkey = options_get_number(global_options, "backspace"); newkey = options_get_number(global_options, "backspace");
@@ -677,6 +698,11 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
log_debug("%s: ignoring key 0x%llx", __func__, key); log_debug("%s: ignoring key 0x%llx", __func__, key);
return (0); 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 * No builtin key sequence; construct an extended key sequence

327
input-kitty.c Normal file
View File

@@ -0,0 +1,327 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
*
* 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 <sys/types.h>
#include <string.h>
#include <wchar.h>
#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);
}

78
input.c
View File

@@ -162,6 +162,7 @@ static void input_set_state(struct input_ctx *,
const struct input_transition *); const struct input_transition *);
static void input_reset_cell(struct input_ctx *); static void input_reset_cell(struct input_ctx *);
static void input_report_current_theme(struct input_ctx *); static void input_report_current_theme(struct input_ctx *);
static int input_pane_supports_extended(struct window_pane *);
static void input_osc_4(struct input_ctx *, const char *); static void input_osc_4(struct input_ctx *, const char *);
static void input_osc_8(struct input_ctx *, const char *); static void input_osc_8(struct input_ctx *, const char *);
static void input_osc_9(struct input_ctx *, const char *); static void input_osc_9(struct input_ctx *, const char *);
@@ -279,6 +280,10 @@ enum input_csi_type {
INPUT_CSI_HPA, INPUT_CSI_HPA,
INPUT_CSI_ICH, INPUT_CSI_ICH,
INPUT_CSI_IL, INPUT_CSI_IL,
INPUT_CSI_KITTY_POP,
INPUT_CSI_KITTY_PUSH,
INPUT_CSI_KITTY_QUERY,
INPUT_CSI_KITTY_SET,
INPUT_CSI_MODOFF, INPUT_CSI_MODOFF,
INPUT_CSI_MODSET, INPUT_CSI_MODSET,
INPUT_CSI_QUERY, INPUT_CSI_QUERY,
@@ -344,7 +349,11 @@ static const struct input_table_entry input_csi_table[] = {
{ 'r', "", INPUT_CSI_DECSTBM }, { 'r', "", INPUT_CSI_DECSTBM },
{ 's', "", INPUT_CSI_SCP }, { 's', "", INPUT_CSI_SCP },
{ 't', "", INPUT_CSI_WINOPS }, { '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. */ /* Input transition. */
@@ -1171,6 +1180,29 @@ input_send_reply(struct input_ctx *ictx, const char *reply)
} }
} }
/* Return whether attached tty clients can provide extended keys to a pane. */
static int
input_pane_supports_extended(struct window_pane *wp)
{
struct client *c;
if (wp == NULL)
return (1);
TAILQ_FOREACH(c, &clients, entry) {
if ((c->flags & (CLIENT_TERMINAL|CLIENT_ATTACHED)) !=
(CLIENT_TERMINAL|CLIENT_ATTACHED))
continue;
if (c->flags &
(CLIENT_UNATTACHEDFLAGS|CLIENT_CONTROL|CLIENT_READONLY))
continue;
if (c->session == NULL || !session_has(c->session, wp->window))
continue;
if (!input_key_client_supports_extended(c, 0))
return (0);
}
return (1);
}
/* Reply to terminal query. */ /* Reply to terminal query. */
static void printflike(3, 4) static void printflike(3, 4)
input_reply(struct input_ctx *ictx, int add, const char *fmt, ...) input_reply(struct input_ctx *ictx, int add, const char *fmt, ...)
@@ -1525,6 +1557,9 @@ input_csi_dispatch(struct input_ctx *ictx)
screen_write_cursormove(sctx, m - 1, n - 1, 1); screen_write_cursormove(sctx, m - 1, n - 1, 1);
break; break;
case INPUT_CSI_MODSET: case INPUT_CSI_MODSET:
if (options_get_number(global_options, "extended-keys-format") ==
EXTENDED_KEYS_KITTY)
break;
n = input_get(ictx, 0, 0, 0); n = input_get(ictx, 0, 0, 0);
if (n != 4) if (n != 4)
break; break;
@@ -1544,6 +1579,9 @@ input_csi_dispatch(struct input_ctx *ictx)
screen_write_mode_set(sctx, MODE_KEYS_EXTENDED); screen_write_mode_set(sctx, MODE_KEYS_EXTENDED);
break; break;
case INPUT_CSI_MODOFF: case INPUT_CSI_MODOFF:
if (options_get_number(global_options, "extended-keys-format") ==
EXTENDED_KEYS_KITTY)
break;
n = input_get(ictx, 0, 0, 0); n = input_get(ictx, 0, 0, 0);
if (n != 4) if (n != 4)
break; break;
@@ -1557,6 +1595,44 @@ input_csi_dispatch(struct input_ctx *ictx)
if (options_get_number(global_options, "extended-keys") == 2) if (options_get_number(global_options, "extended-keys") == 2)
screen_write_mode_set(sctx, MODE_KEYS_EXTENDED); screen_write_mode_set(sctx, MODE_KEYS_EXTENDED);
break; 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;
n = s->kitty_keys.flags;
if (!input_pane_supports_extended(ictx->wp))
n = 0;
input_reply(ictx, 1, "\033[?%uu", n);
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: case INPUT_CSI_WINOPS:
input_csi_dispatch_winops(ictx); input_csi_dispatch_winops(ictx);
break; break;

View File

@@ -45,6 +45,29 @@ static const struct {
{ "F10", KEYC_F10|KEYC_IMPLIED_META }, { "F10", KEYC_F10|KEYC_IMPLIED_META },
{ "F11", KEYC_F11|KEYC_IMPLIED_META }, { "F11", KEYC_F11|KEYC_IMPLIED_META },
{ "F12", KEYC_F12|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 }, { "IC", KEYC_IC|KEYC_IMPLIED_META },
{ "Insert", KEYC_IC|KEYC_IMPLIED_META }, { "Insert", KEYC_IC|KEYC_IMPLIED_META },
{ "DC", KEYC_DC|KEYC_IMPLIED_META }, { "DC", KEYC_DC|KEYC_IMPLIED_META },
@@ -122,6 +145,54 @@ static const struct {
{ "KPEnter", KEYC_KP_ENTER|KEYC_KEYPAD }, { "KPEnter", KEYC_KP_ENTER|KEYC_KEYPAD },
{ "KP0", KEYC_KP_ZERO|KEYC_KEYPAD }, { "KP0", KEYC_KP_ZERO|KEYC_KEYPAD },
{ "KP.", KEYC_KP_PERIOD|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. */ /* Mouse keys. */
KEYC_MOUSE_STRING(MOUSEDOWN1, MouseDown1), KEYC_MOUSE_STRING(MOUSEDOWN1, MouseDown1),
@@ -226,9 +297,14 @@ key_string_get_modifiers(const char **string)
modifiers |= KEYC_META; modifiers |= KEYC_META;
break; break;
case 'S': case 'S':
case 's':
modifiers |= KEYC_SHIFT; modifiers |= KEYC_SHIFT;
break; break;
case 's':
modifiers |= KEYC_SUPER;
break;
case 'H':
modifiers |= KEYC_HYPER;
break;
default: default:
*string = NULL; *string = NULL;
return (0); return (0);
@@ -349,6 +425,10 @@ key_string_lookup_key(key_code key, int with_flags)
strlcat(out, "M-", sizeof out); strlcat(out, "M-", sizeof out);
if (key & KEYC_SHIFT) if (key & KEYC_SHIFT)
strlcat(out, "S-", sizeof out); 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; key &= KEYC_MASK_KEY;
/* Handle no key. */ /* Handle no key. */

View File

@@ -104,7 +104,7 @@ static const char *options_table_extended_keys_list[] = {
"off", "on", "always", NULL "off", "on", "always", NULL
}; };
static const char *options_table_extended_keys_format_list[] = { 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[] = { static const char *options_table_allow_passthrough_list[] = {
"off", "on", "all", NULL "off", "on", "all", NULL

View File

@@ -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); bufferevent_write(job_get_event(pd->job), buf, len);
return (0); 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); return (0);

View File

@@ -0,0 +1,144 @@
#!/bin/sh
# Test per-client keyboard protocol negotiation and pane-side fallback.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
KCONF=$(mktemp)
UCONF=$(mktemp)
LCONF=$(mktemp)
OUT=$(mktemp)
TMP=$(mktemp)
SOCKETS="testKmc$$ testKka$$ testKua$$ testKrt$$ testKru$$ testKlq$$ testKlu$$"
printf '%s\n' 'set -g extended-keys on' \
'set -g extended-keys-format kitty' >"$KCONF"
printf '%s\n' 'set -g extended-keys on' \
'set -g extended-keys-format csi-u' >"$UCONF"
printf '%s\n' 'set -g extended-keys off' >"$LCONF"
cleanup()
{
rm -f "$KCONF" "$UCONF" "$LCONF" "$OUT" "$TMP"
for socket in $SOCKETS; do
$TEST_TMUX -L"$socket" kill-server 2>/dev/null
done
}
trap cleanup 0 1 15
wait_for_mode()
{
tmux=$1
wanted=$2
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" ]
}
wait_for_output()
{
i=0
while [ ! -s "$1" ] && [ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ -s "$1" ]
}
client_mode()
{
tmux=$1
session=$2
if [ -z "$session" ]; then
$tmux list-clients -F '#{client_key_mode}'
else
$tmux list-clients -F '#{client_session}:#{client_key_mode}' |
sed -n "s/^$session://p"
fi
}
wait_for_client_mode()
{
tmux=$1
session=$2
wanted=$3
i=0
while [ "$(client_mode "$tmux" "$session")" != "$wanted" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$(client_mode "$tmux" "$session")" = "$wanted" ]
}
# A server preferring Kitty must negotiate each client independently.
M="$TEST_TMUX -LtestKmc$$ -f$KCONF"
KA="$TEST_TMUX -LtestKka$$ -f$KCONF"
UA="$TEST_TMUX -LtestKua$$ -f$UCONF"
$M new-session -d -x80 -y24 -s kitty || exit 1
$M new-session -d -x80 -y24 -s csiu || exit 1
$KA new-session -d -x80 -y24 "$M attach-session -t kitty" || exit 1
$UA new-session -d -x80 -y24 "$M attach-session -t csiu" || exit 1
wait_for_mode "$KA" 'Kitty 1' || exit 1
wait_for_mode "$UA" 'Ext 2' || exit 1
# Each client must report the protocol its own terminal negotiated.
wait_for_client_mode "$M" kitty 'Kitty 1' || exit 1
wait_for_client_mode "$M" csiu 'Ext' || exit 1
uc=$($M list-clients -F '#{client_name} #{client_session}' |
awk '$2 == "csiu" { print $1 }')
[ -n "$uc" ] || exit 1
$M command-prompt -t"$uc" -k 'display-message -pl "%%"' >"$TMP" &
pid=$!
sleep 0.2
$UA send-keys 'Escape [97;5u'
wait "$pid"
[ "$(tr -d '[:space:]' <"$TMP")" = 'C-a' ] || exit 1
# A Kitty-capable terminal must still be reported when it is not in use.
$M set-option -g extended-keys off
wait_for_client_mode "$M" kitty 'VT10x (Kitty)' || exit 1
wait_for_client_mode "$M" csiu 'VT10x' || exit 1
$M set-option -g extended-keys on
wait_for_client_mode "$M" kitty 'Kitty 1' || exit 1
$KA kill-server 2>/dev/null
$UA kill-server 2>/dev/null
$M kill-server 2>/dev/null
# Standard extended input must be translated for a Kitty-requesting pane.
: >"$OUT"
R="$TEST_TMUX -LtestKrt$$ -f$KCONF"
RU="$TEST_TMUX -LtestKru$$ -f$UCONF"
$R 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
$RU new-session -d -x80 -y24 "$R attach-session" || exit 1
wait_for_mode "$RU" 'Ext 2' || exit 1
wait_for_client_mode "$R" '' 'Ext' || exit 1
$RU send-keys C-a
wait_for_output "$OUT" || exit 1
[ "$(tr -d ' \n' <"$OUT")" = '1b5b39373b3575' ] || exit 1
$RU kill-server 2>/dev/null
$R kill-server 2>/dev/null
# Do not report Kitty active when an attached client only supports VT10x.
: >"$OUT"
printf '%s\n' "set -as terminal-overrides ',*:Eneks@:Dseks@'" >>"$KCONF"
LQ="$TEST_TMUX -LtestKlq$$ -f$KCONF"
LU="$TEST_TMUX -LtestKlu$$ -f$LCONF"
$LQ new-session -d -x80 -y24 \
"stty raw -echo; sleep 2; printf '\033[>1u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" || exit 1
$LU new-session -d -x80 -y24 "$LQ attach-session" || exit 1
wait_for_output "$OUT" || exit 1
[ "$(tr -d ' \n' <"$OUT")" = '1b5b3f3075' ] || exit 1
wait_for_client_mode "$LQ" '' 'VT10x' || exit 1
exit 0

119
regress/kitty-keys-input.sh Normal file
View File

@@ -0,0 +1,119 @@
#!/bin/sh
# Test client-side Kitty keyboard protocol support: an outer tmux acts as a
# Kitty-capable terminal for the inner tmux which is the one under test. The
# inner tmux queries the outer one, the outer one replies, and the inner one
# then parses the key sequences sent to it.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
CONF=$(mktemp)
TMP=$(mktemp)
printf '%s\n' 'set -g extended-keys on' \
'set -g extended-keys-format kitty' >"$CONF"
TMUX="$TEST_TMUX -LtestKIA$$ -f$CONF"
TMUX2="$TEST_TMUX -LtestKIB$$ -f$CONF"
trap 'rm -f "$CONF" "$TMP"; $TMUX kill-server 2>/dev/null; \
$TMUX2 kill-server 2>/dev/null' 0 1 15
$TMUX2 new-session -d -x80 -y24 || exit 1
$TMUX new-session -d -x80 -y24 "$TMUX2 attach" || exit 1
exit_status=0
wait_for_mode()
{
i=0
while [ "$($TMUX display-message -pt: '#{pane_key_mode}')" != "$1" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($TMUX display-message -pt: '#{pane_key_mode}')" = "$1" ]
}
assert_key()
{
keys=$1
expected=$2
$TMUX2 command-prompt -k 'display-message -pl "%%"' >"$TMP" &
sleep 0.15
$TMUX send-keys $keys
wait
actual=$(tr -d '[:space:]' <"$TMP")
if [ "$actual" = "$expected" ]; then
[ -n "$VERBOSE" ] && echo "[PASS] $keys -> $actual"
else
echo "[FAIL] $keys -> $expected (got '$actual')"
exit_status=1
fi
}
# The inner tmux must detect the outer one and push its flags.
if ! wait_for_mode 'Kitty 1'; then
echo "[FAIL] inner tmux did not enable the Kitty protocol"
exit 1
fi
# The handshake is done, so stop the outer tmux encoding the raw sequences
# below as Kitty keys itself.
$TMUX set-option -g extended-keys-format csi-u
# Modifiers, including the Kitty-only Super and Hyper.
assert_key 'Escape [97;2u' 'S-a'
assert_key 'Escape [97;3u' 'M-a'
assert_key 'Escape [97;5u' 'C-a'
assert_key 'Escape [97;6u' 'C-S-a'
assert_key 'Escape [97;7u' 'C-M-a'
assert_key 'Escape [97;9u' 's-a'
assert_key 'Escape [97;17u' 'H-a'
assert_key 'Escape [65;2u' 'S-A'
# Keys with a dedicated tmux key code.
assert_key 'Escape [9;5u' 'C-Tab'
assert_key 'Escape [9;2u' 'BTab'
assert_key 'Escape [13;5u' 'C-Enter'
assert_key 'Escape [27u' 'Escape'
assert_key 'Escape [32;5u' 'C-Space'
assert_key 'Escape [127;5u' 'C-BSpace'
# Keys with a CSI final byte other than u.
assert_key 'Escape [1;5A' 'C-Up'
assert_key 'Escape [1;3D' 'M-Left'
assert_key 'Escape [1;5H' 'C-Home'
assert_key 'Escape [1;5P' 'C-F1'
# Functional and non-ASCII keys.
assert_key 'Escape [57399;5u' 'C-KP0'
assert_key 'Escape [233;5u' 'C-é'
# The alternate key and associated text fields are not requested but must be
# ignored if a terminal sends them anyway.
assert_key 'Escape [97:65;2u' 'S-a'
assert_key 'Escape [97:65:97;5u' 'C-a'
assert_key 'Escape [97;5;97u' 'C-a'
# A repeat event is a key press, a release event is dropped. The C-b that
# follows shows that the sequence was consumed and not passed through.
assert_key 'Escape [97;5:1u' 'C-a'
assert_key 'Escape [97;5:2u' 'C-a'
assert_key 'Escape [97;5:3u Escape [98;5u' 'C-b'
# The Meta modifier bit is not representable and is dropped.
assert_key 'Escape [97;33u Escape [98;5u' 'C-b'
# Lock modifiers are ignored rather than dropping the key.
assert_key 'Escape [97;65u' 'a'
assert_key 'Escape [97;129u' 'a'
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,85 @@
#!/bin/sh
# Test the Kitty keyboard protocol against a real terminal rather than against
# another copy of tmux. This drives kitty through its remote control interface
# so that kitty itself encodes the key presses.
#
# This has to be run by hand from this directory. The Makefile runs tests with
# env -i, which removes DISPLAY and HOME, so kitty cannot start and this exits
# immediately without testing anything. It is also skipped unless kitty is
# installed and is new enough to have send-key.
PATH=/bin:/usr/bin
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
command -v kitty >/dev/null 2>&1 || exit 0
[ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ] || exit 0
kitty @ --help 2>/dev/null | grep -q 'send-key' || exit 0
DIR=$(mktemp -d)
CONF=$DIR/conf
SOCKET=$DIR/kitty
OUT=$DIR/out
TMUX="$TEST_TMUX -LtestKrt$$ -f$CONF"
printf '%s\n' 'set -g extended-keys on' \
'set -g extended-keys-format kitty' >"$CONF"
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
kitty -o allow_remote_control=yes --listen-on "unix:$SOCKET" \
-o confirm_os_window_close=0 -e \
$TMUX new-session -s r "sleep 120" >"$DIR/err" 2>&1 &
# The terminfo entry for kitty may be missing, so treat a server which never
# appears as a reason to skip rather than as a failure.
i=0
while ! $TMUX has-session -t r 2>/dev/null && [ "$i" -lt 150 ]; do
sleep 0.1
i=$((i + 1))
done
$TMUX has-session -t r 2>/dev/null || exit 0
# The client must negotiate the protocol with the real terminal.
i=0
while [ "$($TMUX list-clients -F '#{client_key_mode}')" != 'Kitty 1' ] &&
[ "$i" -lt 100 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($TMUX list-clients -F '#{client_key_mode}')" = 'Kitty 1' ] || exit 1
# kitty encodes the key, tmux parses it and encodes it again for the pane.
try_key()
{
key=$1
expected=$2
: >"$OUT"
$TMUX respawn-pane -k -t r: \
"stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 20" || return 1
sleep 1
kitty @ --to "unix:$SOCKET" send-key "$key" 2>/dev/null || return 1
i=0
while [ ! -s "$OUT" ] && [ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$(tr -d ' \n' <"$OUT")" = "$expected" ]
}
try_key ctrl+a '1b5b39373b3575' || exit 1
try_key alt+a '1b5b39373b3375' || exit 1
try_key ctrl+shift+a '1b5b39373b3675' || exit 1
# Super cannot be represented by the standard extended keys at all, so this
# only succeeds if the whole chain used the Kitty protocol.
try_key super+a '1b5b39373b3975' || exit 1
exit 0

107
regress/kitty-keys.sh Normal file
View File

@@ -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[<u\033[?u\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

View File

@@ -437,8 +437,11 @@ screen_write_reset(struct screen_write_ctx *ctx)
s->mode = MODE_CURSOR|MODE_WRAP; 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; s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED;
input_kitty_reset(s);
screen_write_clearscreen(ctx, 8); screen_write_clearscreen(ctx, 8);
screen_write_set_cursor(ctx, 0, 0); screen_write_set_cursor(ctx, 0, 0);

View File

@@ -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->default_cstyle = SCREEN_CURSOR_DEFAULT;
s->mode = MODE_CURSOR; s->mode = MODE_CURSOR;
s->default_mode = 0; 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->ccolour = -1;
s->default_ccolour = -1; s->default_ccolour = -1;
s->tabs = NULL; s->tabs = NULL;
@@ -116,11 +118,13 @@ screen_reinit(struct screen *s, int check)
s->mode = MODE_CURSOR|MODE_WRAP|(s->mode & MODE_CRLF); 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; s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED;
if (SCREEN_IS_ALTERNATE(s)) if (SCREEN_IS_ALTERNATE(s))
screen_alternate_off(s, NULL, 0); screen_alternate_off(s, NULL, 0);
input_kitty_reset(s);
s->saved_cx = UINT_MAX; s->saved_cx = UINT_MAX;
s->saved_cy = 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)) if (SCREEN_IS_ALTERNATE(s))
return 0; return 0;
input_kitty_alternate_on(s);
sx = screen_size_x(s); sx = screen_size_x(s);
sy = screen_size_y(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; s->cy = screen_size_y(s) - 1;
return 0; return 0;
} }
input_kitty_alternate_off(s);
/* Restore the saved grid. */ /* Restore the saved grid. */
grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0, grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0,

View File

@@ -2194,6 +2194,7 @@ server_client_reset_state(struct client *c)
log_debug("%s: client %s mode %s", __func__, c->name, log_debug("%s: client %s mode %s", __func__, c->name,
screen_mode_to_string(mode)); screen_mode_to_string(mode));
} }
tty_update_kitty(tty, s);
/* Reset region and margin. */ /* Reset region and margin. */
tty_region_off(tty); tty_region_off(tty);

63
tmux.1
View File

@@ -4907,12 +4907,18 @@ If enabled, the server will exit when there are no attached clients.
.It Xo Ic extended\-keys .It Xo Ic extended\-keys
.Op Ic on | off | always .Op Ic on | off | always
.Xc .Xc
Controls how modified keys (keys pressed together with Control, Meta, or Shift) Controls how modified keys are reported.
are reported. For the
This is the equivalent of the .Ic csi\-u
and
.Ic xterm
formats, this controls the
.Ic modifyOtherKeys .Ic modifyOtherKeys
.Xr xterm 1 .Xr xterm 1
resource. resource.
For the
.Ic kitty
format, this controls the progressive keyboard protocol enhancements.
.Pp .Pp
When set to When set to
.Ic on , .Ic on ,
@@ -4923,12 +4929,25 @@ When set to
.Ic always , .Ic always ,
modes 1 and 2 can still be requested by applications, but mode 1 will be forced modes 1 and 2 can still be requested by applications, but mode 1 will be forced
instead of the standard mode. instead of the standard mode.
This applies only to applications inside panes and does not change what
.Nm
requests from the terminal.
When set to When set to
.Ic off , .Ic off ,
this feature is disabled and only standard keys are reported. this feature is disabled and only standard keys are reported.
.Pp .Pp
.Nm .Nm
will always request extended keys itself if the terminal supports them. will always request extended keys itself if the terminal supports them.
When the
.Ic kitty
format is selected, it is preferred separately for each client; clients which
do not support it use standard extended keys if available, or standard VT10x
keys otherwise.
Input from standard extended-key clients is translated to Kitty encoding for
applications which have requested it.
A Kitty keyboard state query reflects the clients attached to the window at
the time of the query; if none are attached, the requested state is reported.
Applications are not notified if client support changes after the reply.
See also the See also the
.Ic extkeys .Ic extkeys
feature for the feature for the
@@ -4937,13 +4956,28 @@ option, the
.Ic extended\-keys\-format .Ic extended\-keys\-format
option and the option and the
.Ic pane_key_mode .Ic pane_key_mode
variable. and
.Ic client_key_mode
variables.
.It Xo Ic extended\-keys\-format .It Xo Ic extended\-keys\-format
.Op Ic csi\-u | xterm .Op Ic csi\-u | xterm | kitty
.Xc .Xc
Selects one of the two possible formats for reporting modified keys to Selects the format for reporting modified keys to applications.
applications. The
This is the equivalent of 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 .Ic formatOtherKeys
.Xr xterm 1 .Xr xterm 1
resource. resource.
@@ -4955,6 +4989,10 @@ and as
.Ql \[ha][[65;6u .Ql \[ha][[65;6u
when set to when set to
.Ic csi\-u . .Ic csi\-u .
With
.Ic kitty ,
it will be reported as
.Ql \[ha][[97;6u .
.It Xo Ic focus\-events .It Xo Ic focus\-events
.Op Ic on | off .Op Ic on | off
.Xc .Xc
@@ -7309,6 +7347,7 @@ The following variables are available, where appropriate:
.It Li "client_discarded" Ta "" Ta "Bytes discarded when client behind" .It Li "client_discarded" Ta "" Ta "Bytes discarded when client behind"
.It Li "client_flags" Ta "" Ta "List of client flags" .It Li "client_flags" Ta "" Ta "List of client flags"
.It Li "client_height" Ta "" Ta "Height of client" .It Li "client_height" Ta "" Ta "Height of client"
.It Li "client_key_mode" Ta "" Ta "Extended key reporting mode for this client"
.It Li "client_key_table" Ta "" Ta "Current key table" .It Li "client_key_table" Ta "" Ta "Current key table"
.It Li "client_last_session" Ta "" Ta "Name of the client's last session" .It Li "client_last_session" Ta "" Ta "Name of the client's last session"
.It Li "client_name" Ta "" Ta "Name of client" .It Li "client_name" Ta "" Ta "Name of client"
@@ -8328,17 +8367,11 @@ Otherwise, the format of
.Ar message .Ar message
is described in the is described in the
.Sx FORMATS .Sx FORMATS
section; session, window and pane information is taken from section; information is taken from
.Ar target\-pane .Ar target\-pane
if if
.Fl t .Fl t
is given, otherwise the active pane. is given, otherwise the active pane.
If
.Fl c
is given, the
.Ql #{client_*}
variables are taken from
.Ar target\-client .
.Pp .Pp
If If
.Fl j .Fl j

117
tmux.h
View File

@@ -142,6 +142,8 @@ struct winlink;
#define VISUAL_BOTH 2 #define VISUAL_BOTH 2
/* Key modifier bits. */ /* Key modifier bits. */
#define KEYC_SUPER 0x00010000000000ULL
#define KEYC_HYPER 0x00020000000000ULL
#define KEYC_META 0x00100000000000ULL #define KEYC_META 0x00100000000000ULL
#define KEYC_CTRL 0x00200000000000ULL #define KEYC_CTRL 0x00200000000000ULL
#define KEYC_SHIFT 0x00400000000000ULL #define KEYC_SHIFT 0x00400000000000ULL
@@ -374,6 +376,29 @@ enum {
KEYC_F10, KEYC_F10,
KEYC_F11, KEYC_F11,
KEYC_F12, 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_IC,
KEYC_DC, KEYC_DC,
KEYC_HOME, KEYC_HOME,
@@ -405,6 +430,54 @@ enum {
KEYC_KP_ENTER, KEYC_KP_ENTER,
KEYC_KP_ZERO, KEYC_KP_ZERO,
KEYC_KP_PERIOD, 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. */ /* Theme reporting. */
KEYC_REPORT_DARK_THEME, 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 CURSOR_MODES (MODE_CURSOR|MODE_CURSOR_BLINKING|MODE_CURSOR_VERY_VISIBLE)
#define EXTENDED_KEY_MODES (MODE_KEYS_EXTENDED|MODE_KEYS_EXTENDED_2) #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. */ /* Mouse protocol constants. */
#define MOUSE_PARAM_MAX 0xff #define MOUSE_PARAM_MAX 0xff
#define MOUSE_PARAM_UTF8_MAX 0x7ff #define MOUSE_PARAM_UTF8_MAX 0x7ff
@@ -1068,6 +1151,11 @@ struct progress_bar {
/* Virtual screen. */ /* Virtual screen. */
struct screen_sel; struct screen_sel;
struct screen_titles; struct screen_titles;
struct kitty_key_state {
u_int flags;
u_int saved_flags;
int have_saved;
};
struct screen { struct screen {
char *title; char *title;
char *path; char *path;
@@ -1089,6 +1177,8 @@ struct screen {
int mode; int mode;
int default_mode; int default_mode;
struct kitty_key_state kitty_keys;
struct kitty_key_state saved_kitty_keys;
u_int saved_cx; u_int saved_cx;
u_int saved_cy; u_int saved_cy;
@@ -1819,9 +1909,13 @@ struct tty {
#define TTY_WAITBG 0x4000 #define TTY_WAITBG 0x4000
#define TTY_BRACKETPASTE 0x8000 #define TTY_BRACKETPASTE 0x8000
#define TTY_HAVESYNC 0x10000 #define TTY_HAVESYNC 0x10000
#define TTY_HAVEKKB 0x20000
#define TTY_KKBPUSHED 0x40000
#define TTY_KKBSUPPORT 0x80000
#define TTY_ALL_REQUEST_FLAGS \ #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; int flags;
u_int kitty_keys;
struct tty_term *term; struct tty_term *term;
@@ -2974,6 +3068,7 @@ int tty_open(struct tty *, char **);
void tty_close(struct tty *); void tty_close(struct tty *);
void tty_free(struct tty *); void tty_free(struct tty *);
void tty_update_features(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_set_selection(struct tty *, const char *, const char *, size_t);
void tty_write(void (*)(struct tty *, const struct tty_ctx *), void tty_write(void (*)(struct tty *, const struct tty_ctx *),
struct tty_ctx *); struct tty_ctx *);
@@ -3450,10 +3545,26 @@ void input_cancel_requests(struct client *);
/* input-key.c */ /* input-key.c */
void input_key_build(void); void input_key_build(void);
int input_key_pane(struct window_pane *, key_code, struct mouse_event *); int input_key_pane(struct window_pane *, key_code, struct mouse_event *,
int input_key(struct screen *, struct bufferevent *, key_code); int);
int input_key(struct screen *, struct bufferevent *, key_code, int);
int input_key_get_mouse(struct screen *, struct mouse_event *, u_int, int input_key_get_mouse(struct screen *, struct mouse_event *, u_int,
u_int, const char **, size_t *); 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 */ /* colour.c */
int colour_find_rgb(u_char, u_char, u_char); int colour_find_rgb(u_char, u_char, u_char);

View File

@@ -772,6 +772,17 @@ tty_keys_next(struct tty *tty)
goto partial_key; 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? */ /* Is this a synchronized update mode response? */
switch (tty_keys_sync(tty, buf, len, &size)) { switch (tty_keys_sync(tty, buf, len, &size)) {
case 0: /* yes */ case 0: /* yes */
@@ -858,6 +869,18 @@ tty_keys_next(struct tty *tty)
goto partial_key; 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? */ /* Is this an extended key press? */
switch (tty_keys_extended_key(tty, buf, len, &size, &key)) { switch (tty_keys_extended_key(tty, buf, len, &size, &key)) {
case 0: /* yes */ case 0: /* yes */
@@ -1089,6 +1112,8 @@ tty_keys_extended_key(struct tty *tty, const char *buf, size_t len,
utf8_char uc; utf8_char uc;
*size = 0; *size = 0;
if (tty->flags & TTY_KKBPUSHED)
return (-1);
/* First two bytes are always \033[. */ /* First two bytes are always \033[. */
if (buf[0] != '\033') if (buf[0] != '\033')
@@ -1527,6 +1552,8 @@ 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); log_debug("%s: received primary DA %.*s", c->name, (int)*size, buf);
/* A DA response without a preceding keyboard response means no support. */
tty->flags |= TTY_HAVEKKB;
tty_update_features(tty); tty_update_features(tty);
tty->flags |= TTY_HAVEDA; tty->flags |= TTY_HAVEDA;

440
tty-kitty.c Normal file
View File

@@ -0,0 +1,440 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
*
* 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 <sys/types.h>
#include <ctype.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#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(&copy, ":")) != 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[<u");
tty->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(&copy, ";")) != 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);
}

20
tty.c
View File

@@ -409,6 +409,11 @@ tty_send_requests(struct tty *tty)
return; return;
if (tty->term->flags & TERM_VT100LIKE) { 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) if (~tty->flags & TTY_HAVEDA)
tty_puts(tty, "\033[c"); tty_puts(tty, "\033[c");
if (~tty->flags & TTY_HAVEDA2) if (~tty->flags & TTY_HAVEDA2)
@@ -507,6 +512,10 @@ tty_stop_tty(struct tty *tty)
tty_raw(tty, "\033[?7727l"); tty_raw(tty, "\033[?7727l");
tty_raw(tty, tty_term_string(tty->term, TTYC_DSFCS)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSFCS));
tty_raw(tty, tty_term_string(tty->term, TTYC_DSEKS)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSEKS));
if (tty->flags & TTY_KKBPUSHED) {
tty_raw(tty, "\033[<u");
tty->flags &= ~TTY_KKBPUSHED;
}
if (tty_use_margin(tty)) if (tty_use_margin(tty))
tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG));
@@ -553,14 +562,23 @@ void
tty_update_features(struct tty *tty) tty_update_features(struct tty *tty)
{ {
struct client *c = tty->client; struct client *c = tty->client;
int extended_keys, format;
if (tty_apply_features(tty->term)) if (tty_apply_features(tty->term))
tty_term_apply_overrides(tty->term); tty_term_apply_overrides(tty->term);
if (tty_use_margin(tty)) if (tty_use_margin(tty))
tty_putcode(tty, TTYC_ENMG); tty_putcode(tty, TTYC_ENMG);
if (options_get_number(global_options, "extended-keys")) extended_keys = options_get_number(global_options, "extended-keys");
format = options_get_number(global_options, "extended-keys-format");
if (extended_keys == 0 ||
(format == EXTENDED_KEYS_KITTY &&
(tty->flags & (TTY_HAVEKKB|TTY_KKBSUPPORT)) ==
(TTY_HAVEKKB|TTY_KKBSUPPORT)))
tty_puts(tty, tty_term_string(tty->term, TTYC_DSEKS));
else
tty_puts(tty, tty_term_string(tty->term, TTYC_ENEKS)); tty_puts(tty, tty_term_string(tty->term, TTYC_ENEKS));
tty_update_kitty(tty, NULL);
if (options_get_number(global_options, "focus-events")) if (options_get_number(global_options, "focus-events"))
tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS)); tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS));
if (tty->term->flags & TERM_VT100LIKE) if (tty->term->flags & TERM_VT100LIKE)

View File

@@ -1980,7 +1980,8 @@ window_pane_copy_paste(struct window_pane *wp, char *buf, size_t len)
} }
static void 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; struct window_pane *loop;
@@ -1991,7 +1992,7 @@ window_pane_copy_key(struct window_pane *wp, key_code key)
(~loop->flags & PANE_INPUTOFF) && (~loop->flags & PANE_INPUTOFF) &&
window_pane_is_visible(loop) && window_pane_is_visible(loop) &&
options_get_number(loop->options, "synchronize-panes")) 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 winlink *wl, key_code key, struct mouse_event *m)
{ {
struct window_mode_entry *wme; struct window_mode_entry *wme;
int extended_encoding;
if (KEYC_IS_MOUSE(key) && m == NULL) if (KEYC_IS_MOUSE(key) && m == NULL)
return (-1); 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) if (wp->fd == -1 || wp->flags & PANE_INPUTOFF)
return (0); 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); return (-1);
if (KEYC_IS_MOUSE(key)) if (KEYC_IS_MOUSE(key))
return (0); return (0);
if (options_get_number(wp->options, "synchronize-panes")) if (options_get_number(wp->options, "synchronize-panes"))
window_pane_copy_key(wp, key); window_pane_copy_key(wp, key, extended_encoding);
return (0); return (0);
} }