diff --git a/Makefile.am b/Makefile.am index 757bcca9..e154ed0c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -234,6 +234,11 @@ endif # Enable sixel support. if ENABLE_SIXEL_IMAGES dist_tmux_SOURCES += image.c image-sixel.c +else +# If not sixel, still need image.c for kitty. +if ENABLE_KITTY_IMAGES +dist_tmux_SOURCES += image.c +endif endif # Enable kitty graphics protocol support. diff --git a/image-kitty.c b/image-kitty.c index ae3be2d8..fa089cb3 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -190,6 +190,49 @@ kitty_free(struct kitty_image *ki) free(ki); } +/* + * Serialize a kitty_image back into an APC escape sequence for transmission + * to the terminal. This recreates the original command that was parsed. + */ +char * +kitty_print(struct kitty_image *ki, size_t *outlen) +{ + char *out; + size_t total, pos; + + if (ki == NULL || ki->ctrl == NULL) + return (NULL); + + /* Calculate total length: ESC _ G + ctrl + ; + encoded + ESC \ */ + total = 3 + ki->ctrllen; /* \033_G + ctrl */ + if (ki->encoded != NULL && ki->encodedlen > 0) { + total += 1 + ki->encodedlen; /* ; + encoded */ + } + total += 2; /* \033\\ */ + + out = xmalloc(total + 1); + *outlen = total; + + /* Build the sequence */ + pos = 0; + memcpy(out + pos, "\033_G", 3); + pos += 3; + memcpy(out + pos, ki->ctrl, ki->ctrllen); + pos += ki->ctrllen; + + if (ki->encoded != NULL && ki->encodedlen > 0) { + out[pos++] = ';'; + memcpy(out + pos, ki->encoded, ki->encodedlen); + pos += ki->encodedlen; + } + + memcpy(out + pos, "\033\\", 2); + pos += 2; + out[pos] = '\0'; + + return (out); +} + char * kitty_delete_all(size_t *outlen) { diff --git a/image.c b/image.c index 0948925b..a5014b5a 100644 --- a/image.c +++ b/image.c @@ -61,7 +61,22 @@ image_free(struct image *im) all_images_count--; TAILQ_REMOVE(&s->images, im, entry); - sixel_free(im->data); + + switch (im->type) { +#ifdef ENABLE_SIXEL_IMAGES + case IMAGE_SIXEL: + sixel_free(im->data.sixel); + break; +#endif +#ifdef ENABLE_KITTY_IMAGES + case IMAGE_KITTY: + kitty_free(im->data.kitty); + break; +#endif + default: + break; + } + free(im->fallback); free(im); } @@ -81,13 +96,30 @@ image_free_all(struct screen *s) /* Create text placeholder for an image. */ static void -image_fallback(char **ret, u_int sx, u_int sy) +image_fallback(char **ret, enum image_type type, u_int sx, u_int sy) { char *buf, *label; u_int py, size, lsize; + const char *type_name; + + switch (type) { +#ifdef ENABLE_SIXEL_IMAGES + case IMAGE_SIXEL: + type_name = "SIXEL"; + break; +#endif +#ifdef ENABLE_KITTY_IMAGES + case IMAGE_KITTY: + type_name = "KITTY"; + break; +#endif + default: + type_name = "UNKNOWN"; + break; + } /* Allocate first line. */ - lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; + lsize = xasprintf(&label, "%s IMAGE (%ux%u)\r\n", type_name, sx, sy) + 1; if (sx < lsize - 3) size = lsize - 1; else @@ -122,19 +154,62 @@ image_fallback(char **ret, u_int sx, u_int sy) } struct image* -image_store(struct screen *s, struct sixel_image *si) +image_store(struct screen *s, enum image_type type, void *data) { struct image *im; im = xcalloc(1, sizeof *im); + + im->type = type; im->s = s; - im->data = si; im->px = s->cx; im->py = s->cy; - sixel_size_in_cells(si, &im->sx, &im->sy); - image_fallback(&im->fallback, im->sx, im->sy); + switch (type) { +#ifdef ENABLE_SIXEL_IMAGES + case IMAGE_SIXEL: + im->data.sixel = data; + sixel_size_in_cells(im->data.sixel, &im->sx, &im->sy); + break; +#endif +#ifdef ENABLE_KITTY_IMAGES + case IMAGE_KITTY: + im->data.kitty = data; + + /* Kitty images have size info in the structure */ + im->sx = im->data.kitty->cols; + im->sy = im->data.kitty->rows; + + /* + * If cols/rows are 0, they mean "auto" - calculate from + * pixel dimensions. The terminal will figure out the actual + * size, but we need a reasonable estimate for our cache. + */ + if (im->sx == 0 && im->data.kitty->pixel_w > 0 && + im->data.kitty->xpixel > 0) { + im->sx = (im->data.kitty->pixel_w + + im->data.kitty->xpixel - 1) / + im->data.kitty->xpixel; + } + if (im->sy == 0 && im->data.kitty->pixel_h > 0 && + im->data.kitty->ypixel > 0) { + im->sy = (im->data.kitty->pixel_h + + im->data.kitty->ypixel - 1) / im->data.kitty->ypixel; + } + + /* If still 0, use a reasonable default */ + if (im->sx == 0) + im->sx = 10; + if (im->sy == 0) + im->sy = 10; + break; +#endif + default: + break; + } + + image_fallback(&im->fallback, type, im->sx, im->sy); image_log(im, __func__, NULL); TAILQ_INSERT_TAIL(&s->images, im, entry); @@ -188,8 +263,10 @@ image_scroll_up(struct screen *s, u_int lines) { struct image *im, *im1; int redraw = 0; - u_int sx, sy; +#ifdef ENABLE_SIXEL_IMAGES struct sixel_image *new; + u_int sx, sy; +#endif TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) { if (im->py >= lines) { @@ -204,20 +281,46 @@ image_scroll_up(struct screen *s, u_int lines) redraw = 1; continue; } - sx = im->sx; - sy = (im->py + im->sy) - lines; - image_log(im, __func__, "3, lines=%u, sy=%u", lines, sy); - new = sixel_scale(im->data, 0, 0, 0, im->sy - sy, sx, sy, 1); - sixel_free(im->data); - im->data = new; + /* Image is partially scrolled off - need to crop it */ + switch (im->type) { +#ifdef ENABLE_SIXEL_IMAGES + case IMAGE_SIXEL: + sx = im->sx; + sy = (im->py + im->sy) - lines; + image_log(im, __func__, "3 sixel, lines=%u, sy=%u", + lines, sy); - im->py = 0; - sixel_size_in_cells(im->data, &im->sx, &im->sy); + new = sixel_scale(im->data.sixel, 0, 0, 0, im->sy - sy, + sx, sy, 1); + sixel_free(im->data.sixel); + im->data.sixel = new; - free(im->fallback); - image_fallback(&im->fallback, im->sx, im->sy); - redraw = 1; + im->py = 0; + sixel_size_in_cells(im->data.sixel, &im->sx, &im->sy); + + free(im->fallback); + image_fallback(&im->fallback, im->type, im->sx, im->sy); + redraw = 1; + break; +#endif +#ifdef ENABLE_KITTY_IMAGES + case IMAGE_KITTY: + /* + * For kitty images, we can't rescale - the terminal + * owns the placement. Just adjust position and let + * the terminal handle clipping. + */ + image_log(im, __func__, + "3 kitty, lines=%u (no rescale)", lines); + im->py = 0; + /* Height remains the same - terminal will clip */ + redraw = 1; + break; +#endif + default: + break; + } } return (redraw); } diff --git a/input.c b/input.c index e3323aa8..1bb26121 100644 --- a/input.c +++ b/input.c @@ -1561,12 +1561,31 @@ input_csi_dispatch(struct input_ctx *ictx) case -1: break; case 0: + { + /* + * Report sixel support only if outer terminal supports it. + * This ensures applications choose the right protocol. + */ + int has_sixel = 0; #ifdef ENABLE_SIXEL_IMAGES - input_reply(ictx, 1, "\033[?1;2;4c"); -#else - input_reply(ictx, 1, "\033[?1;2c"); + if (ictx->wp != NULL) { + struct client *c; + TAILQ_FOREACH(c, &clients, entry) { + if (c->session != NULL && + c->session->curw->window == ictx->wp->window && + (c->tty.term->flags & TERM_SIXEL)) { + has_sixel = 1; + break; + } + } + } #endif + if (has_sixel) + input_reply(ictx, 1, "\033[?1;2;4c"); + else + input_reply(ictx, 1, "\033[?1;2c"); break; + } default: log_debug("%s: unknown '%c'", __func__, ictx->ch); break; @@ -2729,6 +2748,7 @@ input_exit_apc(struct input_ctx *ictx) struct window_pane *wp = ictx->wp; #ifdef ENABLE_KITTY_IMAGES struct kitty_image *ki; + struct window *w; #endif if (ictx->flags & INPUT_DISCARD) @@ -2740,6 +2760,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp == NULL) return; + w = wp->window; /* * Intercept only a=q (query) to reply OK ourselves. * Everything else — including a=T (transmit+display), @@ -2749,8 +2770,12 @@ input_exit_apc(struct input_ctx *ictx) * scrolling; tmux must not interfere. */ ki = kitty_parse(ictx->input_buf + 1, - ictx->input_len - 1, 0, 0); - if (ki != NULL && ki->action == 'q') { + ictx->input_len - 1, w->xpixel, w->ypixel); + if (ki == NULL) + return; + + /* Handle query commands */ + if (ki->action == 'q') { if (ki->image_id != 0) input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", @@ -2761,26 +2786,31 @@ input_exit_apc(struct input_ctx *ictx) kitty_free(ki); return; } - kitty_free(ki); - /* Reconstruct APC and pass through verbatim. */ - { + /* + * Store the image and trigger a redraw. + * Similar to sixel, we cache the image and let the + * redraw mechanism handle sending it to terminals. + */ + if (ki->action == 'T' || ki->action == 't' || ki->action == 'p') { + screen_write_kittyimage(sctx, ki); + } else { + /* For other actions (delete, etc.), pass through */ 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); + 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); + kitty_free(ki); } return; } diff --git a/screen-write.c b/screen-write.c index fc9efc50..216773bf 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2450,7 +2450,7 @@ screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, screen_write_collect_flush(ctx, 0, __func__); screen_write_initctx(ctx, &ttyctx, 0); - ttyctx.ptr = image_store(s, si); + ttyctx.ptr = image_store(s, IMAGE_SIXEL, si); tty_write(tty_cmd_sixelimage, &ttyctx); @@ -2458,6 +2458,38 @@ screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, } #endif +#ifdef ENABLE_KITTY_IMAGES +void +screen_write_kittyimage(struct screen_write_ctx *ctx, struct kitty_image *ki) +{ + struct screen *s = ctx->s; + struct tty_ctx ttyctx; + struct image *im; + + if (ki == NULL) + return; + + /* Store the image in the cache */ + im = image_store(s, IMAGE_KITTY, ki); + + /* Trigger a tty write to send to all terminals */ + if (im != NULL && ctx->wp != NULL) { + screen_write_collect_flush(ctx, 0, __func__); + screen_write_initctx(ctx, &ttyctx, 0); + ttyctx.ptr = im; + ttyctx.arg = ctx->wp; + ttyctx.ocx = s->cx; + ttyctx.ocy = s->cy; + ttyctx.set_client_cb = tty_set_client_cb; + tty_write(tty_cmd_kittyimage, &ttyctx); + } + + /* Move cursor past the image */ + if (ki->rows > 0) + screen_write_cursormove(ctx, 0, s->cy + ki->rows, 0); +} +#endif + /* Turn alternate screen on. */ void screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, diff --git a/screen.c b/screen.c index 316f7311..7cc2f55b 100644 --- a/screen.c +++ b/screen.c @@ -89,7 +89,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->tabs = NULL; s->sel = NULL; -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES TAILQ_INIT(&s->images); TAILQ_INIT(&s->saved_images); #endif @@ -127,7 +127,7 @@ screen_reinit(struct screen *s) screen_clear_selection(s); screen_free_titles(s); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES image_free_all(s); #endif @@ -164,7 +164,7 @@ screen_free(struct screen *s) hyperlinks_free(s->hyperlinks); screen_free_titles(s); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES image_free_all(s); #endif } @@ -324,7 +324,7 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow, if (sy != screen_size_y(s)) screen_resize_y(s, sy, eat_empty, &cy); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES image_free_all(s); #endif @@ -649,7 +649,7 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) } memcpy(&s->saved_cell, gc, sizeof s->saved_cell); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES TAILQ_CONCAT(&s->saved_images, &s->images, entry); #endif @@ -707,7 +707,7 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) grid_destroy(s->saved_grid); s->saved_grid = NULL; -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES image_free_all(s); TAILQ_CONCAT(&s->images, &s->saved_images, entry); #endif diff --git a/tmux.h b/tmux.h index 5ffb77e9..9ac8059d 100644 --- a/tmux.h +++ b/tmux.h @@ -68,6 +68,11 @@ struct screen_write_cline; struct screen_write_ctx; struct session; +/* Convenience macro: defined if any image protocol is compiled in. */ +#if defined(ENABLE_SIXEL_IMAGES) || defined(ENABLE_KITTY_IMAGES) +#define ENABLE_IMAGES +#endif + #ifdef ENABLE_SIXEL_IMAGES struct sixel_image; #endif @@ -930,11 +935,23 @@ struct style { enum style_default_type default_type; }; -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES +/* Image types. */ +enum image_type { + IMAGE_SIXEL, + IMAGE_KITTY +}; + /* Image. */ struct image { + enum image_type type; struct screen *s; - struct sixel_image *data; + union { + struct sixel_image *sixel; +#ifdef ENABLE_KITTY_IMAGES + struct kitty_image *kitty; +#endif + } data; char *fallback; u_int px; @@ -1027,7 +1044,7 @@ struct screen { bitstr_t *tabs; struct screen_sel *sel; -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES struct images images; struct images saved_images; #endif @@ -2664,7 +2681,7 @@ struct visible_ranges *tty_check_overlay_range(struct tty *, u_int, u_int, void tty_draw_line(struct tty *, struct screen *, u_int, u_int, u_int, u_int, u_int, const struct grid_cell *, struct colour_palette *); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES void tty_draw_images(struct client *, struct window_pane *, struct screen *); #endif @@ -2698,11 +2715,16 @@ void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *); void tty_cmd_setselection(struct tty *, const struct tty_ctx *); void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); +#ifdef ENABLE_IMAGES +int tty_set_client_cb(struct tty_ctx *, struct client *); +#endif + #ifdef ENABLE_SIXEL_IMAGES void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *); #endif #ifdef ENABLE_KITTY_IMAGES +void tty_cmd_kittyimage(struct tty *, const struct tty_ctx *); 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, @@ -3315,6 +3337,10 @@ 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 +#ifdef ENABLE_KITTY_IMAGES +void screen_write_kittyimage(struct screen_write_ctx *, + struct kitty_image *); +#endif void screen_write_alternateon(struct screen_write_ctx *, struct grid_cell *, int); @@ -3779,14 +3805,16 @@ struct window_pane *spawn_pane(struct spawn_context *, char **); /* regsub.c */ char *regsub(const char *, const char *, const char *, int); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES /* image.c */ int image_free_all(struct screen *); -struct image *image_store(struct screen *, struct sixel_image *); +struct image *image_store(struct screen *, enum image_type, void *); int image_check_line(struct screen *, u_int, u_int); int image_check_area(struct screen *, u_int, u_int, u_int, u_int); int image_scroll_up(struct screen *, u_int); +#endif +#ifdef ENABLE_SIXEL_IMAGES /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024 struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int); @@ -3804,6 +3832,7 @@ struct screen *sixel_to_screen(struct sixel_image *); /* 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_print(struct kitty_image *, size_t *); char *kitty_delete_all(size_t *); #endif diff --git a/tty.c b/tty.c index 50cdee5f..bd837123 100644 --- a/tty.c +++ b/tty.c @@ -67,7 +67,7 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code, static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); static int tty_check_overlay(struct tty *, u_int, u_int); -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *), struct client *, struct tty_ctx *); #endif @@ -1459,9 +1459,9 @@ tty_check_overlay_range(struct tty *tty, u_int px, u_int py, u_int nx) return (c->overlay_check(c, c->overlay_data, px, py, nx)); } -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES /* Update context for client. */ -static int +int tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c) { struct window_pane *wp = ttyctx->arg; @@ -1485,7 +1485,6 @@ 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_IMAGES struct image *im; struct tty_ctx ttyctx; @@ -1507,11 +1506,23 @@ tty_draw_images(struct client *c, struct window_pane *wp, struct screen *s) ttyctx.arg = wp; ttyctx.set_client_cb = tty_set_client_cb; ttyctx.allow_invisible_panes = 1; - tty_write_one(tty_cmd_sixelimage, c, &ttyctx); - } -#else - (void)c; (void)wp; (void)s; + + /* Call the appropriate rendering function based on image type */ + switch (im->type) { +#ifdef ENABLE_SIXEL_IMAGES + case IMAGE_SIXEL: + tty_write_one(tty_cmd_sixelimage, c, &ttyctx); + break; #endif +#ifdef ENABLE_KITTY_IMAGES + case IMAGE_KITTY: + tty_write_one(tty_cmd_kittyimage, c, &ttyctx); + break; +#endif + default: + break; + } + } } #endif @@ -1588,7 +1599,7 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *), } } -#ifdef ENABLE_SIXEL_IMAGES +#ifdef ENABLE_IMAGES /* Only write to the incoming tty instead of every client. */ static void tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *), @@ -2118,7 +2129,7 @@ void tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) { struct image *im = ctx->ptr; - struct sixel_image *si = im->data; + struct sixel_image *si = im->data.sixel; struct sixel_image *new; char *data; size_t size; @@ -2173,6 +2184,59 @@ tty_has_kitty(struct tty *tty) tty_term_has(tty->term, TTYC_KTY)); } +void +tty_cmd_kittyimage(struct tty *tty, const struct tty_ctx *ctx) +{ + struct image *im = ctx->ptr; + struct kitty_image *ki; + char *data; + size_t size; + u_int cx = ctx->ocx, cy = ctx->ocy; + int fallback = 0; + + log_debug("%s: called, im=%p", __func__, im); + + if (im == NULL) { + log_debug("%s: NULL image pointer", __func__); + return; + } + + ki = im->data.kitty; + log_debug("%s: ki=%p, type=%d", __func__, ki, im->type); + + if (ki == NULL) { + log_debug("%s: NULL kitty_image pointer", __func__); + return; + } + + /* Check if this terminal supports kitty graphics */ + if (!tty_has_kitty(tty)) + fallback = 1; + + log_debug("%s: image at %u,%u (fallback=%d)", __func__, cx, cy, fallback); + + if (fallback == 1) { + /* Use text fallback for non-kitty terminals */ + data = xstrdup(im->fallback); + size = strlen(data); + } else { + /* Re-serialize the kitty image command */ + data = kitty_print(ki, &size); + } + + if (data != NULL) { + log_debug("%s: %zu bytes", __func__, size); + tty_region_off(tty); + tty_margin_off(tty); + tty_cursor(tty, cx + ctx->xoff, cy + ctx->yoff); + + tty->flags |= TTY_NOBLOCK; + tty_add(tty, data, size); + tty_invalidate(tty); + free(data); + } +} + /* * 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