Compare commits

..

7 Commits

Author SHA1 Message Date
Michael Grant
8c96fde851 Detection, done like sync:
- ESC[?u goes to every VT100-like terminal at attach, whatever the
  options.
- A reply turns on a new kittykeys terminal feature. Users can force
  it on or off with terminal-features kittykeys, including kittykeys@
  to turn it off.
- Changing extended-keys now takes effect immediately for attached
  clients (options.c).

Use, per client, when extended-keys is on: Kitty for clients that have
kittykeys, modifyOtherKeys for clients with extkeys, VT10x otherwise.

Pane side:

- Applications can request either protocol; Kitty wins if one asks for
  both.
- Each application gets what it asked for, whichever client typed the
  key.
- ESC[?u is answered truthfully.
- The per-client gate is gone. window.c and popup.c are simply
  master's versions again.

extended-keys always is back to its master meaning, forcing
modifyOtherKeys mode 1 only. Forcing Kitty on every program would have
sent bash ESC[97;5u for Ctrl-A.

extended-keys-format kitty is still accepted, but now behaves the same
as csi-u.
2026-09-19 07:32:57 +01:00
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
33 changed files with 1682 additions and 2101 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.
* Add support for the Kitty keyboard protocol. When extended-keys is on,
tmux uses it with terminals which support it, detected when a client
attaches or set with a new kittykeys terminal feature, and standard
extended keys or VT10x keys with other terminals, so terminals with
different capabilities may be attached at the same time. Programs inside
tmux may ask for either the Kitty protocol or modifyOtherKeys and get keys
in the form they asked for. The Kitty protocol can report the Super and
Hyper modifiers (key name prefixes s- and H-).
* 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
in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output.

View File

@@ -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 \

View File

@@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = {
.name = "copy-mode",
.alias = NULL,
.args = { "UcdekHMqSs:t:u", 0, 0, NULL },
.usage = "[-UcdekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE,
.args = { "dekHMqSs:t:u", 0, 0, NULL },
.usage = "[-dekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE,
.source = { 's', CMD_FIND_PANE, 0 },
.target = { 't', CMD_FIND_PANE, 0 },

View File

@@ -1617,6 +1617,41 @@ format_cb_client_height(struct format_tree *ft)
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.term->flags & TERM_KITTYKEYS) != 0);
eks = (options_get_number(global_options, "extended-keys") != 0);
if (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. */
static void *
format_cb_client_key_table(struct format_tree *ft)
@@ -2487,7 +2522,14 @@ 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 (ft->wp->screen->kitty_keys.flags != 0) {
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"));
@@ -3642,6 +3684,9 @@ static const struct format_table_entry format_table[] = {
{ "client_height", FORMAT_TABLE_STRING,
format_cb_client_height
},
{ "client_key_mode", FORMAT_TABLE_STRING,
format_cb_client_key_mode
},
{ "client_key_table", FORMAT_TABLE_STRING,
format_cb_client_key_table
},

2
grid.c
View File

@@ -1760,8 +1760,6 @@ grid_line_flags_string(int flags)
strlcat(s, "START_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT)
strlcat(s, "END_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT_STATUS)
strlcat(s, "END_OUTPUT_STATUS,", sizeof s);
if (flags & GRID_LINE_HYPERLINK)
strlcat(s, "HYPERLINK,", sizeof s);
if (*s == '\0')

View File

@@ -464,7 +464,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);
@@ -588,6 +589,10 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
return (0);
}
/* Kitty keys take precedence if the application asked for them. */
if (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 +682,10 @@ 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 (s->kitty_keys.flags & KITTY_KEY_SUPPORTED)
return (input_key_vt10x(bev, key));
/*
* No builtin key sequence; construct an extended key sequence

313
input-kitty.c Normal file
View File

@@ -0,0 +1,313 @@
/* $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' }
};
void
input_kitty_reset(struct screen *s)
{
memset(&s->kitty_keys, 0, sizeof s->kitty_keys);
}
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;
}
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;
}
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 = 0;
s->kitty_keys.have_saved = 0;
}
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);
}

100
input.c
View File

@@ -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. */
@@ -1557,6 +1565,33 @@ 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)
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)
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)
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)
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;
@@ -3194,14 +3229,13 @@ input_osc_112(struct input_ctx *ictx, const char *p)
/* Parse the OSC 133 D exit status. */
static int
input_osc_133_exit_status(const char *p, int *present)
input_osc_133_exit_status(const char *p)
{
const char *end;
char *copy;
char *endptr;
const char *errstr;
long long status;
*present = 0;
if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2)
return (0);
end = strchr(p + 2, ';');
@@ -3215,11 +3249,9 @@ input_osc_133_exit_status(const char *p, int *present)
free(copy);
return (0);
}
*present = 1;
errno = 0;
status = (int)strtol(copy, &endptr, 10);
status = strtonum(copy, 0, 255, &errstr);
free(copy);
if (errno != 0 || endptr == copy || status < 0 || status > 255)
if (errstr != NULL)
return (255);
return (status);
}
@@ -3264,22 +3296,6 @@ input_fire_command_event(struct window_pane *wp, const char *name)
events_fire(name, ep);
}
/* Check if an OSC 133 prompt is secondary or a continuation. */
static int
input_osc_133_secondary_prompt(const char *p)
{
const char *cp;
while ((cp = strstr(p, ";k=")) != NULL) {
cp += 3;
if ((*cp == 's' || *cp == 'c') &&
(cp[1] == '\0' || cp[1] == ';'))
return (1);
p = cp;
}
return (0);
}
/* Handle the OSC 133 sequence. */
static void
input_osc_133(struct input_ctx *ictx, const char *p)
@@ -3289,7 +3305,8 @@ input_osc_133(struct input_ctx *ictx, const char *p)
struct grid *gd = s->grid;
u_int line = s->cy + gd->hsize;
struct grid_line *gl = NULL;
int status, status_present;
const char *cp;
int status;
if (line < gd->hsize + gd->sy)
gl = grid_get_line(gd, line);
@@ -3298,14 +3315,9 @@ input_osc_133(struct input_ctx *ictx, const char *p)
case 'A':
case 'N':
if (gl != NULL) {
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
}
memset(&gl->osc133_data, 0, sizeof gl->osc133_data);
gl->osc133_data.prompt_col = s->cx;
gl->flags |= GRID_LINE_START_PROMPT;
}
if (wp != NULL) {
wp->last_prompt_time = time(NULL);
@@ -3314,25 +3326,23 @@ input_osc_133(struct input_ctx *ictx, const char *p)
break;
case 'P':
if (gl != NULL) {
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
}
cp = strstr(p, ";k=s");
if (cp != NULL && (cp[4] == ';' || cp[4] == '\0'))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
gl->osc133_data.prompt_col = s->cx;
}
break;
case 'B':
case 'I':
if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) {
if (gl != NULL) {
gl->flags |= GRID_LINE_START_COMMAND;
gl->osc133_data.cmd_col = s->cx;
}
break;
case 'C':
if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) {
if (gl != NULL) {
gl->flags |= GRID_LINE_START_OUTPUT;
gl->osc133_data.out_start_col = s->cx;
}
@@ -3345,7 +3355,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
break;
case 'D':
status = input_osc_133_exit_status(p, &status_present);
status = input_osc_133_exit_status(p);
if (wp != NULL) {
wp->cmd_end_time = time(NULL);
wp->flags &= ~PANE_CMDRUNNING;
@@ -3354,8 +3364,6 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
if (gl != NULL) {
gl->flags |= GRID_LINE_END_OUTPUT;
if (status_present)
gl->flags |= GRID_LINE_END_OUTPUT_STATUS;
gl->osc133_data.out_end_col = s->cx;
gl->osc133_data.exit_status = status;
}

View File

@@ -52,9 +52,7 @@
" '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \
" '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \
" ''" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Line Numbers Off,Line Numbers On},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_fold_view,Fold View Off,Fold View On},}' 'O' {send -X fold-view-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},Edit,}' 'e' {if -F '#{selection_present}' {send -X open-selection} {send -X open-output}}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \
" ''" \
" '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \
@@ -577,7 +575,6 @@ key_bindings_init(void)
"bind -Tcopy-mode C-l { send -X recentre-top-bottom }",
"bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }",
"bind -Tcopy-mode C-n { send -X cursor-down }",
"bind -Tcopy-mode C-o { send -X select-output }",
"bind -Tcopy-mode C-p { send -X cursor-up }",
"bind -Tcopy-mode C-r { command-prompt -P -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }",
"bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }",
@@ -586,19 +583,15 @@ key_bindings_init(void)
"bind -Tcopy-mode Escape { send -X cancel }",
"bind -Tcopy-mode C-[ { send -X cancel }",
"bind -Tcopy-mode Space { send -X page-down }",
"bind -Tcopy-mode Tab { send -X toggle-output }",
"bind -Tcopy-mode BTab { send -X toggle-output -a }",
"bind -Tcopy-mode , { send -X jump-reverse }",
"bind -Tcopy-mode \\; { send -X jump-again }",
"bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }",
"bind -Tcopy-mode L { send -X line-numbers-toggle }",
"bind -Tcopy-mode N { send -X search-reverse }",
"bind -Tcopy-mode O { send -X fold-view-toggle }",
"bind -Tcopy-mode P { send -X toggle-position }",
"bind -Tcopy-mode R { send -X rectangle-toggle }",
"bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode X { send -X set-mark }",
"bind -Tcopy-mode e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode f { command-prompt -P -1p'(jump forward)' { send -X jump-forward -- '%%' } }",
"bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }",
"bind -Tcopy-mode n { send -X search-again }",
@@ -607,7 +600,7 @@ key_bindings_init(void)
"bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode Home { send -X start-of-line }",
"bind -Tcopy-mode End { send -X end-of-line }",
"bind -Tcopy-mode MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode MouseDown1Pane select-pane",
"bind -Tcopy-mode MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }",
@@ -667,8 +660,6 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi Escape { send -X clear-selection }",
"bind -Tcopy-mode-vi C-[ { send -X clear-selection }",
"bind -Tcopy-mode-vi Space { send -X begin-selection }",
"bind -Tcopy-mode-vi Tab { send -X toggle-output }",
"bind -Tcopy-mode-vi BTab { send -X toggle-output -a }",
"bind -Tcopy-mode-vi '$' { send -X end-of-line }",
"bind -Tcopy-mode-vi , { send -X jump-reverse }",
"bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }",
@@ -696,10 +687,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi K { send -X scroll-up }",
"bind -Tcopy-mode-vi L { send -X bottom-line }",
"bind -Tcopy-mode-vi M { send -X middle-line }",
"bind -Tcopy-mode-vi M-e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode-vi M-o { send -X select-output }",
"bind -Tcopy-mode-vi N { send -X search-reverse }",
"bind -Tcopy-mode-vi O { send -X fold-view-toggle }",
"bind -Tcopy-mode-vi P { send -X toggle-position }",
"bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode-vi V { send -X select-line }",
@@ -727,7 +715,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi % { send -X next-matching-bracket }",
"bind -Tcopy-mode-vi Home { send -X start-of-line }",
"bind -Tcopy-mode-vi End { send -X end-of-line }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane }",
"bind -Tcopy-mode-vi MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }",

View File

@@ -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. */

View File

@@ -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
@@ -115,6 +115,7 @@ static const char *options_table_theme_list[] = {
static const char *options_table_copy_mode_line_numbers_list[] = {
"off", "default", "absolute", "relative", "hybrid", NULL
};
/* Status line format. */
#define OPTIONS_TABLE_STATUS_FORMAT1 \
"#[align=left range=left #{E:status-left-style}]" \
@@ -1384,13 +1385,6 @@ const struct options_table_entry options_table[] = {
.text = "Style of search matches in copy mode."
},
{ .name = "copy-mode-exit-status-format",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.default_str = "#{?exit_status,#[fg=themered]!, }",
.text = "Format of OSC 133 exit status indicator in copy mode."
},
{ .name = "copy-mode-current-match-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,

View File

@@ -1400,6 +1400,12 @@ options_push_changes(const char *name)
TAILQ_FOREACH(loop, &clients, entry)
server_client_set_key_table(loop, NULL);
}
if (strcmp(name, "extended-keys") == 0) {
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->tty.flags & TTY_STARTED)
tty_update_features(&loop->tty);
}
}
if (strcmp(name, "user-keys") == 0) {
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->tty.flags & TTY_OPENED)

View File

@@ -1,81 +0,0 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007p\\$ \\033]133;B\\007echo hi\\n\\033]133;C\\007hello\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward hello || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != hello ] || exit 1
$TMUX send-keys -X search-backward separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != separator ] || exit 1
$TMUX send-keys BTab || exit 1
$TMUX send-keys -X search-backward hello || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = hello ] || exit 1
$TMUX send-keys -X select-line || exit 1
$TMUX send-keys -X copy-selection || exit 1
selected=$($TMUX show-buffer)
[ "$selected" = hello ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"first\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != separator ] || exit 1
$TMUX send-keys BTab || exit 1
$TMUX send-keys -X search-backward separator || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = separator ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P> \\033]133;B\\007for i in 1 2 3; do\\n\\033]133;A;k=s\\007> \\033]133;B\\007echo \\$i\\n\\033]133;P;k=s\\007> \\033]133;B\\007done\\033]133;C\\007\\nRESULT\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007P> \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward RESULT || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != RESULT ] || exit 1
$TMUX send-keys -X search-backward 'echo $i' || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = 'echo $i' ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X search-backward RESULT || exit 1
$TMUX send-keys Tab || exit 1
cursor=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$cursor" = done ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P0> \\033]133;B\\007\\r\\n\\033]133;D;0\\007empty-separator\\n\\033]133;A\\007P1> \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
collapsed=$($TMUX capture-pane -p)
case "$collapsed" in
*"+ P0>"*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -X search-backward empty-separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != empty-separator ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -1,102 +0,0 @@
#!/bin/sh
# Check OSC 133 exit status indicators in copy mode using a real client.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
fail() {
echo "$*" >&2
exit 1
}
capture() {
$TMUX capture-pane -pS0 -E- >$TMP || exit 1
}
check_grep() {
grep -Fq "$1" $TMP || fail "missing pattern: $1"
}
check_no_grep() {
grep -Fq "$1" $TMP && fail "unexpected pattern: $1"
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15
$TMUX2 new-session -d -x80 -y10 \
"printf '\033]133;A\007p\$ \033]133;B\007one\n\033]133;C\007out1\n\033]133;D;0\007\033]133;A\007p\$ \033]133;B\007two\n\033]133;C\007out2\n\033]133;D;123\007\033]133;A\007p\$ \033]133;B\007silent\n\033]133;C\007\033]133;D;7\007\033]133;A\007p\$ \033]133;B\007three\n\033]133;C\007out3\n\033]133;D\007\033]133;A\007p\$ \033]133;B\007'; exec sleep 100" || \
exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g copy-mode-position-format '#[align=left]POS' || exit 1
$TMUX2 list-keys -Tcopy-mode O | grep -Fq 'fold-view-toggle' || exit 1
$TMUX2 list-keys -Tcopy-mode-vi O | grep -Fq 'fold-view-toggle' || exit 1
$TMUX new-session -d -x80 -y10 || exit 1
$TMUX set -g status off || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
$TMUX2 copy-mode -U || exit 1
$TMUX2 send -X history-top || exit 1
sleep 1
capture
check_grep "!- p\$ two"
check_grep "! p\$ silent"
check_no_grep "!+ p\$ silent"
check_grep "POS"
# Rebuilding with collapsed output must not expand copy-mode formats before
# the viewport has been restored for the shorter backing grid.
$TMUX2 send -X collapse-output -a || exit 1
sleep 1
capture
check_grep "!+ p\$ two"
check_grep "! p\$ silent"
check_no_grep "!+ p\$ silent"
$TMUX2 send -X line-numbers-on || exit 1
sleep 1
capture
check_grep " 3 !+ p\$ two"
$TMUX2 send -X fold-view-toggle || exit 1
[ "$($TMUX2 display -p '#{copy_fold_view}')" = 0 ] || exit 1
sleep 1
capture
check_grep "out2"
check_no_grep "!+ p\$ two"
check_grep " 3 p\$ two"
$TMUX2 send -X fold-view-toggle || exit 1
[ "$($TMUX2 display -p '#{copy_fold_view}')" = 1 ] || exit 1
sleep 1
capture
check_grep " 3 !- p\$ two"
# A format wider than the standard three-column gutter is not truncated.
$TMUX2 send -X cancel || exit 1
$TMUX2 set -g copy-mode-exit-status-format \
'#[align=right]#{?exit_status,!#{exit_status}, }' || exit 1
$TMUX2 copy-mode -c || exit 1
$TMUX2 send -X history-top || exit 1
sleep 1
capture
check_grep "!123+ p\$ two"
$TMUX2 send -X select-line || exit 1
$TMUX2 send -X copy-selection || exit 1
selection=$($TMUX2 show-buffer)
case $selection in
*RC=*) fail "exit status was copied" ;;
esac
exit 0

View File

@@ -5,12 +5,9 @@
start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n'
check_capture alternate 'MAINZ'
start_pane osc133 20 12 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nxz\033]133;A;k=s\007more\nxw\033]133;P;k=c\007more\nxv\033]133;A;k=c\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n'
start_pane osc133 20 8 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n'
check_capture osc133 'xxp>cmd
xymore
xzmore
xwmore
xvmore
zzout
qbad
qqbig
@@ -18,13 +15,10 @@ zzzok'
check_raw_matches osc133 \
'L 0 \(0\) flags=START_PROMPT,START_COMMAND\[[0-9a-f]+\].* osc133=2,4,0,0,0' \
'L 1 \(1\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 2 \(2\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 3 \(3\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 4 \(4\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,7' \
'L 6 \(6\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,1,4,255' \
'L 7 \(7\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,255' \
'L 8 \(8\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0'
'L 2 \(2\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,7' \
'L 3 \(3\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,1,4,255' \
'L 4 \(4\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,255' \
'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0'
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,146 @@
#!/bin/sh
# Test per-client keyboard protocol detection and pane-side encoding.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
KCONF=$(mktemp)
LCONF=$(mktemp)
OUT=$(mktemp)
TMP=$(mktemp)
SOCKETS="testKmc$$ testKka$$ testKua$$ testKrt$$ testKru$$ testKlq$$ testKlu$$"
printf '%s\n' 'set -g extended-keys on' >"$KCONF"
printf '%s\n' 'set -g extended-keys off' >"$LCONF"
cleanup()
{
rm -f "$KCONF" "$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" ]
}
# Each client must be negotiated independently. A tmux with extended-keys off
# stands in for a terminal without Kitty keys, since it does not answer the
# query.
M="$TEST_TMUX -LtestKmc$$ -f$KCONF"
KA="$TEST_TMUX -LtestKka$$ -f$KCONF"
UA="$TEST_TMUX -LtestKua$$ -f$LCONF"
$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
# 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$LCONF"
$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_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
# An application is told the Kitty flags it asked for even when a client that
# only supports VT10x is attached, and keys from that client are encoded as it
# asked.
: >"$OUT"
: >"$TMP"
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'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$TMP'; 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")" = '1b5b3f3175' ] || exit 1
wait_for_client_mode "$LQ" '' 'VT10x' || exit 1
$LU send-keys C-a
wait_for_output "$TMP" || exit 1
[ "$(tr -d ' \n' <"$TMP")" = '1b5b39373b3575' ] || exit 1
exit 0

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

@@ -0,0 +1,116 @@
#!/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' >"$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
# Send raw bytes so the outer tmux does not encode them itself.
raw=$(printf '%s' "$keys" | sed -e 's/Escape /\\033/g' -e 's/ //g')
$TMUX send-keys -H $(printf "$raw" | od -An -v -t x1)
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
# 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

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

@@ -0,0 +1,108 @@
#!/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' >"$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"
# "always" forces modifyOtherKeys mode 1, not Kitty keys.
$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 1b5b3f3075
wait_for_mode 'Ext 1'
$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

@@ -1,131 +0,0 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -L${TEST_SOCKET:-testO$$} -f/dev/null"
OUT=/tmp/tmux-output-commands-$$
EXPECTED=$(printf 'one\ntwo')
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -f "$OUT"
}
trap cleanup EXIT HUP INT TERM
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\ntwo\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007broken\\n\\033]133;C\\007unfinished\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X copy-output || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X pipe-output "wc -c >$OUT" || exit 1
sleep 1
[ "$(cat "$OUT")" = 10 ] || exit 1
$TMUX send-keys -X copy-pipe-output "wc -c >$OUT" output || exit 1
sleep 1
[ "$(cat "$OUT")" = 10 ] || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X select-output || exit 1
$TMUX send-keys -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X select-output || exit 1
$TMUX send-keys -X pipe-selection "wc -c >$OUT" || exit 1
sleep 1
[ "$(cat "$OUT")" = 10 ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X search-backward separator || exit 1
$TMUX send-keys -X copy-output || exit 1
[ "$($TMUX show-buffer)" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward echo || exit 1
$TMUX send-keys -X select-output || exit 1
$TMUX send-keys -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X search-backward one || exit 1
$TMUX send-keys -X copy-output || exit 1
[ "$($TMUX show-buffer)" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX new-window -d -n plain "printf 'alpha\\nbeta\\n'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X copy-output -a || exit 1
all=$($TMUX show-buffer)
case "$all" in
*alpha*beta*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -t :plain.0 -X cancel || exit 1
$TMUX set-buffer -b keep unchanged || exit 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X copy-output || exit 1
[ "$($TMUX show-buffer -b keep)" = unchanged ] || exit 1
$TMUX send-keys -t :plain.0 -X cancel || exit 1
$TMUX new-window -d -n empty "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007true\\033]133;C\\033]133;D;0\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :empty || exit 1
$TMUX send-keys -t :empty.0 -X copy-output || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :empty.0 -X cancel || exit 1
$TMUX new-window -d -n prompt "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :prompt || exit 1
$TMUX send-keys -t :prompt.0 -X copy-output || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :prompt.0 -X cancel || exit 1
$TMUX copy-mode -c -t :prompt || exit 1
$TMUX send-keys -t :prompt.0 -X expand-output || exit 1
$TMUX send-keys -t :prompt.0 -X -N 100 cursor-down || exit 1
$TMUX send-keys -t :prompt.0 -X select-output || exit 1
[ "$($TMUX display-message -p -t :prompt.0 '#{selection_present}')" = 1 ] || exit 1
$TMUX send-keys -t :prompt.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :prompt.0 -X cancel || exit 1
$TMUX copy-mode -t :prompt || exit 1
$TMUX send-keys -t :prompt.0 -X -N 100 cursor-down || exit 1
$TMUX send-keys -t :prompt.0 C-o || exit 1
[ "$($TMUX display-message -p -t :prompt.0 '#{selection_present}')" = 1 ] || exit 1
$TMUX send-keys -t :prompt.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :prompt.0 -X cancel || exit 1
$TMUX new-window -d -n same "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :same || exit 1
$TMUX send-keys -t :same.0 -X search-backward one || exit 1
$TMUX send-keys -t :same.0 -X select-output || exit 1
$TMUX send-keys -t :same.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :same.0 -X cancel || exit 1
$TMUX new-window -d -n clear "printf 'old1\\nold2\\nold3\\nold4\\nold5\\nold6\\n\\033]133;A\\007p\\$ \\033]133;B\\007echo 1; clear; ps\\n\\033]133;C\\0071\\n\\033[H\\033[2JPID TTY\\n1 pts/0\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :clear || exit 1
$TMUX send-keys -t :clear.0 C-o || exit 1
$TMUX send-keys -t :clear.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = "$(printf 'PID TTY\n1 pts/0')" ] || exit 1
$TMUX send-keys -t :clear.0 -X cancel || exit 1
exit 0

View File

@@ -439,6 +439,7 @@ screen_write_reset(struct screen_write_ctx *ctx)
if (options_get_number(global_options, "extended-keys") == 2)
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);

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->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;
@@ -118,9 +120,9 @@ screen_reinit(struct screen *s, int check)
if (options_get_number(global_options, "extended-keys") == 2)
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 +700,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 +761,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,

View File

@@ -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);

View File

@@ -701,7 +701,7 @@ spawn_editor_finish(struct window_pane *wp)
struct spawn_editor_state *
spawn_editor(struct client *c, const char *buf, size_t len,
const char *editor, spawn_finish_edit_cb cb, void *arg)
spawn_finish_edit_cb cb, void *arg)
{
struct spawn_editor_state *es;
struct spawn_context sc = { 0 };
@@ -715,13 +715,13 @@ spawn_editor(struct client *c, const char *buf, size_t len,
FILE *f;
char *cmd, *cause = NULL;
char path[] = _PATH_TMP "tmux.XXXXXXXX";
const char *editor;
int fd;
if (w->modal != NULL)
return (NULL);
if (editor == NULL)
editor = options_get_string(global_options, "editor");
editor = options_get_string(global_options, "editor");
fd = mkstemp(path);
if (fd == -1)
return (NULL);

181
tmux.1
View File

@@ -2006,25 +2006,6 @@ Same as
.Ic copy\-pipe
but also exit copy mode.
.It Xo
.Ic copy\-pipe\-output
.Op Fl aCP
.Op Ar command
.Op Ar prefix
.Xc
Copy the current OSC 133 command output, pipe it to
.Ar command ,
and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-output
.Op Fl aCP
.Op Ar prefix
.Xc
Copy the current OSC 133 command output and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-selection
.Op Fl CP
.Op Ar prefix
@@ -2190,27 +2171,6 @@ Hide line numbers in copy mode.
.Xc
Toggle line numbers in copy mode.
.It Xo
.Ic collapse\-output
.Op Fl a
.Xc
Collapse the output for the command on the cursor line.
With
.Fl a ,
collapse all command output in the copy-mode buffer.
Text before the first OSC 133 prompt is treated as an initial command output
region.
.It Xo
.Ic expand\-output
.Op Fl a
.Xc
Expand the output for the command on the cursor line.
With
.Fl a ,
expand all command output in the copy-mode buffer.
.Pp
The Tab key toggles output for the command on the cursor line and S-Tab
toggles all command output.
.It Xo
.Ic middle\-line
(vi: M)
(emacs: M\-r)
@@ -2264,32 +2224,6 @@ but treat all non-whitespace characters as part of a word.
.Xc
Switch at which end of the selection the cursor sits.
.It Xo
.Ic open\-output
.Op Fl a
.Op Ar command
.Xc
Open the current OSC 133 command output in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
.It Xo
.Ic open\-selection
.Op Ar command
.Xc
Open the current selection in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
In emacs mode,
.Ql e ,
and in vi mode,
.Ql M\-e ,
opens the selection if one is present, otherwise the command output.
.It Xo
.Ic page\-down
(vi: C\-f)
(emacs: PageDown)
@@ -2330,21 +2264,6 @@ Same as
.Ic pipe
but also exit copy mode.
.It Xo
.Ic pipe\-output
.Op Fl a
.Op Ar command
.Xc
Pipe the current OSC 133 command output to
.Ar command
and clear the selection.
.It Xo
.Ic pipe\-selection
.Op Ar command
.Xc
Pipe the selected text to
.Ar command
and clear the selection.
.It Xo
.Ic previous\-matching\-bracket
(emacs: M\-C\-b)
.Xc
@@ -2534,15 +2453,6 @@ Search forward for the specified plain text.
Repeat the last search in the reverse direction (forward becomes backward and
backward becomes forward).
.It Xo
.Ic select\-output
.Op Fl a
.Xc
Select the current OSC 133 command output.
The default emacs binding is
.Ql C\-o
and the default vi binding is
.Ql M\-o .
.It Xo
.Ic select\-line
(vi: V)
.Xc
@@ -2586,20 +2496,6 @@ Toggle the visibility of the position indicator in the top right.
Move to the top line.
.El
.Pp
The output commands use OSC 133 output text from the
.Ql C
marker up to its matching
.Ql D
marker.
If there is no
.Ql D
marker, the output ends at the end of the copy-mode buffer.
At the following prompt or below it, they use the most recent nonempty output.
With
.Fl a ,
they use the entire copy-mode buffer instead.
If no output range can be found, they do nothing.
.Pp
The search commands come in several varieties:
.Ql search\-forward
and
@@ -2707,19 +2603,12 @@ The synopsis for the
command is:
.Bl -tag -width Ds
.It Xo Ic copy\-mode
.Op Fl UcdekHMqSu
.Op Fl dekHMqSu
.Op Fl s Ar src\-pane
.Op Fl t Ar target\-pane
.Xc
Enter copy mode.
.Pp
.Fl c
enters copy mode with OSC 133 command output collapsed.
.Fl U
enters copy mode with OSC 133 output controls shown and all output expanded.
When output is collapsed, the prompt and entered command are shown and text
from the output start marker to the next primary prompt marker is hidden.
.Pp
.Fl u
enters copy mode and scrolls one page up and
.Fl d
@@ -5018,14 +4907,30 @@ 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.
When enabled,
.Nm
uses the Kitty keyboard protocol with terminals which support it, and the
.Ic modifyOtherKeys
.Xr xterm 1
resource.
resource with other terminals which support extended keys.
Each client uses the best that its terminal supports, so clients with different
terminals may be attached at the same time.
.Pp
When set to
Programs inside panes may request either the Kitty keyboard protocol or
.Ic modifyOtherKeys ,
and keys are sent to each program in the form it requested, whichever client
they were typed on.
The Kitty keyboard protocol can report the Super and Hyper modifiers, whose key
name prefixes are
.Ql s-
and
.Ql H- ;
its disambiguate and report-all enhancements are supported.
.Pp
For
.Ic modifyOtherKeys ,
when set to
.Ic on ,
the program inside the pane can request one of two modes: mode 1 which changes
the sequence for only keys which lack an existing well-known representation; or
@@ -5034,26 +4939,35 @@ When set to
.Ic always ,
modes 1 and 2 can still be requested by applications, but mode 1 will be forced
instead of the standard mode.
This applies only to applications inside panes; it does not force the Kitty
keyboard protocol or change what
.Nm
requests from the terminal.
When set to
.Ic off ,
this feature is disabled and only standard keys are reported.
.Pp
.Nm
will always request extended keys itself if the terminal supports them.
Support for the Kitty keyboard protocol is detected when a client attaches.
See also the
.Ic kittykeys
and
.Ic extkeys
feature for the
features for the
.Ic terminal\-features
option, the
.Ic extended\-keys\-format
option and the
.Ic pane_key_mode
variable.
and
.Ic client_key_mode
variables.
.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.
Selects the format for reporting modified keys to programs which request
.Ic modifyOtherKeys .
This is the equivalent of the
.Ic formatOtherKeys
.Xr xterm 1
@@ -5066,6 +4980,11 @@ and as
.Ql \[ha][[65;6u
when set to
.Ic csi\-u .
.Ic kitty
is accepted for compatibility and is the same as
.Ic csi\-u ;
programs which request the Kitty keyboard protocol receive it whatever this
option is set to.
.It Xo Ic focus\-events
.Op Ic on | off
.Xc
@@ -5214,6 +5133,9 @@ Ignore function keys from
and use the
.Nm
internal set only.
.It kittykeys
Supports the Kitty keyboard protocol.
This is added automatically if the terminal answers the Kitty keyboard query.
.It margins
Supports DECSLRM margins.
.It mouse
@@ -5920,20 +5842,6 @@ section.
.It Ic copy\-mode\-position\-format Ar format
Format of the position indicator in copy mode.
.Pp
.It Ic copy\-mode\-exit\-status\-format Ar format
Format of the OSC 133 exit status indicator in the copy-mode gutter.
The default displays a theme red
.Ql !
for a nonzero status and a space otherwise.
The gutter grows to fit the expanded format.
The format is drawn to the left of the collapse control, and its alignment
applies within that area.
The format is expanded with
.Ql exit_status
and
.Ql exit_status_present ,
which is set only when the OSC 133 end-of-output marker includes a status.
.Pp
.It Ic copy\-mode\-position\-style Ar style
Set the style of the position indicator in copy mode.
For how to specify
@@ -7434,6 +7342,7 @@ The following variables are available, where appropriate:
.It Li "client_discarded" Ta "" Ta "Bytes discarded when client behind"
.It Li "client_flags" Ta "" Ta "List of client flags"
.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_last_session" Ta "" Ta "Name of the client's last session"
.It Li "client_name" Ta "" Ta "Name of client"

117
tmux.h
View File

@@ -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
@@ -816,7 +899,6 @@ struct colour_palette {
#define GRID_LINE_START_OUTPUT 0x40
#define GRID_LINE_END_OUTPUT 0x80
#define GRID_LINE_HYPERLINK 0x100
#define GRID_LINE_END_OUTPUT_STATUS 0x200
/* All OSC 133 flags. */
#define GRID_LINE_OSC133_FLAGS \
@@ -824,8 +906,7 @@ struct colour_palette {
GRID_LINE_SECOND_PROMPT| \
GRID_LINE_START_COMMAND| \
GRID_LINE_START_OUTPUT| \
GRID_LINE_END_OUTPUT| \
GRID_LINE_END_OUTPUT_STATUS)
GRID_LINE_END_OUTPUT)
/* Grid string flags. */
#define GRID_STRING_WITH_SEQUENCES 0x1
@@ -1070,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;
@@ -1091,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;
@@ -1741,6 +1829,7 @@ struct tty_term {
#define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
#define TERM_KITTYKEYS 0x100
int flags;
LIST_ENTRY(tty_term) entry;
@@ -1821,9 +1910,12 @@ struct tty {
#define TTY_WAITBG 0x4000
#define TTY_BRACKETPASTE 0x8000
#define TTY_HAVESYNC 0x10000
#define TTY_HAVEKKB 0x20000
#define TTY_KKBPUSHED 0x40000
#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;
@@ -2976,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 *);
@@ -3457,6 +3550,20 @@ int input_key(struct screen *, struct bufferevent *, key_code);
int input_key_get_mouse(struct screen *, struct mouse_event *, u_int,
u_int, const char **, size_t *);
/* 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);
int colour_join_rgb(u_char, u_char, u_char);
@@ -4229,7 +4336,7 @@ struct winlink *spawn_window(struct spawn_context *, char **);
struct window_pane *spawn_pane(struct spawn_context *, char **);
typedef void (*spawn_finish_edit_cb)(char *, size_t, void *);
struct spawn_editor_state *spawn_editor(struct client *, const char *, size_t,
const char *, spawn_finish_edit_cb, void *);
spawn_finish_edit_cb, void *);
void spawn_cancel_editor(struct spawn_editor_state *);
pid_t spawn_get_editor_pid(struct spawn_editor_state *);
void spawn_editor_finish(struct window_pane *);

View File

@@ -235,6 +235,13 @@ static const struct tty_feature tty_feature_sync = {
0
};
/* Terminal supports the Kitty keyboard protocol. */
static const struct tty_feature tty_feature_kittykeys = {
"kittykeys",
NULL,
TERM_KITTYKEYS
};
/* Terminal supports extended keys. */
static const char *const tty_feature_extkeys_capabilities[] = {
"Eneks=\\E[>4;2m",
@@ -386,6 +393,7 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys,
&tty_feature_focus,
&tty_feature_ignorefkeys,
&tty_feature_kittykeys,
&tty_feature_margins,
&tty_feature_mouse,
&tty_feature_osc7,

View File

@@ -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,8 @@ tty_keys_extended_key(struct tty *tty, const char *buf, size_t len,
utf8_char uc;
*size = 0;
if (tty->flags & TTY_KKBPUSHED)
return (-1);
/* First two bytes are always \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);
/* A DA response without a preceding keyboard response means no support. */
tty->flags |= TTY_HAVEKKB;
tty_update_features(tty);
tty->flags |= TTY_HAVEDA;

430
tty-kitty.c Normal file
View File

@@ -0,0 +1,430 @@
/* $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 (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;
log_debug("%s: received Kitty keyboard flags %u", c->name, flags);
tty_parse_client_features(c, "kittykeys", ",");
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 ||
(~tty->term->flags & TERM_KITTYKEYS)) {
if (tty->flags & TTY_KKBPUSHED) {
tty_puts(tty, "\033[<u");
tty->flags &= ~TTY_KKBPUSHED;
}
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)
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);
}

12
tty.c
View File

@@ -409,6 +409,8 @@ tty_send_requests(struct tty *tty)
return;
if (tty->term->flags & TERM_VT100LIKE) {
if (~tty->flags & TTY_HAVEKKB)
tty_puts(tty, "\033[?u");
if (~tty->flags & TTY_HAVEDA)
tty_puts(tty, "\033[c");
if (~tty->flags & TTY_HAVEDA2)
@@ -507,6 +509,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[<u");
tty->flags &= ~TTY_KKBPUSHED;
}
if (tty_use_margin(tty))
tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG));
@@ -559,8 +565,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") == 0 ||
(tty->term->flags & TERM_KITTYKEYS))
tty_puts(tty, tty_term_string(tty->term, TTYC_DSEKS));
else
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)

View File

@@ -606,8 +606,7 @@ window_buffer_start_edit(struct window_buffer_modedata *data,
ed->name = xstrdup(paste_buffer_name(pb));
ed->pb = pb;
ed->editor = spawn_editor(c, buf, len, NULL, window_buffer_edit_close_cb,
ed);
ed->editor = spawn_editor(c, buf, len, window_buffer_edit_close_cb, ed);
if (ed->editor == NULL)
window_buffer_finish_edit(ed);
else {

File diff suppressed because it is too large Load Diff

View File

@@ -2104,8 +2104,8 @@ window_customize_start_edit(struct window_customize_modedata *data,
buf = "\n";
len = 1;
}
ed->editor = spawn_editor(c, buf, len, NULL,
window_customize_edit_close_cb, ed);
ed->editor = spawn_editor(c, buf, len, window_customize_edit_close_cb,
ed);
free(value);
if (ed->editor == NULL)
window_customize_finish_edit(ed);