diff --git a/Makefile.am b/Makefile.am index 7dddbe49..d824f30b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -236,6 +236,11 @@ if ENABLE_SIXEL dist_tmux_SOURCES += image.c image-sixel.c endif +# Enable kitty graphics protocol support. +if ENABLE_KITTY +dist_tmux_SOURCES += image-kitty.c +endif + if NEED_FUZZING check_PROGRAMS = fuzz/input-fuzzer fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS) diff --git a/configure.ac b/configure.ac index fa474825..8371a987 100644 --- a/configure.ac +++ b/configure.ac @@ -471,6 +471,16 @@ if test "x$enable_sixel" = xyes; then fi AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes]) +# Enable kitty graphics protocol support. +AC_ARG_ENABLE( + kitty, + AS_HELP_STRING(--enable-kitty, enable kitty terminal graphics protocol) +) +if test "x$enable_kitty" = xyes; then + AC_DEFINE(ENABLE_KITTY) +fi +AM_CONDITIONAL(ENABLE_KITTY, [test "x$enable_kitty" = xyes]) + # Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well. AC_MSG_CHECKING(for b64_ntop) AC_LINK_IFELSE([AC_LANG_PROGRAM( diff --git a/format.c b/format.c index 62aba0c4..5bbabd08 100644 --- a/format.c +++ b/format.c @@ -2571,6 +2571,17 @@ format_cb_sixel_support(__unused struct format_tree *ft) #endif } +/* Callback for kitty_support. */ +static void * +format_cb_kitty_support(__unused struct format_tree *ft) +{ +#ifdef ENABLE_KITTY + return (xstrdup("1")); +#else + return (xstrdup("0")); +#endif +} + /* Callback for active_window_index. */ static void * format_cb_active_window_index(struct format_tree *ft) @@ -3469,6 +3480,9 @@ static const struct format_table_entry format_table[] = { { "sixel_support", FORMAT_TABLE_STRING, format_cb_sixel_support }, + { "kitty_support", FORMAT_TABLE_STRING, + format_cb_kitty_support + }, { "socket_path", FORMAT_TABLE_STRING, format_cb_socket_path }, diff --git a/image-kitty.c b/image-kitty.c new file mode 100644 index 00000000..ae3be2d8 --- /dev/null +++ b/image-kitty.c @@ -0,0 +1,203 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Thomas Adam + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* + * Parse control-data key=value pairs from a kitty APC sequence. + * Format: key=value,key=value,... + */ +static int +kitty_parse_control(const char *ctrl, size_t ctrllen, struct kitty_image *ki) +{ + const char *p = ctrl, *end = ctrl + ctrllen, *errstr; + char key[4], val[32]; + size_t klen, vlen; + + while (p < end) { + klen = 0; + while (p < end && *p != '=' && klen < sizeof(key) - 1) + key[klen++] = *p++; + key[klen] = '\0'; + if (p >= end || *p != '=') + return (-1); + p++; + + vlen = 0; + while (p < end && *p != ',' && vlen < sizeof(val) - 1) + val[vlen++] = *p++; + val[vlen] = '\0'; + if (p < end && *p == ',') + p++; + + if (klen != 1) + continue; + + switch (key[0]) { + case 'a': + ki->action = val[0]; + break; + case 'f': + ki->format = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 't': + ki->medium = val[0]; + break; + case 's': + ki->pixel_w = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'v': + ki->pixel_h = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'c': + ki->cols = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'r': + ki->rows = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'i': + ki->image_id = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'I': + ki->image_num = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'p': + ki->placement_id = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'm': + ki->more = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'q': + ki->quiet = strtonum(val, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'z': + ki->z_index = strtonum(val, INT_MIN, INT_MAX, &errstr); + if (errstr != NULL) + return (-1); + break; + case 'o': + ki->compression = val[0]; + break; + case 'd': + ki->delete_what = val[0]; + break; + } + } + return (0); +} + +/* + * Parse a kitty APC body (after the leading 'G'). + * Stores the original control string and base64 payload verbatim for + * pass-through re-emission to the outer terminal. + */ +struct kitty_image * +kitty_parse(const u_char *buf, size_t len, u_int xpixel, u_int ypixel) +{ + struct kitty_image *ki; + const u_char *semi; + const char *ctrl; + size_t ctrllen, paylen; + + if (len == 0) + return (NULL); + + semi = memchr(buf, ';', len); + if (semi != NULL) { + ctrl = (const char *)buf; + ctrllen = semi - buf; + paylen = len - ctrllen - 1; + } else { + ctrl = (const char *)buf; + ctrllen = len; + paylen = 0; + } + + ki = xcalloc(1, sizeof *ki); + ki->xpixel = xpixel; + ki->ypixel = ypixel; + ki->action = 'T'; + ki->format = 32; + ki->medium = 'd'; + + if (kitty_parse_control(ctrl, ctrllen, ki) != 0) { + free(ki); + return (NULL); + } + + if (paylen > 0) { + ki->encoded = xmalloc(paylen + 1); + memcpy(ki->encoded, semi + 1, paylen); + ki->encoded[paylen] = '\0'; + ki->encodedlen = paylen; + } + + ki->ctrl = xmalloc(ctrllen + 1); + memcpy(ki->ctrl, ctrl, ctrllen); + ki->ctrl[ctrllen] = '\0'; + ki->ctrllen = ctrllen; + + return (ki); +} + +void +kitty_free(struct kitty_image *ki) +{ + if (ki == NULL) + return; + free(ki->encoded); + free(ki->ctrl); + free(ki); +} + +char * +kitty_delete_all(size_t *outlen) +{ + char *out; + + *outlen = 3 + 7 + 2; + out = xmalloc(*outlen + 1); + memcpy(out, "\033_Ga=d,d=a\033\\", *outlen); + out[*outlen] = '\0'; + return (out); +} diff --git a/input.c b/input.c index 42bafa89..d3dd6f04 100644 --- a/input.c +++ b/input.c @@ -145,6 +145,7 @@ struct input_ctx { */ struct evbuffer *since_ground; struct event ground_timer; + }; /* Helper functions. */ @@ -1381,6 +1382,10 @@ input_esc_dispatch(struct input_ctx *ictx) input_reset_cell(ictx); screen_write_reset(sctx); screen_write_fullredraw(sctx); +#ifdef ENABLE_KITTY + if (ictx->wp != NULL) + tty_kitty_delete_all_pane(ictx->wp); +#endif break; case INPUT_ESC_IND: screen_write_linefeed(sctx, 0, ictx->cell.cell.bg); @@ -2722,11 +2727,65 @@ input_exit_apc(struct input_ctx *ictx) { struct screen_write_ctx *sctx = &ictx->ctx; struct window_pane *wp = ictx->wp; +#ifdef ENABLE_KITTY + struct kitty_image *ki; +#endif if (ictx->flags & INPUT_DISCARD) return; log_debug("%s: \"%s\"", __func__, ictx->input_buf); +#ifdef ENABLE_KITTY + if (ictx->input_len >= 1 && ictx->input_buf[0] == 'G') { + if (wp == NULL) + return; + + /* + * Intercept only a=q (query) to reply OK ourselves. + * Everything else — including a=T (transmit+display), + * a=d (delete), a=p (place), multi-chunk sequences — + * passes through verbatim to the outer terminal. + * The outer terminal manages all image placement and + * scrolling; tmux must not interfere. + */ + ki = kitty_parse(ictx->input_buf + 1, + ictx->input_len - 1, 0, 0); + if (ki != NULL && ki->action == 'q') { + if (ki->image_id != 0) + input_reply(ictx, 0, + "\033_Gi=%u;OK\033\\", + ki->image_id); + else + input_reply(ictx, 0, + "\033_Ga=q;OK\033\\"); + kitty_free(ki); + return; + } + kitty_free(ki); + + /* Reconstruct APC and pass through verbatim. */ + { + char *apc; + size_t apclen; + + /* input_buf already starts with 'G'. */ + apclen = 2 + ictx->input_len + 2; + apc = xmalloc(apclen + 1); + apc[0] = '\033'; + apc[1] = '_'; + memcpy(apc + 2, ictx->input_buf, + ictx->input_len); + apc[2 + ictx->input_len] = '\033'; + apc[2 + ictx->input_len + 1] = '\\'; + apc[apclen] = '\0'; + tty_kitty_passthrough(wp, apc, apclen, + sctx->s->cx, sctx->s->cy); + free(apc); + } + return; + } +#endif /* ENABLE_KITTY */ + if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf)) { diff --git a/screen-redraw.c b/screen-redraw.c index 8449f02f..073b73a3 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -667,6 +667,16 @@ screen_redraw_screen(struct client *c) } if (flags & CLIENT_REDRAWWINDOW) { log_debug("%s: redrawing panes", c->name); +#ifdef ENABLE_KITTY + /* + * Delete all kitty image placements before redrawing panes. + * This must happen unconditionally — even when the new window + * has no images — so that images from the previous window + * (or from a `reset` in the shell) are cleared from the outer + * terminal before new content is drawn over them. + */ + tty_kitty_delete_all(&c->tty); +#endif screen_redraw_draw_panes(&ctx); screen_redraw_draw_pane_scrollbars(&ctx); } @@ -697,8 +707,12 @@ screen_redraw_pane(struct client *c, struct window_pane *wp, tty_sync_start(&c->tty); tty_update_mode(&c->tty, c->tty.mode, NULL); - if (!redraw_scrollbar_only) + if (!redraw_scrollbar_only) { +#ifdef ENABLE_KITTY + tty_kitty_delete_all(&c->tty); +#endif screen_redraw_draw_pane(&ctx, wp); + } if (window_pane_show_scrollbar(wp, ctx.pane_scrollbars)) screen_redraw_draw_pane_scrollbar(&ctx, wp); diff --git a/tmux.h b/tmux.h index 2896cc40..07dce198 100644 --- a/tmux.h +++ b/tmux.h @@ -72,6 +72,10 @@ struct session; struct sixel_image; #endif +#ifdef ENABLE_KITTY +struct kitty_image; +#endif + struct tty_ctx; struct tty_code; struct tty_key; @@ -614,6 +618,7 @@ enum tty_code_code { TTYC_SMULX, TTYC_SMXX, TTYC_SXL, + TTYC_KTY, TTYC_SS, TTYC_SWD, TTYC_SYNC, @@ -943,6 +948,44 @@ struct image { TAILQ_HEAD(images, image); #endif +#ifdef ENABLE_KITTY +/* + * kitty_image stores the raw decoded pixel data and metadata from a kitty + * graphics protocol APC sequence. It is used to re-emit the sequence to the + * outer terminal on redraw. + */ +struct kitty_image { + /* Control-data fields parsed from the APC sequence. */ + char action; /* a=: 'T'=transmit+display, 't', 'p', 'd', ... */ + u_int format; /* f=: 32=RGBA, 24=RGB, 100=PNG */ + char medium; /* t=: 'd'=direct, 'f'=file, 't'=tmp, 's'=shm */ + u_int pixel_w; /* s=: source image pixel width */ + u_int pixel_h; /* v=: source image pixel height */ + u_int cols; /* c=: display columns (0=auto) */ + u_int rows; /* r=: display rows (0=auto) */ + u_int image_id; /* i=: image id (0=unassigned) */ + u_int image_num; /* I=: image number */ + u_int placement_id; /* p=: placement id */ + u_int more; /* m=: 1=more chunks coming, 0=last */ + u_int quiet; /* q=: suppress responses */ + int z_index; /* z=: z-index */ + char compression; /* o=: 'z'=zlib, 0=none */ + char delete_what; /* d=: delete target (used with a=d) */ + + /* Cell size at the time of parsing (from the owning window). */ + u_int xpixel; + u_int ypixel; + + /* Original base64-encoded payload (concatenated across all chunks). */ + char *encoded; + size_t encodedlen; + + char *ctrl; + size_t ctrllen; +}; + +#endif + /* Cursor style. */ enum screen_cursor_style { SCREEN_CURSOR_DEFAULT, @@ -1566,6 +1609,7 @@ struct tty_term { #define TERM_RGBCOLOURS 0x10 #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 +#define TERM_KITTY 0x80 int flags; LIST_ENTRY(tty_term) entry; @@ -2658,6 +2702,13 @@ void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *); #endif +#ifdef ENABLE_KITTY +void tty_kitty_delete_all(struct tty *); +void tty_kitty_delete_all_pane(struct window_pane *); +void tty_kitty_passthrough(struct window_pane *, const char *, size_t, + u_int, u_int); +#endif + void tty_cmd_syncstart(struct tty *, const struct tty_ctx *); void tty_default_colours(struct grid_cell *, struct window_pane *); @@ -3264,6 +3315,7 @@ void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int, void screen_write_sixelimage(struct screen_write_ctx *, struct sixel_image *, u_int); #endif + void screen_write_alternateon(struct screen_write_ctx *, struct grid_cell *, int); void screen_write_alternateoff(struct screen_write_ctx *, @@ -3748,6 +3800,13 @@ char *sixel_print(struct sixel_image *, struct sixel_image *, struct screen *sixel_to_screen(struct sixel_image *); #endif +#ifdef ENABLE_KITTY +/* image-kitty.c */ +struct kitty_image *kitty_parse(const u_char *, size_t, u_int, u_int); +void kitty_free(struct kitty_image *); +char *kitty_delete_all(size_t *); +#endif + /* server-acl.c */ void server_acl_init(void); struct server_acl_user *server_acl_user_find(uid_t); diff --git a/tty-features.c b/tty-features.c index 31d9b7a8..586139e2 100644 --- a/tty-features.c +++ b/tty-features.c @@ -357,6 +357,17 @@ static const struct tty_feature tty_feature_sixel = { TERM_SIXEL }; +/* Terminal has kitty graphics protocol capability. */ +static const char *const tty_feature_kitty_capabilities[] = { + "Kty", + NULL +}; +static const struct tty_feature tty_feature_kitty = { + "kitty", + tty_feature_kitty_capabilities, + TERM_KITTY +}; + /* Available terminal features. */ static const struct tty_feature *const tty_features[] = { &tty_feature_256, @@ -368,6 +379,7 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_extkeys, &tty_feature_focus, &tty_feature_ignorefkeys, + &tty_feature_kitty, &tty_feature_margins, &tty_feature_mouse, &tty_feature_osc7, @@ -500,7 +512,19 @@ tty_default_features(int *feat, const char *name, u_int version) */ .features = TTY_FEATURES_BASE_MODERN_XTERM ",ccolour,cstyle,extkeys,focus" - } + }, +#ifdef ENABLE_KITTY + { .name = "kitty", + .features = TTY_FEATURES_BASE_MODERN_XTERM + ",ccolour,cstyle,extkeys,focus,sync,hyperlinks" + ",kitty" + }, + { .name = "ghostty", + .features = TTY_FEATURES_BASE_MODERN_XTERM + ",ccolour,cstyle,extkeys,focus,sync,hyperlinks" + ",kitty" + }, +#endif }; u_int i; diff --git a/tty-keys.c b/tty-keys.c index 8fc51174..e265005c 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -60,6 +60,10 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); static int tty_keys_palette(struct tty *, const char *, size_t, size_t *); +#ifdef ENABLE_KITTY +static int tty_keys_kitty_graphics(struct tty *, const char *, size_t, + size_t *); +#endif /* A key tree entry. */ struct tty_key { @@ -759,6 +763,19 @@ tty_keys_next(struct tty *tty) goto partial_key; } +#ifdef ENABLE_KITTY + /* Is this a kitty graphics protocol response? ESC _ G ... ESC \ */ + switch (tty_keys_kitty_graphics(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; + } +#endif + /* Is this a primary device attributes response? */ switch (tty_keys_device_attributes(tty, buf, len, &size)) { case 0: /* yes */ @@ -1640,6 +1657,14 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, tty_default_features(features, "mintty", 0); else if (strncmp(tmp, "foot(", 5) == 0) tty_default_features(features, "foot", 0); +#ifdef ENABLE_KITTY + else if (strncmp(tmp, "kitty ", 6) == 0 || + strcmp(tmp, "kitty") == 0) + tty_default_features(features, "kitty", 0); + else if (strncmp(tmp, "ghostty ", 8) == 0 || + strcmp(tmp, "ghostty") == 0) + tty_default_features(features, "ghostty", 0); +#endif log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); @@ -1795,3 +1820,75 @@ tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) return (0); } + +#ifdef ENABLE_KITTY +/* + * Handle kitty graphics protocol response from outer terminal. + * Format: ESC _ G ; ESC \ + * The response to our capability probe (a=q) is: + * ESC _ G i=31;OK ESC \ (supported) + * ESC _ G i=31;ENOSYS:... (not supported) + * Returns 0 for success, -1 for not a kitty response, 1 for partial. + */ +static int +tty_keys_kitty_graphics(struct tty *tty, const char *buf, size_t len, + size_t *size) +{ + struct client *c = tty->client; + int *features = &c->term_features; + size_t i; + char tmp[256]; + + *size = 0; + + /* + * Kitty APC response starts with ESC _ G (3 bytes). + * The 8-bit C1 equivalent 0x9f could also be used but is rare. + */ + if (buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != '_') + return (-1); + if (len == 2) + return (1); + if (buf[2] != 'G') + return (-1); + if (len == 3) + return (1); + + /* Collect body up to ESC \ (ST). */ + for (i = 0; i < sizeof(tmp) - 1; i++) { + if (3 + i + 1 >= len) + return (1); /* partial */ + if (buf[3 + i] == '\033' && buf[3 + i + 1] == '\\') { + tmp[i] = '\0'; + *size = 3 + i + 2; + break; + } + tmp[i] = buf[3 + i]; + } + if (i == sizeof(tmp) - 1) + return (-1); /* too long, not a valid response */ + + log_debug("%s: kitty graphics response: %s", c->name, tmp); + + /* + * Check if the message (after the semicolon) starts with "OK". + * The format is: i=31;OK or i=31;ENOSYS:... + */ + { + const char *semi = strchr(tmp, ';'); + if (semi != NULL && strncmp(semi + 1, "OK", 2) == 0) { + log_debug("%s: kitty graphics supported", c->name); + tty_add_features(features, "kitty", ","); + tty_update_features(tty); + } else { + log_debug("%s: kitty graphics not supported", c->name); + } + } + + return (0); +} +#endif diff --git a/tty-term.c b/tty-term.c index 29dbaf59..0d298dd8 100644 --- a/tty-term.c +++ b/tty-term.c @@ -270,6 +270,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_SETULC1] = { TTYCODE_STRING, "Setulc1" }, [TTYC_SE] = { TTYCODE_STRING, "Se" }, [TTYC_SXL] = { TTYCODE_FLAG, "Sxl" }, + [TTYC_KTY] = { TTYCODE_FLAG, "Kty" }, [TTYC_SGR0] = { TTYCODE_STRING, "sgr0" }, [TTYC_SITM] = { TTYCODE_STRING, "sitm" }, [TTYC_SMACS] = { TTYCODE_STRING, "smacs" }, @@ -461,6 +462,9 @@ tty_term_apply_overrides(struct tty_term *term) /* Log the SIXEL flag. */ log_debug("SIXEL flag is %d", !!(term->flags & TERM_SIXEL)); + /* Log the KITTY flag. */ + log_debug("KITTY flag is %d", !!(term->flags & TERM_KITTY)); + /* Update the RGB flag if the terminal has RGB colours. */ if (tty_term_has(term, TTYC_SETRGBF) && tty_term_has(term, TTYC_SETRGBB)) diff --git a/tty.c b/tty.c index 6965b748..6e013a2c 100644 --- a/tty.c +++ b/tty.c @@ -391,6 +391,23 @@ tty_send_requests(struct tty *tty) return; if (tty->term->flags & TERM_VT100LIKE) { +#ifdef ENABLE_KITTY + /* + * Send the kitty graphics capability probe BEFORE the DA1 + * request. Per the kitty spec, a supporting terminal sends + * its APC response before processing any subsequent requests. + * So the reply ordering will be: + * 1. ESC _ G i=31;OK ESC \ (kitty response, if supported) + * 2. ESC [ ? ... c (DA1 response) + * 3. ESC [ > ... c (DA2 response) + * 4. ESC P > | ... ESC \ (XDA response) + * which is exactly what our parsers expect. + * Only probe if the kitty feature isn't already enabled. + */ + if (~tty->term->flags & TERM_KITTY) + tty_puts(tty, + "\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\"); +#endif if (~tty->flags & TTY_HAVEDA) tty_puts(tty, "\033[c"); if (~tty->flags & TTY_HAVEDA2) @@ -1468,6 +1485,7 @@ tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c) void tty_draw_images(struct client *c, struct window_pane *wp, struct screen *s) { +#ifdef ENABLE_SIXEL struct image *im; struct tty_ctx ttyctx; @@ -1491,6 +1509,9 @@ tty_draw_images(struct client *c, struct window_pane *wp, struct screen *s) ttyctx.allow_invisible_panes = 1; tty_write_one(tty_cmd_sixelimage, c, &ttyctx); } +#else + (void)c; (void)wp; (void)s; +#endif } #endif @@ -2144,6 +2165,102 @@ tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) } #endif +#ifdef ENABLE_KITTY +static int +tty_has_kitty(struct tty *tty) +{ + return ((tty->term->flags & TERM_KITTY) || + tty_term_has(tty->term, TTYC_KTY)); +} + +/* + * Pass a kitty APC sequence directly to all attached kitty-capable clients + * showing the given pane. The outer terminal's cursor is first moved to + * the pane-relative position (cx, cy) so that images placed at "current + * cursor" land in the right spot. Pass cx=cy=UINT_MAX to skip cursor + * positioning (e.g. for delete commands that don't depend on position). + */ +void +tty_kitty_passthrough(struct window_pane *wp, const char *data, size_t len, + u_int cx, u_int cy) +{ + struct client *c; + struct tty *tty; + u_int x, y; + + TAILQ_FOREACH(c, &clients, entry) { + if (c->session == NULL || c->tty.term == NULL) + continue; + if (c->flags & CLIENT_SUSPENDED) + continue; + if (c->tty.flags & TTY_FREEZE) + continue; + tty = &c->tty; + if (!tty_has_kitty(tty)) + continue; + if (c->session->curw->window != wp->window) + continue; + if (!window_pane_visible(wp)) + continue; + + /* Position cursor at the correct screen location. */ + if (cx != UINT_MAX && cy != UINT_MAX) { + x = wp->xoff + cx; + y = wp->yoff + cy; + if (status_at_line(c) == 0) + y += status_line_size(c); + tty_region_off(tty); + tty_margin_off(tty); + tty_cursor(tty, x, y); + } + + tty->flags |= TTY_NOBLOCK; + tty_add(tty, data, len); + tty_invalidate(tty); + } +} + +/* + * Delete all kitty image placements from the outer terminal unconditionally. + * Called directly (not via tty_write) so it fires on every full window + * redraw regardless of whether the current window has any stored images. + */ +void +tty_kitty_delete_all(struct tty *tty) +{ + char *data; + size_t size; + + if (!tty_has_kitty(tty)) + return; + + if ((data = kitty_delete_all(&size)) == NULL) + return; + + tty->flags |= TTY_NOBLOCK; + tty_add(tty, data, size); + tty_invalidate(tty); + free(data); +} + +/* + * Delete all kitty image placements via passthrough for a specific pane. + * Used on terminal reset (RIS) so images are cleared from the outer terminal. + */ +void +tty_kitty_delete_all_pane(struct window_pane *wp) +{ + char *data; + size_t size; + + if ((data = kitty_delete_all(&size)) == NULL) + return; + + tty_kitty_passthrough(wp, data, size, UINT_MAX, UINT_MAX); + free(data); +} +#endif + void tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx) {