From a0d576351591c44e49230b42ea298c142f2435b6 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Fri, 9 Oct 2026 10:44:11 +0200 Subject: [PATCH] Fix Kitty image deletion, placeholders and placement geometry. --- grid.c | 6 +- image-kitty.c | 691 +++++++++++++++--------- image.c | 317 ++++++++--- input.c | 49 +- regress/image-kitty-clear.sh | 2 +- regress/image-kitty-protocol.sh | 157 +++++- regress/image-resize-many-placements.sh | 5 +- regress/image-support.sh | 19 +- screen-write.c | 6 +- tmux.h | 40 +- tty-draw.c | 9 + tty.c | 18 +- 12 files changed, 939 insertions(+), 380 deletions(-) diff --git a/grid.c b/grid.c index 9ef7af7c3..1e29878f8 100644 --- a/grid.c +++ b/grid.c @@ -739,7 +739,7 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg) if (nx == 0 || ny == 0) return; #ifdef ENABLE_IMAGES - image_grid_remove_sixel_spans(gd, px, py, nx, ny); + image_grid_remove_overwritten_spans(gd, px, py, nx, ny); #endif if (px == 0 && nx == gd->sx) { @@ -793,8 +793,8 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg) for (yy = py; yy < py + ny; yy++) { gl = &gd->linedata[yy]; #ifdef ENABLE_IMAGES - /* Remove any SIXEL spans invalidated by clearing this line. */ - image_grid_remove_sixel_spans(gd, 0, yy, gd->sx, 1); + /* Remove SIXEL and placeholder spans on this line. */ + image_grid_remove_overwritten_spans(gd, 0, yy, gd->sx, 1); /* Preserve the remaining Kitty spans. */ images = gl->images; #endif diff --git a/image-kitty.c b/image-kitty.c index 19181ace6..284511549 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -85,6 +86,7 @@ struct kitty_state { u_int columns; u_int rows; u_int image_id; + u_int image_number; u_int placement_id; int32_t z; u_int quiet; @@ -92,6 +94,7 @@ struct kitty_state { int virtual; u_int data_size; int more; + int unsupported; u_char *raw; size_t rawlen; @@ -101,13 +104,13 @@ struct kitty_placement { u_int placement_id; u_int server_id; int32_t z; + int virtual; struct kitty_placement *next; }; struct kitty_source { u_int app_id; u_int server_id; - u_int virtual_id; struct kitty_placement *placements; struct kitty_source *next; }; @@ -575,6 +578,7 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) const u_char *value, *end = buf + len, *comma; size_t valuelen; u_int number; + int32_t signed_number; char key; while (buf < end) { @@ -606,6 +610,20 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'o': ks->compression = value[0]; break; + case 'P': case 'Q': + if (kitty_number((const char *)value, valuelen, + &number) != 0) + return (-1); + if (number != 0) + ks->unsupported = 1; + break; + case 'H': case 'V': + if (kitty_signed_number((const char *)value, valuelen, + &signed_number) != 0) + return (-1); + if (signed_number != 0) + ks->unsupported = 1; + break; case 'z': if (kitty_signed_number((const char *)value, valuelen, &ks->z) != 0) @@ -621,6 +639,7 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'c': case 'r': case 'i': + case 'I': case 'p': case 'q': case 'm': @@ -643,6 +662,7 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'c': ks->columns = number; break; case 'r': ks->rows = number; break; case 'i': ks->image_id = number; break; + case 'I': ks->image_number = number; break; case 'p': ks->placement_id = number; break; case 'q': ks->quiet = number; break; case 'm': ks->more = (number != 0); break; @@ -683,34 +703,13 @@ kitty_placements_free(struct kitty_source *source) source->placements = NULL; } -/* Free placement images belonging to all Kitty source images. */ +/* Free a source image and its parser-side placements. */ static void -kitty_placements_free_all(struct kitty_context *kc) +kitty_source_free(struct kitty_source *source) { - struct kitty_source *source; - - for (source = kc->sources; source != NULL; source = source->next) - kitty_placements_free(source); -} - -/* Remove all parser-side placements at a Kitty z-index. */ -static void -kitty_placements_remove_z(struct kitty_context *kc, int32_t z) -{ - struct kitty_source *source; - struct kitty_placement **pp, *placement; - - for (source = kc->sources; source != NULL; source = source->next) { - for (pp = &source->placements; (placement = *pp) != NULL; ) { - if (placement->z != z) { - pp = &placement->next; - continue; - } - *pp = placement->next; - image_free(placement->server_id); - free(placement); - } - } + kitty_placements_free(source); + image_free(source->server_id); + free(source); } /* Free Kitty graphics parser state. */ @@ -725,11 +724,7 @@ kitty_free_state(void *state) kitty_state_free(kc->transfer); for (source = kc->sources; source != NULL; source = next) { next = source->next; - kitty_placements_free(source); - if (source->virtual_id != 0) - image_free(source->virtual_id); - image_free(source->server_id); - free(source); + kitty_source_free(source); } free(kc); } @@ -765,10 +760,6 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) } else { old_id = source->server_id; kitty_placements_free(source); - if (source->virtual_id != 0) { - image_free(source->virtual_id); - source->virtual_id = 0; - } image_free(source->server_id); } image_ref(image_get_id(im)); @@ -778,31 +769,32 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) /* Associate a placement ID with an image. */ static u_int -kitty_placement_set(struct kitty_context *kc, u_int image_id, - u_int placement_id, int32_t z, struct image *im) +kitty_placement_set(struct kitty_context *kc, const struct kitty_state *ks, + struct image *im) { struct kitty_source *source; struct kitty_placement *placement; u_int old_id; - if (placement_id == 0) - return (0); - source = kitty_source_find(kc, image_id); + source = kitty_source_find(kc, ks->image_id); if (source == NULL) return (0); for (placement = source->placements; placement != NULL; placement = placement->next) { - if (placement->placement_id == placement_id) - break; + if (ks->placement_id != 0) { + if (placement->placement_id == ks->placement_id) + break; + } } if (placement == NULL) { placement = xcalloc(1, sizeof *placement); - placement->placement_id = placement_id; + placement->placement_id = ks->placement_id; placement->next = source->placements; source->placements = placement; } old_id = placement->server_id; - placement->z = z; + placement->z = ks->z; + placement->virtual = ks->virtual; image_ref(image_get_id(im)); placement->server_id = image_get_id(im); if (old_id != 0) @@ -810,72 +802,6 @@ kitty_placement_set(struct kitty_context *kc, u_int image_id, return (old_id); } -/* Remove and return the image associated with a Kitty placement ID. */ -static struct image * -kitty_placement_remove(struct kitty_context *kc, u_int image_id, - u_int placement_id) -{ - struct kitty_source *source; - struct kitty_placement **pp, *placement; - struct image *im; - - source = kitty_source_find(kc, image_id); - if (source == NULL) - return (NULL); - for (pp = &source->placements; (placement = *pp) != NULL; - pp = &placement->next) { - if (placement->placement_id != placement_id) - continue; - im = image_find(placement->server_id); - if (im != NULL) - image_ref(image_get_id(im)); - *pp = placement->next; - image_free(placement->server_id); - free(placement); - return (im); - } - return (NULL); -} - -/* Replace the virtual image associated with a Kitty source image. */ -static void -kitty_virtual_set(struct kitty_context *kc, u_int id, struct image *im) -{ - struct kitty_source *source; - - source = kitty_source_find(kc, id); - if (source == NULL) - return; - if (source->virtual_id != 0) - image_free(source->virtual_id); - image_ref(image_get_id(im)); - source->virtual_id = image_get_id(im); -} - -/* Remove and return a Kitty source image. */ -static struct image * -kitty_source_remove(struct kitty_context *kc, u_int id) -{ - struct kitty_source **pp, *source; - struct image *im; - - for (pp = &kc->sources; (source = *pp) != NULL; pp = &source->next) { - if (source->app_id != id) - continue; - im = image_find(source->server_id); - if (im != NULL) - image_ref(image_get_id(im)); - *pp = source->next; - kitty_placements_free(source); - if (source->virtual_id != 0) - image_free(source->virtual_id); - image_free(source->server_id); - free(source); - return (im); - } - return (NULL); -} - /* Find and reference a Kitty source image. */ static struct image * kitty_source_get(struct kitty_context *kc, u_int id) @@ -892,6 +818,104 @@ kitty_source_get(struct kitty_context *kc, u_int id) return (im); } +/* Remove records for ordinary placements no longer present in the grid. */ +static u_int +kitty_prune_placements(struct kitty_source *source, struct grid *gd) +{ + struct kitty_placement **pp, *placement; + u_int removed = 0; + + for (pp = &source->placements; (placement = *pp) != NULL; ) { + if (placement->virtual) + goto keep; + if (image_grid_has_image(gd, placement->server_id)) + goto keep; + *pp = placement->next; + image_free(placement->server_id); + free(placement); + removed++; + continue; +keep: + pp = &placement->next; + } + return (removed); +} + +/* Delete selected placements and release unreferenced image data. */ +void +kitty_delete_images(void *state, struct screen_write_ctx *ctx, + const struct kitty_parse_result *result) +{ + struct kitty_context *kc = state; + struct grid *gd = ctx->s->grid; + struct kitty_source **sp, *source; + struct kitty_placement **pp, *placement; + u_int removed; + u_int placement_id = result->placement_id; + char how = result->delete; + int selected, release = 0; + + if (kc == NULL) + return; + if (how >= 'A') { + if (how <= 'Z') { + how += 'a' - 'A'; + release = 1; + } + } + if (how == 'r') + placement_id = 0; + for (source = kc->sources; source != NULL; source = source->next) + (void)kitty_prune_placements(source, gd); + image_clear_kitty(ctx, result); + for (sp = &kc->sources; (source = *sp) != NULL; ) { + removed = kitty_prune_placements(source, gd); + selected = 0; + if (how == 'i') { + if (source->app_id == result->image_id) + selected = 1; + } else if (how == 'r') { + if (source->app_id >= result->x) { + if (source->app_id <= result->y) + selected = 1; + } + } + if (selected) { + for (pp = &source->placements; + (placement = *pp) != NULL; ) { + if (!placement->virtual) + goto keep_placement; + if (placement_id != 0) { + if (placement->placement_id != placement_id) + goto keep_placement; + } + *pp = placement->next; + image_free(placement->server_id); + free(placement); + removed++; + continue; +keep_placement: + pp = &placement->next; + } + if (placement_id == 0) + removed++; + } + if (!release) + goto keep_source; + if (removed == 0) + goto keep_source; + if (source->placements != NULL) + goto keep_source; + if (image_grid_has_image(gd, source->server_id)) + goto keep_source; + *sp = source->next; + kitty_source_free(source); + continue; +keep_source: + sp = &source->next; + } +} + /* Decode and append one base64-encoded Kitty payload chunk. */ static int kitty_append(struct kitty_state *ks, const u_char *buf, size_t len) @@ -949,8 +973,11 @@ kitty_raw(struct kitty_state *ks, u_char *data, size_t size) u_int bytes; bytes = (ks->format == 24 ? 3 : 4); - if (ks->width == 0 || ks->height == 0 || - (uint64_t)ks->width * ks->height * bytes > IMAGE_SIZE_LIMIT) + if (ks->width == 0) + return (NULL); + if (ks->height == 0) + return (NULL); + if ((uint64_t)ks->width * ks->height > IMAGE_SIZE_LIMIT / 4) return (NULL); expected = (size_t)ks->width * ks->height * bytes; raw = data; @@ -990,88 +1017,137 @@ static struct image * kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, u_int ypixel) { - uint64_t numerator, denominator, value; - u_int x, y, width, height, sx, sy, canvas_width; - u_int canvas_height, cell_width, cell_height, source_width; - u_int source_height, display_width, display_height; + struct image_view view = { 0 }; struct image *im; + u_int cell_width, cell_height, source_width, source_height; + u_int x_offset, y_offset; + uint64_t canvas_width, canvas_height; + double width_scale, height_scale, scale = 1, units = 1; + double value; + int natural_size = 0; - x = ks->source_x; - y = ks->source_y; image_get_size(source, &source_width, &source_height); - if (x >= source_width || y >= source_height) + view.x = ks->source_x; + view.y = ks->source_y; + if (view.x >= source_width) return (NULL); - width = ks->source_width; - if (width == 0 || width > source_width - x) - width = source_width - x; - height = ks->source_height; - if (height == 0 || height > source_height - y) - height = source_height - y; - if (ks->x_offset > UINT_MAX - width || - ks->y_offset > UINT_MAX - height) + if (view.y >= source_height) return (NULL); - display_width = width + ks->x_offset; - display_height = height + ks->y_offset; - + view.width = source_width - view.x; + if (ks->source_width != 0) { + if (view.width > ks->source_width) + view.width = ks->source_width; + } + view.height = source_height - view.y; + if (ks->source_height != 0) { + if (view.height > ks->source_height) + view.height = ks->source_height; + } cell_width = (xpixel == 0 ? 8 : xpixel); cell_height = (ypixel == 0 ? 16 : ypixel); - if (ks->columns == 0 && ks->rows == 0) { - image_size_in_cells(display_width, display_height, cell_width, - cell_height, - &sx, &sy); - value = (uint64_t)sx * cell_width; - if (value > UINT_MAX) - return (NULL); - canvas_width = value; - value = (uint64_t)sy * cell_height; - if (value > UINT_MAX) - return (NULL); - canvas_height = value; - } else if (ks->columns != 0 && ks->rows != 0) { - sx = ks->columns; - sy = ks->rows; - canvas_width = display_width; - canvas_height = display_height; - } else if (ks->columns != 0) { - sx = ks->columns; - numerator = (uint64_t)display_height * sx * cell_width; - denominator = (uint64_t)display_width * cell_height; - value = (numerator + denominator - 1) / denominator; - if (value == 0 || value > UINT_MAX) - return (NULL); - sy = value; - canvas_width = display_width; - numerator = (uint64_t)sy * cell_height * display_width; - denominator = (uint64_t)sx * cell_width; - value = (numerator + denominator - 1) / denominator; - if (value < display_height) - value = display_height; - if (value > UINT_MAX) - return (NULL); - canvas_height = value; - } else { - sy = ks->rows; - numerator = (uint64_t)display_width * sy * cell_height; - denominator = (uint64_t)display_height * cell_width; - value = (numerator + denominator - 1) / denominator; - if (value == 0 || value > UINT_MAX) - return (NULL); - sx = value; - canvas_height = display_height; - numerator = (uint64_t)sx * cell_width * display_height; - denominator = (uint64_t)sy * cell_height; - value = (numerator + denominator - 1) / denominator; - if (value < display_width) - value = display_width; - if (value > UINT_MAX) - return (NULL); - canvas_width = value; + x_offset = ks->x_offset; + y_offset = ks->y_offset; + /* Match Kitty by clamping offsets to the starting cell. */ + if (x_offset >= cell_width) + x_offset = cell_width - 1; + if (y_offset >= cell_height) + y_offset = cell_height - 1; + view.sx = ks->columns; + view.sy = ks->rows; + if (view.sx > USHRT_MAX) + return (NULL); + if (view.sy > USHRT_MAX) + return (NULL); + if (view.sx == 0) { + if (view.sy == 0) + natural_size = 1; + } + if (natural_size) { + if (x_offset > UINT_MAX - view.width) + return (NULL); + if (y_offset > UINT_MAX - view.height) + return (NULL); + image_size_in_cells(view.width + x_offset, + view.height + y_offset, cell_width, cell_height, + &view.sx, &view.sy); + } else if (view.sy == 0) { + scale = ((double)view.sx * cell_width - x_offset) / view.width; + value = ceil((view.height * scale + y_offset) / cell_height); + if (value > USHRT_MAX) + return (NULL); + view.sy = value; + } else if (view.sx == 0) { + scale = ((double)view.sy * cell_height - y_offset) / + view.height; + value = ceil((view.width * scale + x_offset) / cell_width); + if (value > USHRT_MAX) + return (NULL); + view.sx = value; + } else { + width_scale = ((double)view.sx * cell_width - x_offset) / + view.width; + height_scale = ((double)view.sy * cell_height - y_offset) / + view.height; + scale = width_scale; + if (height_scale < scale) + scale = height_scale; + } + canvas_width = (uint64_t)view.sx * cell_width; + canvas_height = (uint64_t)view.sy * cell_height; + /* Preserve source resolution when shrinking the image. */ + if (scale < 1) + units = 1 / scale; + value = ceil(canvas_width * units); + if (value > UINT_MAX) + return (NULL); + view.canvas_width = value; + value = ceil(canvas_height * units); + if (value > UINT_MAX) + return (NULL); + view.canvas_height = value; + view.scaled_width = floor(view.width * scale * units); + if (view.scaled_width < view.width) + view.scaled_width = view.width; + view.scaled_height = floor(view.height * scale * units); + if (view.scaled_height < view.height) + view.scaled_height = view.height; + view.x_offset = floor(x_offset * units); + view.y_offset = floor(y_offset * units); + if (view.x_offset > view.canvas_width) + return (NULL); + if (view.scaled_width > view.canvas_width - view.x_offset) + return (NULL); + if (view.y_offset > view.canvas_height) + return (NULL); + if (view.scaled_height > view.canvas_height - view.y_offset) + return (NULL); + if (ks->columns != 0) { + if (ks->rows != 0) { + view.x_offset += (view.canvas_width - view.x_offset - + view.scaled_width) / 2; + view.y_offset += (view.canvas_height - view.y_offset - + view.scaled_height) / 2; + } + } + /* A matching rectangle needs neither resampling nor padding. */ + if (x_offset == 0) { + if (y_offset == 0) { + if ((uint64_t)view.width * canvas_height == + (uint64_t)view.height * canvas_width) { + view.canvas_width = view.width; + view.scaled_width = view.width; + view.canvas_height = view.height; + view.scaled_height = view.height; + view.x_offset = 0; + view.y_offset = 0; + } + } + } + im = image_create_view(source, &view); + if (im != NULL) { + if (ks->no_cursor) + image_set_no_cursor(im); } - - im = image_create_view(source, x, y, width, height, canvas_width, - canvas_height, sx, sy, ks->x_offset, ks->y_offset); - if (im != NULL && ks->no_cursor) - image_set_no_cursor(im); return (im); } @@ -1086,6 +1162,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, { struct kitty_context *kc = *state; struct kitty_state *ks; + struct kitty_state command = { .action = 't' }; const u_char *semi; u_char *decoded, *pixels; u_char *uncompressed; @@ -1094,7 +1171,6 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, u_int sx, sy, cell_width, cell_height; uint64_t canvas_width, canvas_height; struct image *im = NULL, *source; - struct kitty_source *stored; int error; if (kc == NULL) { @@ -1107,6 +1183,14 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, semi = memchr(buf, ';', len); controllen = (semi == NULL ? len : (size_t)(semi - buf)); payloadlen = (semi == NULL ? 0 : len - controllen - 1); + if (ks != NULL) { + /* A delete command aborts an incomplete upload. */ + (void)kitty_control(&command, buf, controllen); + if (command.action == 'd') { + kitty_state_free(ks); + kc->transfer = ks = NULL; + } + } if (ks == NULL) { ks = xcalloc(1, sizeof *ks); ks->action = 't'; @@ -1116,18 +1200,35 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, } ks->more = 0; error = kitty_control(ks, buf, controllen); - if (ks->image_id == 0) - ks->placement_id = 0; + if (ks->image_id == 0) { + if (ks->image_number == 0) + ks->placement_id = 0; + } result->image_id = ks->image_id; + result->image_number = ks->image_number; result->quiet = ks->quiet; result->action = ks->action; result->delete = ks->delete; result->placement_id = ks->placement_id; result->z = ks->z; + result->x = ks->source_x; + result->y = ks->source_y; if (error != 0) goto fail; - if (ks->medium != 'd') + if (ks->image_number != 0) { + if (ks->image_id != 0) + goto fail; + result->status = KITTY_PARSE_UNSUPPORTED; goto fail; + } + if (ks->unsupported) { + result->status = KITTY_PARSE_UNSUPPORTED; + goto fail; + } + if (ks->medium != 'd') { + result->status = KITTY_PARSE_UNSUPPORTED; + goto fail; + } if (payloadlen != 0) { if (kitty_append(ks, semi + 1, payloadlen) != 0) goto fail; @@ -1147,7 +1248,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, im = kitty_place_image(source, ks, xpixel, ypixel); image_free(image_get_id(source)); if (im != NULL) { - kitty_virtual_set(kc, ks->image_id, im); + (void)kitty_placement_set(kc, ks, im); image_free(image_get_id(im)); im = NULL; result->action = 'u'; @@ -1157,8 +1258,8 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, im = kitty_place_image(source, ks, xpixel, ypixel); image_free(image_get_id(source)); if (im != NULL) { - result->replace_id = kitty_placement_set(kc, ks->image_id, - ks->placement_id, ks->z, im); + result->replace_id = + kitty_placement_set(kc, ks, im); result->status = KITTY_PARSE_OK; } } @@ -1166,39 +1267,30 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, return (im); } if (ks->action == 'd') { - if (ks->delete == 'a' || ks->delete == 'A') { - kitty_placements_free_all(kc); - im = NULL; - } else if (ks->delete == 'i') { - if (ks->placement_id != 0) - im = kitty_placement_remove(kc, ks->image_id, - ks->placement_id); - else { - source = kitty_source_get(kc, ks->image_id); - if (source != NULL) { - stored = kitty_source_find(kc, - ks->image_id); - kitty_placements_free(stored); - } - im = source; - } - } else if (ks->delete == 'I') - im = kitty_source_remove(kc, ks->image_id); - else if (ks->delete == 'z' || ks->delete == 'Z') { - kitty_placements_remove_z(kc, ks->z); - im = NULL; - } else - goto fail; - if ((ks->delete == 'i' || ks->delete == 'I') && - im == NULL) - result->status = KITTY_PARSE_MISSING; - else + switch (ks->delete) { + case 'a': case 'A': case 'i': case 'I': + case 'c': case 'C': case 'p': case 'P': + case 'q': case 'Q': case 'r': case 'R': + case 'x': case 'X': case 'y': case 'Y': + case 'z': case 'Z': result->status = KITTY_PARSE_OK; + break; + default: + result->status = KITTY_PARSE_UNSUPPORTED; + break; + } kitty_state_free(ks); - return (im); + return (NULL); } - if (ks->action != 'T' && ks->action != 't' && ks->action != 'q') + switch (ks->action) { + case 'T': case 't': case 'q': + break; + case 'f': case 'a': case 'c': + result->status = KITTY_PARSE_UNSUPPORTED; + /* FALLTHROUGH */ + default: goto fail; + } decoded = ks->raw; decodedlen = ks->rawlen; @@ -1263,15 +1355,14 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) result->status = KITTY_PARSE_ERROR; - else if (ks->placement_id != 0) - (void)kitty_placement_set(kc, ks->image_id, - ks->placement_id, ks->z, im); + else + (void)kitty_placement_set(kc, ks, im); } else if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) result->status = KITTY_PARSE_ERROR; else { - kitty_virtual_set(kc, ks->image_id, im); + (void)kitty_placement_set(kc, ks, im); image_free(image_get_id(im)); im = NULL; result->action = 'u'; @@ -1344,6 +1435,44 @@ kitty_placeholder_index(uint32_t value, u_int *index) return (0); } +/* Return whether a cell starts with a Kitty Unicode placeholder. */ +int +kitty_cell_is_placeholder(const struct grid_cell *gc) +{ + if (gc->data.size < 4) + return (0); + if (gc->data.data[0] != 0xf4) + return (0); + if (gc->data.data[1] != 0x8e) + return (0); + if (gc->data.data[2] != 0xbb) + return (0); + if (gc->data.data[3] != 0xae) + return (0); + return (1); +} + +/* Decode an image or placement ID from a placeholder colour. */ +static int +kitty_colour_id(int colour, u_int *id) +{ + if (colour < 0) + return (0); + if (colour & COLOUR_FLAG_RGB) + *id = colour & 0xffffff; + else if (colour & COLOUR_FLAG_256) + *id = colour & 0xff; + else if (colour < 8) + *id = colour; + else if (colour >= 90) { + if (colour >= 98) + return (0); + *id = colour - 90 + 8; + } else + return (0); + return (1); +} + /* Resolve a Kitty Unicode placeholder to an image and source cell. */ int kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, @@ -1351,60 +1480,95 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, { struct kitty_context *kc = state; struct kitty_source *source; + struct kitty_placement *placement; + struct kitty_placeholder left; + struct grid_cell left_cell; struct image *im; uint32_t value; size_t offset = 0; - u_int values[3], nvalues = 0, id, x, y, sx, sy; - u_int left_x, left_y; + u_int values[3], nvalues = 0, id, x = 0, y = 0; + u_int sx, sy, placement_id = 0; + int inherit = 0; - if (kc == NULL || - !kitty_placeholder_character(gc->data.data, gc->data.size, &offset, - &value) || value != 0x10eeee) + if (kc == NULL) + return (0); + if (!kitty_placeholder_character(gc->data.data, gc->data.size, &offset, + &value)) + return (0); + if (value != 0x10eeee) return (0); while (offset < gc->data.size && nvalues < nitems(values)) { if (!kitty_placeholder_character(gc->data.data, gc->data.size, - &offset, &value) || - !kitty_placeholder_index(value, &values[nvalues])) + &offset, &value)) + return (0); + if (!kitty_placeholder_index(value, &values[nvalues])) return (0); nvalues++; } if (offset != gc->data.size) return (0); - if (gc->fg & COLOUR_FLAG_RGB) - id = gc->fg & 0xffffff; - else if (gc->fg >= 0 && gc->fg <= 255) - id = gc->fg; - else + if (!kitty_colour_id(gc->fg, &id)) return (0); - if (nvalues == 3) + (void)kitty_colour_id(gc->us, &placement_id); + if (nvalues >= 1) + y = values[0]; + if (nvalues >= 2) + x = values[1]; + if (grid_x != 0) { + grid_view_get_cell(gd, grid_x - 1, grid_y, &left_cell); + if (left_cell.fg == gc->fg) { + if (left_cell.us == gc->us) { + inherit = image_grid_get_placeholder(gd, + grid_x - 1, gd->hsize + grid_y, &left); + } + } + } + if (inherit) { + if (nvalues >= 1) { + if (left.source_y != y) + inherit = 0; + } + if (nvalues >= 2) { + if (left.source_x + 1 != x) + inherit = 0; + } + } + if (inherit) { + if (nvalues == 0) + y = left.source_y; + if (nvalues < 2) + x = left.source_x + 1; + if (nvalues < 3) + id |= left.image_id & 0xff000000; + } + if (nvalues == 3) { + if (values[2] > 255) + return (0); id |= values[2] << 24; + } source = kitty_source_find(kc, id); if (source == NULL) return (0); - im = image_find(source->virtual_id != 0 ? source->virtual_id : - source->server_id); + for (placement = source->placements; placement != NULL; + placement = placement->next) { + if (!placement->virtual) + continue; + if (placement_id != 0) { + if (placement->placement_id != placement_id) + continue; + } + break; + } + if (placement != NULL) { + im = image_find(placement->server_id); + placeholder->z = placement->z; + placement_id = placement->placement_id; + } else + return (0); if (im == NULL) return (0); image_get_size_in_cells(im, &sx, &sy); - - if (nvalues >= 1) - y = values[0]; - else { - if (grid_x == 0 || !image_grid_get_source(gd, grid_x - 1, - gd->hsize + grid_y, im, &left_x, &left_y)) - return (0); - y = left_y; - } - if (nvalues >= 2) - x = values[1]; - else { - if (grid_x == 0 || !image_grid_get_source(gd, grid_x - 1, - gd->hsize + grid_y, im, &left_x, &left_y) || - left_x == UINT_MAX) - return (0); - x = left_x + 1; - } if (x >= sx || y >= sy) return (0); @@ -1412,11 +1576,6 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, placeholder->source_x = x; placeholder->source_y = y; placeholder->image_id = id; - if (gc->us & COLOUR_FLAG_RGB) - placeholder->placement_id = gc->us & 0xffffff; - else - placeholder->placement_id = 0; - placeholder->z = 0; - utf8_set(&gc->data, ' '); + placeholder->placement_id = placement_id; return (1); } diff --git a/image.c b/image.c index b1b5b6341..947c5ea33 100644 --- a/image.c +++ b/image.c @@ -63,6 +63,7 @@ struct image_line { enum image_input { IMAGE_INPUT_ALL = -1, + IMAGE_INPUT_OVERWRITTEN = -2, IMAGE_INPUT_SIXEL, IMAGE_INPUT_KITTY }; @@ -72,6 +73,7 @@ struct image_placement { struct image_store *store; struct image *image; enum image_input input; + int placeholder; u_int app_image_id; u_int app_placement_id; int32_t z; @@ -391,8 +393,15 @@ image_line_remove(struct image_line *line, u_int x, u_int width, TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { if (only != NULL && span->placement != only) continue; - if (input != IMAGE_INPUT_ALL && span->placement->input != input) - continue; + if (input == IMAGE_INPUT_OVERWRITTEN) { + if (span->placement->input != IMAGE_INPUT_SIXEL) { + if (!span->placement->placeholder) + continue; + } + } else if (input != IMAGE_INPUT_ALL) { + if (span->placement->input != input) + continue; + } span_end = span->x + span->sx; if (span_end <= x || span->x >= end) continue; @@ -431,10 +440,10 @@ image_store_prune(struct image_store *store) } } -/* Remove SIXEL spans overwritten by text. */ +/* Remove SIXEL and placeholder spans overwritten by text. */ void -image_grid_remove_sixel_spans(struct grid *gd, u_int x, u_int y, u_int width, - u_int height) +image_grid_remove_overwritten_spans(struct grid *gd, u_int x, u_int y, + u_int width, u_int height) { u_int row; @@ -446,7 +455,7 @@ image_grid_remove_sixel_spans(struct grid *gd, u_int x, u_int y, u_int width, height = gd->hsize + gd->sy - y; for (row = y; row < y + height; row++) image_line_remove(gd->linedata[row].images, x, width, - IMAGE_INPUT_SIXEL, NULL); + IMAGE_INPUT_OVERWRITTEN, NULL); image_store_prune(gd->images); } @@ -536,6 +545,7 @@ image_copy_placement(struct image_copy_ctx *ctx, placement = image_placement_create(ctx->destination, source->image, source->input, source->app_image_id, source->app_placement_id, source->z); + placement->placeholder = source->placeholder; ctx->maps = xreallocarray(ctx->maps, ctx->count + 1, sizeof *ctx->maps); ctx->maps[ctx->count].source = source; @@ -651,10 +661,10 @@ image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int width, return (0); } -/* Find source coordinates for an image span at one grid cell. */ +/* Find the resolved placeholder at one grid cell. */ int -image_grid_get_source(struct grid *gd, u_int x, u_int y, struct image *im, - u_int *source_x, u_int *source_y) +image_grid_get_placeholder(struct grid *gd, u_int x, u_int y, + struct kitty_placeholder *placeholder) { struct image_line *line; struct image_span *span, *found = NULL; @@ -663,14 +673,21 @@ image_grid_get_source(struct grid *gd, u_int x, u_int y, struct image *im, (line = gd->linedata[y].images) == NULL) return (0); TAILQ_FOREACH(span, &line->spans, line_entry) { - if (span->placement->image == im && x >= span->x && - x < span->x + span->sx) + if (!span->placement->placeholder) + continue; + if (x < span->x) + continue; + if (x < span->x + span->sx) found = span; } if (found == NULL) return (0); - *source_x = found->source_x + x - found->x; - *source_y = found->source_y; + placeholder->image = found->placement->image; + placeholder->source_x = found->source_x + x - found->x; + placeholder->source_y = found->source_y; + placeholder->image_id = found->placement->app_image_id; + placeholder->placement_id = found->placement->app_placement_id; + placeholder->z = found->placement->z; return (1); } @@ -688,6 +705,8 @@ image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, TAILQ_FOREACH_REVERSE(candidate, &store->placements, image_placements, entry) { + if (!candidate->placeholder) + continue; if (candidate->input == IMAGE_INPUT_KITTY && candidate->image == im && candidate->app_image_id == image_id && @@ -697,9 +716,11 @@ image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, break; } } - if (placement == NULL) + if (placement == NULL) { placement = image_placement_create(gd, im, IMAGE_INPUT_KITTY, image_id, placement_id, z); + placement->placeholder = 1; + } line = image_line_get(&gd->linedata[gd->hsize + y]); TAILQ_FOREACH(span, &line->spans, line_entry) { if (span->placement == placement && span->x + span->sx == x && @@ -950,52 +971,109 @@ image_create(u_int width, u_int height, u_int canvas_width, if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells || sx > USHRT_MAX || sy > USHRT_MAX) return (NULL); + if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof *im->cells) + return (NULL); im = image_alloc(width, height, canvas_width, canvas_height, sx, sy, (size_t)width * 4, pixels); im->flags |= IMAGE_FLAG_OWN_PIXELS; return (im); } -/* Create a cell-aligned view of an existing image with an optional offset. */ +/* Create a cropped and scaled image view with transparent padding. */ struct image * -image_create_view(struct image *source, u_int x, u_int y, u_int width, - u_int height, u_int canvas_width, u_int canvas_height, u_int sx, u_int sy, - u_int x_offset, u_int y_offset) +image_create_view(struct image *source, const struct image_view *view) { struct image *im; - u_char *pixels; - u_int padded_width, padded_height, yy; + u_char *pixels, *row; + const u_char *source_row; + u_int padded_width, padded_height, x, y, source_x, source_y; + int share_pixels = 1; - if (source == NULL || x >= source->width || y >= source->height || - width == 0 || width > source->width - x || height == 0 || - height > source->height - y || canvas_width < width || - canvas_height < height || sx == 0 || sy == 0) + if (source == NULL) return (NULL); - if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells || - sx > USHRT_MAX || sy > USHRT_MAX) + if (view->x >= source->width) + return (NULL); + if (view->y >= source->height) + return (NULL); + if (view->width == 0) + return (NULL); + if (view->width > source->width - view->x) + return (NULL); + if (view->height == 0) + return (NULL); + if (view->height > source->height - view->y) + return (NULL); + if (view->scaled_width == 0) + return (NULL); + if (view->scaled_height == 0) + return (NULL); + if (view->sx == 0) + return (NULL); + if (view->sy == 0) + return (NULL); + if (view->sx > USHRT_MAX) + return (NULL); + if (view->sy > USHRT_MAX) + return (NULL); + if ((uint64_t)view->sx * view->sy > SIZE_MAX / sizeof *im->cells) + return (NULL); + if ((uint64_t)view->sx * view->sy > + IMAGE_SIZE_LIMIT / sizeof *im->cells) + return (NULL); + if (view->x_offset > UINT_MAX - view->scaled_width) + return (NULL); + if (view->y_offset > UINT_MAX - view->scaled_height) + return (NULL); + padded_width = view->scaled_width + view->x_offset; + padded_height = view->scaled_height + view->y_offset; + if (padded_width > view->canvas_width) + return (NULL); + if (padded_height > view->canvas_height) return (NULL); - if (x_offset == 0 && y_offset == 0) { - im = image_alloc(width, height, canvas_width, canvas_height, - sx, sy, source->stride, source->pixels + - (size_t)y * source->stride + (size_t)x * 4); + /* Share pixels when no scaling or padding is needed. */ + if (view->scaled_width != view->width) + share_pixels = 0; + if (view->scaled_height != view->height) + share_pixels = 0; + if (view->x_offset != 0) + share_pixels = 0; + if (view->y_offset != 0) + share_pixels = 0; + if (share_pixels) { + im = image_alloc(view->width, view->height, view->canvas_width, + view->canvas_height, view->sx, view->sy, source->stride, + source->pixels + (size_t)view->y * source->stride + + (size_t)view->x * 4); } else { - if (x_offset > UINT_MAX - width || - y_offset > UINT_MAX - height) - return (NULL); - padded_width = width + x_offset; - padded_height = height + y_offset; - if ((uint64_t)padded_width * padded_height * 4 > SIZE_MAX) + if ((uint64_t)padded_width * padded_height > + IMAGE_SIZE_LIMIT / 4) return (NULL); pixels = xcalloc((size_t)padded_width * padded_height, 4); - for (yy = 0; yy < height; yy++) { - memcpy(pixels + (size_t)(yy + y_offset) * padded_width * 4 + - (size_t)x_offset * 4, - source->pixels + (size_t)(y + yy) * source->stride + - (size_t)x * 4, (size_t)width * 4); + for (y = 0; y < view->scaled_height; y++) { + source_y = view->y + (uint64_t)y * view->height / + view->scaled_height; + source_row = source->pixels + + (size_t)source_y * source->stride; + source_row += (size_t)view->x * 4; + row = pixels + + ((size_t)(y + view->y_offset) * padded_width + + view->x_offset) * 4; + if (view->scaled_width == view->width) { + memcpy(row, source_row, + (size_t)view->width * 4); + continue; + } + for (x = 0; x < view->scaled_width; x++) { + source_x = (uint64_t)x * view->width / + view->scaled_width; + memcpy(row + (size_t)x * 4, + source_row + (size_t)source_x * 4, 4); + } } - im = image_alloc(padded_width, padded_height, canvas_width, - canvas_height, sx, sy, (size_t)padded_width * 4, pixels); + im = image_alloc(padded_width, padded_height, + view->canvas_width, view->canvas_height, view->sx, view->sy, + (size_t)padded_width * 4, pixels); im->flags |= IMAGE_FLAG_OWN_PIXELS; } im->parent_id = source->id; @@ -1075,8 +1153,12 @@ image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, return (-1); placement = found->placement; if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { - if (gc->data.size != 1 || gc->data.data[0] != ' ') - return (-1); + if (!kitty_cell_is_placeholder(gc)) { + if (gc->data.size != 1) + return (-1); + if (gc->data.data[0] != ' ') + return (-1); + } if (placement->z < IMAGE_Z_BELOW_BACKGROUND && !COLOUR_DEFAULT(gc->bg)) return (-1); @@ -1227,44 +1309,151 @@ image_clear(struct screen_write_ctx *ctx, u_int id) if (store == NULL) return; TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { - if (id != 0 && placement->image->id != id && - placement->image->source_id != id) - continue; + if (id != 0) { + if (placement->image->source_id != id) { + if (placement->placeholder) + continue; + if (placement->image->id != id) + continue; + } + } image_remove_placement(placement); } if (ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; } -/* Clear Kitty placements selected by application identity or z-index. */ -void -image_clear_kitty(struct screen_write_ctx *ctx, char how, u_int image_id, - u_int placement_id, int32_t z) +/* Return whether a grid still references an image or its source. */ +int +image_grid_has_image(struct grid *gd, u_int id) { - struct image_store *store = ctx->s->grid->images; + struct image_placement *placement; + + if (gd->images == NULL) + return (0); + TAILQ_FOREACH(placement, &gd->images->placements, entry) { + if (placement->image->id == id) + return (1); + if (placement->image->source_id == id) + return (1); + } + return (0); +} + +/* Return whether a placement intersects the selected screen cells. */ +static int +image_placement_intersects(struct grid *gd, struct image_placement *placement, + const struct kitty_parse_result *result) +{ + struct image_line *line; + struct image_span *span; + u_int x = result->x, y = result->y, row; + char how = result->delete; + int match_x = 0, match_y = 0; + + if (how >= 'A') { + if (how <= 'Z') + how += 'a' - 'A'; + } + switch (how) { + case 'p': case 'q': case 'c': + match_y = 1; + /* FALLTHROUGH */ + case 'x': + match_x = 1; + break; + case 'y': + match_y = 1; + break; + } + for (row = 0; row < gd->sy; row++) { + if (match_y) { + if (y == 0) + continue; + if (row != y - 1) + continue; + } + line = gd->linedata[gd->hsize + row].images; + if (line == NULL) + continue; + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement != placement) + continue; + if (span->x >= gd->sx) + continue; + if (match_x) { + if (x == 0) + continue; + if (x > gd->sx) + continue; + if (x - 1 < span->x) + continue; + if (x - 1 >= span->x + span->sx) + continue; + } + return (1); + } + } + return (0); +} + +/* Clear ordinary Kitty placements selected by a graphics delete command. */ +void +image_clear_kitty(struct screen_write_ctx *ctx, + const struct kitty_parse_result *result) +{ + struct grid *gd = ctx->s->grid; + struct image_store *store = gd->images; struct image_placement *placement, *next; + struct kitty_parse_result selection = *result; + char how = result->delete; int matched; + u_int placement_id = result->placement_id; if (store == NULL) return; + if (how >= 'A') { + if (how <= 'Z') + how += 'a' - 'A'; + } + if (how == 'c') { + selection.x = ctx->s->cx + 1; + selection.y = ctx->s->cy + 1; + } TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { if (placement->input != IMAGE_INPUT_KITTY) continue; + if (placement->placeholder) + continue; matched = 0; switch (how) { - case 'a': case 'A': + case 'i': + if (placement->app_image_id != result->image_id) + break; + if (placement_id != 0) { + if (placement->app_placement_id != placement_id) + break; + } matched = 1; break; - case 'i': - matched = (placement->app_image_id == image_id && - (placement_id == 0 || - placement->app_placement_id == placement_id)); + case 'r': + if (placement->app_image_id < result->x) + break; + if (placement->app_image_id > result->y) + break; + matched = 1; break; - case 'I': - matched = (placement->app_image_id == image_id); + case 'z': + if (placement->z == result->z) + matched = 1; break; - case 'z': case 'Z': - matched = (placement->z == z); + case 'q': + if (placement->z != result->z) + break; + /* FALLTHROUGH */ + case 'a': case 'c': case 'p': case 'x': case 'y': + matched = image_placement_intersects(gd, placement, + &selection); break; } if (matched) @@ -1347,6 +1536,8 @@ image_cell_has_text(struct grid *gd, u_int x, u_int y) struct grid_cell gc; grid_view_get_cell(gd, x, y, &gc); + if (kitty_cell_is_placeholder(&gc)) + return (0); if (gc.data.size != 1 || gc.data.data[0] != ' ') return (1); return (gc.attr != 0); @@ -1561,6 +1752,8 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) for (i = 0; i < nseen; i++) { placement = seen[i]; + if (placement->placeholder) + continue; cx = end_x = source_y = 0; found = 0; diff --git a/input.c b/input.c index 63fad16ea..4b6d66561 100644 --- a/input.c +++ b/input.c @@ -2805,9 +2805,11 @@ input_enter_apc(struct input_ctx *ictx) #ifdef ENABLE_IMAGES /* Reply to a Kitty graphics command with its image and placement IDs. */ static void -input_reply_kitty(struct input_ctx *ictx, struct kitty_parse_result *result, - const char *message) +input_reply_kitty(struct input_ctx *ictx, + const struct kitty_parse_result *result, const char *message) { + char ids[96]; + if (result->quiet >= 2) return; if (result->quiet == 1) { @@ -2815,15 +2817,26 @@ input_reply_kitty(struct input_ctx *ictx, struct kitty_parse_result *result, return; } if (result->image_id == 0) { - if (result->action != 'q') - return; + if (result->image_number == 0) { + if (result->action != 'q') + return; + } + } + if (result->image_number != 0) { + if (result->image_id != 0) { + xsnprintf(ids, sizeof ids, "i=%u,I=%u", + result->image_id, result->image_number); + } else + xsnprintf(ids, sizeof ids, "I=%u", + result->image_number); + } else { + xsnprintf(ids, sizeof ids, "i=%u", result->image_id); } if (result->placement_id != 0) { - input_reply(ictx, 0, "\033_Gi=%u,p=%u;%s\033\\", - result->image_id, result->placement_id, message); + input_reply(ictx, 0, "\033_G%s,p=%u;%s\033\\", ids, + result->placement_id, message); } else { - input_reply(ictx, 0, "\033_Gi=%u;%s\033\\", result->image_id, - message); + input_reply(ictx, 0, "\033_G%s;%s\033\\", ids, message); } } @@ -2845,23 +2858,20 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) if (result.status != KITTY_PARSE_OK) { if (result.status == KITTY_PARSE_MISSING) input_reply_kitty(ictx, &result, "ENOENT"); + else if (result.status == KITTY_PARSE_UNSUPPORTED) + input_reply_kitty(ictx, &result, "ENOTSUP"); else input_reply_kitty(ictx, &result, "EINVAL"); return (1); } if (result.replace_id != 0) image_clear(sctx, result.replace_id); - if (im != NULL) { - if (result.action == 'd') - image_clear_kitty(sctx, result.delete, result.image_id, - result.placement_id, result.z); - else - image_write_kitty(sctx, im, ictx->cell.cell.bg, - result.image_id, result.placement_id, result.z); + if (result.action == 'd') { + kitty_delete_images(ictx->kitty_state, sctx, &result); + } else if (im != NULL) { + image_write_kitty(sctx, im, ictx->cell.cell.bg, + result.image_id, result.placement_id, result.z); image_free(image_get_id(im)); - } else if (result.action == 'd') { - image_clear_kitty(sctx, result.delete, result.image_id, - result.placement_id, result.z); } if (result.action != 'd') input_reply_kitty(ictx, &result, "OK"); @@ -2983,9 +2993,10 @@ input_top_bit_set(struct input_ctx *ictx) if (sctx->s->cx != 0) { x = sctx->s->cx - 1; /* cx-1 is the cell just written. */ grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc); + image_grid_remove_overwritten_spans(sctx->s->grid, x, + sctx->s->grid->hsize + sctx->s->cy, 1, 1); if (kitty_placeholder_to_image(ictx->kitty_state, sctx->s->grid, &gc, x, sctx->s->cy, &placeholder)) { - grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc); image_place_cell_kitty(sctx, placeholder.image, x, sctx->s->cy, placeholder.source_x, placeholder.source_y, placeholder.image_id, diff --git a/regress/image-kitty-clear.sh b/regress/image-kitty-clear.sh index e9aad3be2..4fd6d6abc 100644 --- a/regress/image-kitty-clear.sh +++ b/regress/image-kitty-clear.sh @@ -13,7 +13,7 @@ TMUX2="$TEST_TMUX -Lkitty-clear-outer$$ -f/dev/null" trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15 $TMUX new-session -d -x 10 -y 4 " - printf '\033_Ga=T,q=2,f=24,s=1,v=1,c=1,r=1,C=1;////\033\\' + printf '\033_Ga=T,q=2,f=24,s=1,v=2,c=1,r=1,C=1;////////\033\\' read line printf '\033[H\033[2K' read line diff --git a/regress/image-kitty-protocol.sh b/regress/image-kitty-protocol.sh index 1fde613fb..2fbe2621a 100644 --- a/regress/image-kitty-protocol.sh +++ b/regress/image-kitty-protocol.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Kitty cursor movement, acknowledgements and query isolation. +# Kitty graphics layout, deletion, placeholders and replies. PATH=/bin:/usr/bin TERM=screen @@ -14,6 +14,7 @@ command -v python3 >/dev/null || exit 0 python3 - <<'PY' import base64 import json +import re import os from pathlib import Path import shlex @@ -24,6 +25,8 @@ import zlib tmux = [os.environ['TEST_TMUX'], '-u', '-Limage-protocol' + str(os.getpid()), '-f/dev/null'] +outer = [os.environ['TEST_TMUX'], '-u', '-Limage-protocol-outer' + str(os.getpid()), + '-f/dev/null'] def run(*args): return subprocess.check_output(tmux + list(args), text=True).strip() @@ -36,7 +39,8 @@ reader = ''' import json, os, select, sys, termios, time, tty command, output = sys.argv[1:] tty.setraw(0) -os.write(1, command.encode() + b'\\x1b[c') +with open(command, 'rb') as f: + os.write(1, f.read() + b'\\x1b[c') reply = b'' deadline = time.monotonic() + 3 while time.monotonic() < deadline: @@ -51,10 +55,12 @@ with open(output, 'w') as f: time.sleep(30) ''' -def check(name, command, cursor, expected='', text=None): +def check(name, command, cursor, expected='', text=None, render=None, resize=None): output = directory / name + command_file = directory / (name + '.input') + command_file.write_text(command, encoding='utf-8') pane_command = 'python3 ' + shlex.quote(str(helper)) + ' ' + \ - shlex.quote(command) + ' ' + shlex.quote(str(output)) + shlex.quote(str(command_file)) + ' ' + shlex.quote(str(output)) pane = run('new-window', '-d', '-P', '-F', '#{pane_id}', pane_command) try: deadline = time.monotonic() + 5 @@ -64,11 +70,29 @@ def check(name, command, cursor, expected='', text=None): time.sleep(0.05) reply = json.loads(output.read_text()) assert reply == expected, (name, 'reply', repr(reply), repr(expected)) + if resize is not None: + run('resize-window', '-t', pane, '-x', str(resize[0]), '-y', str(resize[1])) actual = run('display-message', '-pt', pane, '#{cursor_x},#{cursor_y}') assert actual == cursor, (name, 'cursor', actual, cursor) if text is not None: actual = run('capture-pane', '-pt', pane, '-S0', '-E0') assert actual == text, (name, 'text', actual, text) + if render is not None: + run('select-window', '-t', pane) + attach = shlex.join(tmux + ['attach-session']) + client = subprocess.check_output(outer + ['new-window', '-d', '-P', + '-F', '#{pane_id}', attach], text=True).strip() + try: + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + actual = subprocess.check_output(outer + ['capture-pane', '-pe', + '-t', client, '-S0', '-E0'], text=True).rstrip('\n') + if re.search(render, actual): + break + time.sleep(0.05) + assert re.search(render, actual), (name, 'render', repr(actual), render) + finally: + subprocess.run(outer + ['kill-window', '-t', client], check=True) finally: run('kill-window', '-t', pane) @@ -80,6 +104,12 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp: run('new-session', '-d', '-x', '40', '-y', '12') if run('display-message', '-p', '#{image_support}') == '0': raise SystemExit(0) + run('set', '-g', 'status', 'off') + run('set', '-as', 'terminal-features', ',*:sixel@') + run('set', '-as', 'terminal-features', ',*:kitty@') + run('set', '-as', 'terminal-features', ',*:RGB') + subprocess.run(outer + ['new-session', '-d', '-x', '40', '-y', '12'], check=True) + subprocess.run(outer + ['set', '-g', 'status', 'off'], check=True) origin = '\033[3;6H' # Column 6, row 3 (zero-based 5,2). pixel = '/wAA/w==' placement = 'a=T,q=2,f=32,s=1,v=1,c=3,r=2' @@ -123,8 +153,125 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp: # valid for the original three-column placement, but not the query. check('query-virtual', graphics(placement + ',U=1,i=7', pixel) + graphics('a=q,q=2,U=1,i=7,f=32,s=1,v=1,c=1,r=1', pixel) + - '\033[38;2;0;0;7m\U0010eeee\u0305\u030e', '1,0', text='') + '\033[38;2;0;0;7m\U0010eeee\u0305\u030e', '1,0', + text='\U0010eeee\u0305\u030e') + # Uppercase deletion must leave other numbered and unnumbered placements. + transmit = graphics('a=t,q=2,i=7,f=32,s=1,v=1', pixel) + check('delete-one-placement', transmit + graphics('a=p,q=2,i=7,p=1,C=1') + + graphics('a=p,q=2,i=7,p=2,C=1') + graphics('a=d,d=I,q=2,i=7,p=1') + + graphics('a=p,i=7,p=2,C=1'), '0,0', graphics('i=7,p=2', 'OK')) + check('delete-last-placement', transmit + graphics('a=p,q=2,i=7,p=1,C=1') + + graphics('a=d,d=I,q=2,i=7,p=1') + graphics('a=p,i=7'), '0,0', + graphics('i=7', 'ENOENT')) + check('delete-retain-unnumbered', transmit + graphics('a=p,q=2,i=7,p=1,C=1') + + graphics('a=p,q=2,i=7,C=1') + graphics('a=d,d=I,q=2,i=7,p=1') + + graphics('a=p,i=7,C=1'), '0,0', graphics('i=7', 'OK')) + check('delete-soft', transmit + graphics('a=p,q=2,i=7,p=1,C=1') + + graphics('a=d,d=i,q=2,i=7,p=1') + graphics('a=p,i=7,C=1'), + '0,0', graphics('i=7', 'OK')) + for selector in ['A', 'Z']: + check('delete-hard-' + selector, transmit + graphics('a=p,q=2,i=7,C=1,z=9') + + graphics('a=d,d=%s,z=9,q=2' % selector) + graphics('a=p,i=7'), + '0,0', graphics('i=7', 'ENOENT')) + # A fully historical placement retains its source after visible deletion. + check('delete-history', transmit + graphics('a=p,q=2,i=7,C=1') + '\n' * 13 + + graphics('a=d,d=A,q=2') + graphics('a=p,i=7,C=1'), '0,11', + graphics('i=7', 'OK')) + # Delete controls must not inherit an unfinished upload's image ID. + check('delete-aborts-upload', transmit + + graphics('a=t,q=2,i=7,f=32,s=1,v=1,m=1', '/wAA') + + graphics('a=d,d=I') + graphics('a=p,i=7,C=1'), '0,0', + graphics('i=7', 'OK')) + for selector, coordinates in [('C', ''), ('P', ',x=1,y=1'), + ('Q', ',x=1,y=1,z=9'), ('X', ',x=1'), + ('Y', ',y=1'), ('R', ',x=7,y=7')]: + check('delete-selector-' + selector, transmit + + graphics('a=p,q=2,i=7,C=1,z=9') + + graphics('a=d,d=%s,q=2%s' % (selector, coordinates)) + + graphics('a=p,i=7'), '0,0', graphics('i=7', 'ENOENT')) + check('delete-misses', transmit + graphics('a=p,q=2,i=7,C=1,z=9') + + graphics('a=d,d=Q,x=1,y=1,z=8,q=2') + + graphics('a=d,d=P,x=2,y=1,q=2') + + graphics('a=p,i=7,C=1'), '0,0', graphics('i=7', 'OK')) + check('delete-range-unused-source', transmit + graphics('a=d,d=R,x=7,y=7,p=99') + + graphics('a=p,i=7'), '0,0', graphics('i=7', 'ENOENT')) + white = base64.b64encode(b'\xff' * (24 * 32 * 4)).decode() + virtual = graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=24,v=32,c=3,r=2', white) + ph = '\U0010eeee' + colours = '\033[38;2;0;0;7m\033[58;2;0;0;1m' + check('multiple-virtual', virtual + graphics('a=p,q=2,U=1,i=7,p=2,c=1,r=1') + + colours + ph + '\u0305\u030e', '1,0', text=ph + '\u0305\u030e', + render=r'255;255;255m') + high_virtual = graphics('a=T,q=2,U=1,i=16777223,f=32,s=24,v=32,c=3,r=2', white) + check('inherit-high-byte', high_virtual + '\033[38;2;0;0;7m' + + ph + '\u0305\u0305\u030d' + ph, '2,0', + text=ph + '\u0305\u0305\u030d' + ph, render=r'48;2;255;255;255m {2}') + check('inherit-high-byte-row', high_virtual + '\033[38;2;0;0;7m' + + ph + '\u0305\u0305\u030d' + ph + '\u0305', '2,0', + render=r'48;2;255;255;255m {2}') + check('inherit-high-byte-column', high_virtual + '\033[38;2;0;0;7m' + + ph + '\u0305\u0305\u030d' + ph + '\u0305\u030d', '2,0', + render=r'48;2;255;255;255m {2}') + for name, change, diacritics in [ + ('colour', '\033[38;2;0;0;8m', ''), + ('placement', '\033[58;2;0;0;1m', ''), + ('row', '', '\u030d'), + ('column', '', '\u0305\u030e')]: + check('inherit-mismatch-' + name, high_virtual + '\033[38;2;0;0;7m' + + ph + '\u0305\u0305\u030d' + change + ph + diacritics, '2,0', + render=ph) + check('palette-id', virtual + '\033[38;5;7m\033[58;5;1m' + ph, '1,0', + render=r'255;255;255m') + check('negative-virtual', virtual + graphics('a=p,q=2,U=1,i=7,p=1,c=3,r=2,z=-1') + + colours + ph, '1,0', render=r'255;255;255m') + check('placeholder-resize', virtual + colours + ph, '1,0', + render=r'48;2;255;255;255m(?:\x1b\[[0-9;]+m)* \x1b\[(?:0|39)m', + resize=(50, 12)) + for selector in ['a', 'z']: + check('virtual-survives-' + selector, virtual + colours + ph + + graphics('a=d,d=%s,q=2' % selector), '1,0', render=r'255;255;255m') + # Deleting a prototype leaves its existing text-backed display intact. + check('virtual-delete-display', virtual + colours + ph + + graphics('a=d,d=I,i=7,p=1,q=2'), '1,0', render=r'255;255;255m') + check('virtual-delete-prototype', virtual + graphics('a=d,d=i,i=7,p=1,q=2') + + colours + ph, '1,0', render=ph) + check('virtual-replace-display', virtual + colours + ph + + graphics('a=p,i=7,p=1,q=2,C=1,c=3,r=2'), '1,0', + render=r'^(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m(?:\x1b\[[0-9;]+m)* ') + check('placeholder-overwrite', virtual + colours + ph + '\033[HX', '1,0', + text='X', render=r'X') + check('placeholder-erase', virtual + colours + ph + '\033[H\033[2K', '0,0', + text='', render=r'^\s*$') + white_pixel = '/////w==' + check('letterbox', graphics('a=T,q=2,f=32,s=1,v=1,c=2,r=2,C=1', white_pixel), + '0,0', render='▄▄') + offset_source = base64.b64encode(b'\xff' * (8 * 32 * 4)).decode() + check('scaled-offset', graphics('a=T,q=2,f=32,s=8,v=32,c=1,r=1,X=4,C=1', + offset_source), '0,0', render=r'12[78];12[78];12[78]m') + check('offset-clamp', graphics('a=T,q=2,f=32,s=1,v=1,X=4294967295,C=1', + white_pixel), '0,0') + check('oversized-placement', transmit + graphics('a=p,i=7,c=65534,r=32767'), + '0,0', graphics('i=7', 'EINVAL')) + large_rgb = base64.b64encode(zlib.compress(b'\0' * (4096 * 4097 * 3))).decode() + check('oversized-rgb', graphics('a=t,i=7,f=24,s=4096,v=4097,o=z', large_rgb), + '0,0', graphics('i=7', 'EINVAL')) + for action in ['f', 'a', 'c']: + check('unsupported-action-' + action, graphics('a=%s,i=7' % action), + '0,0', graphics('i=7', 'ENOTSUP')) + for key in ['P', 'Q', 'H', 'V']: + check('unsupported-key-' + key, graphics('a=p,i=7,%s=1' % key), + '0,0', graphics('i=7', 'ENOTSUP')) + check('relative-defaults', transmit + graphics('a=p,i=7,P=0,Q=0,H=0,V=0,C=1'), + '0,0', graphics('i=7', 'OK')) + check('unsupported-number', graphics('a=t,I=7,f=32,s=1,v=1', pixel), + '0,0', graphics('I=7', 'ENOTSUP')) + check('id-and-number', graphics('a=t,i=7,I=8,f=32,s=1,v=1', pixel), + '0,0', graphics('i=7,I=8', 'EINVAL')) + check('unknown-extension', graphics('a=T,q=2,f=32,s=1,v=1,c=3,r=2,k=9', pixel), + '3,2') finally: subprocess.run(tmux + ['kill-server'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(outer + ['kill-server'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) PY diff --git a/regress/image-resize-many-placements.sh b/regress/image-resize-many-placements.sh index 485ed55f7..64f5073bc 100644 --- a/regress/image-resize-many-placements.sh +++ b/regress/image-resize-many-placements.sh @@ -14,13 +14,14 @@ TMP=$(mktemp) trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15 # White placements stay behind the final red placement. +# An 8:1 source fills 16 columns by one row without letterboxing. $TMUX2 new-session -d -x 8 -y 4 " i=0 while [ \$i -lt 64 ]; do - printf '\\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=16,r=1;/////w==\\033\\\\' + printf '\\033_Ga=T,q=2,C=1,f=32,s=8,v=1,c=16,r=1;//////////////////////////////////////////8=\\033\\\\' i=\$((i + 1)) done - printf '\\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=16,r=1;/wAA/w==\\033\\\\' + printf '\\033_Ga=T,q=2,C=1,f=32,s=8,v=1,c=16,r=1;/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP8=\\033\\\\' sleep 30" || exit 1 [ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0 $TMUX2 set -g status off || exit 1 diff --git a/regress/image-support.sh b/regress/image-support.sh index 68aa44d72..47b9fe5e0 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -61,7 +61,7 @@ $TMUX send-keys -X cancel || exit 1 # Ordinary text updates the image underlay through the normal grid write path. # Deleting the image reveals the newly written text. $TMUX new-window -d " - printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,i=8;/wAA/wD/AP8AAP///////w==\033\\' + printf '\033_Ga=T,q=2,f=32,s=2,v=4,c=2,r=2,i=8;/wAA/wD/AP//AAD/AP8A/wAA////////AAD///////8=\033\\' printf '\033[HXY' printf '\033_Ga=d,d=i,q=2,i=8\033\\' sleep 10" @@ -148,11 +148,12 @@ grep -q '^#=' $TMP || exit 1 # A retained Kitty image may be placed repeatedly using source rectangles. # Crop the green and white right column from a red/green/blue/white image. +# Each colour spans two source rows to match the cell aspect ratio. $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null $TMUX2 new-session -d -x 10 -y 4 " - printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\' - printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\' + printf '\033_Ga=t,q=2,f=32,s=2,v=4,i=10;/wAA/wD/AP//AAD/AP8A/wAA////////AAD///////8=\033\\' + printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=4,c=1,r=2\033\\' sleep 10" || exit 1 $TMUX2 set -g status off || exit 1 $TMUX new-session -d -x 10 -y 4 || exit 1 @@ -169,7 +170,7 @@ $TMUX capture-pane -pS0 -E1 >$TMP || exit 1 # row off screen. $TMUX2 new-window -d " printf '\033[2;3H' - printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\' + printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=8,c=1,r=4;AAAA/wAAAP9VVVX/VVVV/6qqqv+qqqr///////////8=\033\\' sleep 10" || exit 1 $TMUX2 select-window -t:1 || exit 1 sleep 1 @@ -183,7 +184,7 @@ $TMUX capture-pane -pS0 -E3 >$TMP || exit 1 # the bottom three source rows remain visible and the cursor is on the last row. $TMUX2 new-window -d " printf '\033[3;1H' - printf '\033_Ga=T,q=2,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\' + printf '\033_Ga=T,q=2,f=32,s=1,v=8,c=1,r=4;AAAA/wAAAP9VVVX/VVVV/6qqqv+qqqr///////////8=\033\\' sleep 10" || exit 1 $TMUX2 select-window -t:2 || exit 1 sleep 1 @@ -197,7 +198,7 @@ $TMUX capture-pane -pS0 -E3 >$TMP || exit 1 # reflow. The ten-column rows are clipped to five columns, not split into four # wrapped rows. $TMUX2 new-window -d " - printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=2,c=10,r=2;/wAA//////8=\033\\' + printf '\033_Ga=T,q=2,C=1,f=32,s=5,v=2,c=10,r=2;/wAA//8AAP//AAD//wAA//8AAP///////////////////////////w==\033\\' sleep 10" || exit 1 $TMUX2 select-window -t:3 || exit 1 sleep 1 @@ -271,7 +272,7 @@ sleep 1 # the image. Deleting the placement afterwards reveals the updated text. TEXT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " printf 'test\r' - printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=2,c=2,r=2,i=14,p=7;/wAA/wD/AP8AAP///////w==\033\\' + printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=4,c=2,r=2,i=14,p=7;/wAA/wD/AP//AAD/AP8A/wAA////////AAD///////8=\033\\' printf '\r' printf 'test\n' printf 'test\n' @@ -295,8 +296,8 @@ $TMUX capture-pane -pS0 -E1 >$TMP || exit 1 # negative z-indexes cover backgrounds but not text, and very negative ones # remain below the background. Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " - printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\' - printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\' + printf '\033_Ga=t,q=2,f=32,s=1,v=2,i=21;/wAA//8AAP8=\033\\' + printf '\033_Ga=t,q=2,f=32,s=1,v=2,i=22;//////////8=\033\\' printf 'X\r' printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\' printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\' diff --git a/screen-write.c b/screen-write.c index 723d7cb72..1d5b42fa6 100644 --- a/screen-write.c +++ b/screen-write.c @@ -404,8 +404,8 @@ screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y, { image_redraw_area(ctx, x, y, sx, sy); if (ctx->flags & SCREEN_WRITE_INPUT) - image_grid_remove_sixel_spans(ctx->s->grid, x, ctx->s->grid->hsize + y, - sx, sy); + image_grid_remove_overwritten_spans(ctx->s->grid, x, + ctx->s->grid->hsize + y, sx, sy); } #endif @@ -1492,7 +1492,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) #ifdef ENABLE_IMAGES image_redraw_all(ctx); if (ctx->flags & SCREEN_WRITE_INPUT) - image_grid_remove_sixel_spans(s->grid, 0, s->grid->hsize, + image_grid_remove_overwritten_spans(s->grid, 0, s->grid->hsize, screen_size_x(s), screen_size_y(s)); #endif diff --git a/tmux.h b/tmux.h index 549a5fdf7..c9295d1d4 100644 --- a/tmux.h +++ b/tmux.h @@ -1107,18 +1107,37 @@ struct image { RB_HEAD(images, image); #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) +struct image_view { + u_int x; + u_int y; + u_int width; + u_int height; + u_int scaled_width; + u_int scaled_height; + u_int canvas_width; + u_int canvas_height; + u_int sx; + u_int sy; + u_int x_offset; + u_int y_offset; +}; + enum kitty_parse_status { KITTY_PARSE_ERROR = -1, KITTY_PARSE_OK, KITTY_PARSE_MORE, - KITTY_PARSE_MISSING + KITTY_PARSE_MISSING, + KITTY_PARSE_UNSUPPORTED }; struct kitty_parse_result { u_int image_id; + u_int image_number; u_int replace_id; u_int placement_id; u_int quiet; + u_int x; + u_int y; int32_t z; char action; char delete; @@ -4331,8 +4350,7 @@ char *regsub(const char *, const char *, const char *, int); struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, u_char *); -struct image *image_create_view(struct image *, u_int, u_int, u_int, - u_int, u_int, u_int, u_int, u_int, u_int, u_int); +struct image *image_create_view(struct image *, const struct image_view *); struct image *image_find(u_int); u_int image_get_id(const struct image *); void image_get_size(const struct image *, u_int *, u_int *); @@ -4379,10 +4397,11 @@ void image_rect_get_coords(const struct image_rect *, u_int *, u_int *, u_int *, u_int *, u_int *, u_int *); int32_t image_rect_get_z(const struct image_rect *); void image_clear(struct screen_write_ctx *, u_int); -void image_clear_kitty(struct screen_write_ctx *, char, u_int, - u_int, int32_t); - void image_grid_remove_sixel_spans(struct grid *, u_int, u_int, - u_int, u_int); +void image_clear_kitty(struct screen_write_ctx *, + const struct kitty_parse_result *); +int image_grid_has_image(struct grid *, u_int); +void image_grid_remove_overwritten_spans(struct grid *, u_int, + u_int, u_int, u_int); void image_grid_free_line(struct grid *, struct grid_line *); void image_grid_free(struct grid *); void image_grid_move_cells(struct grid *, u_int, u_int, u_int, @@ -4395,8 +4414,8 @@ void image_grid_resize_width(struct grid *, u_int); int image_grid_line_has_images(const struct grid_line *); int image_grid_area_has_images(struct grid *, u_int, u_int, u_int, u_int); -int image_grid_get_source(struct grid *, u_int, u_int, - struct image *, u_int *, u_int *); +int image_grid_get_placeholder(struct grid *, u_int, u_int, + struct kitty_placeholder *); void image_place_cell_kitty(struct screen_write_ctx *, struct image *, u_int, u_int, u_int, u_int, u_int, u_int, int32_t); /* image-kitty.c */ @@ -4405,6 +4424,9 @@ struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, int kitty_placeholder_to_image(void *, struct grid *, struct grid_cell *, u_int, u_int, struct kitty_placeholder *); +int kitty_cell_is_placeholder(const struct grid_cell *); +void kitty_delete_images(void *, struct screen_write_ctx *, + const struct kitty_parse_result *); void kitty_free_state(void *); void kitty_draw_rect(struct tty *, const struct image_rect *); diff --git a/tty-draw.c b/tty-draw.c index b63407249..00f1d3760 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -124,6 +124,7 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, struct grid_cell gc, ngc, last; #ifdef ENABLE_IMAGES struct grid_cell image_gc; + struct kitty_placeholder placeholder; int image_status; #endif struct grid_line *gl; @@ -270,6 +271,14 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, } else { /* Get the current cell. */ grid_view_get_cell(gd, px + i, py, &gc); +#ifdef ENABLE_IMAGES + if (kitty_cell_is_placeholder(&gc)) { + if (image_grid_get_placeholder(gd, + px + i, gd->hsize + py, + &placeholder)) + utf8_set(&gc.data, ' '); + } +#endif gcp = &gc; #ifdef ENABLE_IMAGES if (image_get_fallback_at(tty, s, px + i, py, &gc, diff --git a/tty.c b/tty.c index 611dfcbed..a4ac8f0f4 100644 --- a/tty.c +++ b/tty.c @@ -1935,6 +1935,22 @@ tty_cmd_alignmenttest(struct tty *tty, const struct tty_ctx *ctx) void tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) { + const struct grid_cell *cell = ctx->cell; +#ifdef ENABLE_IMAGES + struct grid_cell placeholder_cell; + struct kitty_placeholder placeholder; + + if (kitty_cell_is_placeholder(cell)) { + if (image_grid_get_placeholder(ctx->s->grid, ctx->ocx, + ctx->s->grid->hsize + ctx->ocy, &placeholder)) { + memcpy(&placeholder_cell, cell, + sizeof placeholder_cell); + utf8_set(&placeholder_cell.data, ' '); + cell = &placeholder_cell; + } + } +#endif + if (!tty_is_visible(tty, ctx, ctx->ocx, ctx->ocy, 1, 1)) return; @@ -1948,7 +1964,7 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) tty_invalidate(tty); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); - tty_cell(tty, ctx->cell, &ctx->style_ctx); + tty_cell(tty, cell, &ctx->style_ctx); if (ctx->flags & TTY_CTX_CELL_INVALIDATE) tty_invalidate(tty);