diff --git a/format.c b/format.c index c3f01f16e..746cd45b9 100644 --- a/format.c +++ b/format.c @@ -3070,26 +3070,18 @@ format_cb_version(__unused struct format_tree *ft) return (xstrdup(getversion())); } -/* Callback for sixel_support. */ -static void * -format_cb_sixel_support(__unused struct format_tree *ft) -{ -#ifdef ENABLE_SIXEL - return (xstrdup("1")); -#else - return (xstrdup("0")); -#endif -} - /* Callback for image_support. */ static void * format_cb_image_support(__unused struct format_tree *ft) { + char *value; + #ifdef ENABLE_IMAGES - return (xstrdup("1")); + value = xstrdup("1"); #else - return (xstrdup("0")); + value = xstrdup("0"); #endif + return (value); } /* Callback for active_window_index. */ @@ -4104,7 +4096,7 @@ static const struct format_table_entry format_table[] = { format_cb_session_windows }, { "sixel_support", FORMAT_TABLE_STRING, - format_cb_sixel_support + format_cb_image_support }, { "socket_path", FORMAT_TABLE_STRING, format_cb_socket_path diff --git a/grid.c b/grid.c index 4df6f477c..415d32abb 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_damage(gd, px, py, nx, ny); + image_grid_remove_sixel_spans(gd, px, py, nx, ny); #endif if (px == 0 && nx == gd->sx) { @@ -793,7 +793,9 @@ 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 - image_grid_damage(gd, 0, yy, gd->sx, 1); + /* Remove any SIXEL spans invalidated by clearing this line. */ + image_grid_remove_sixel_spans(gd, 0, yy, gd->sx, 1); + /* Preserve the remaining Kitty spans. */ images = gl->images; #endif free(gl->celldata); @@ -1242,6 +1244,7 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx, grid_get_cell(gd, xx, py, &gc); if (gc.flags & GRID_FLAG_PADDING) continue; + if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) { grid_string_cells_code(*lastgc, &gc, code, sizeof code, flags, s, &has_link); diff --git a/image-fallback.c b/image-fallback.c index 6ff7a5011..5292b3959 100644 --- a/image-fallback.c +++ b/image-fallback.c @@ -216,6 +216,7 @@ image_glyph_block_key(enum image_glyph_detail detail, u_int mask) TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT, TTY_ACS_IMAGE_BLOCK }; + u_char key; if (detail == IMAGE_GLYPH_HALF) return (half[mask]); @@ -229,7 +230,8 @@ image_glyph_block_key(enum image_glyph_detail detail, u_int mask) return (TTY_ACS_IMAGE_HALF_RIGHT); if (mask == 63) return (TTY_ACS_IMAGE_BLOCK); - return (tty_acs_image_sextant(mask)); + key = tty_acs_image_sextant(mask); + return (key); } static u_char * @@ -243,9 +245,8 @@ image_glyph_make_shades(struct image *im, u_int levels) int reverse; u_char *result; - if (data == NULL) { + if (data == NULL) data = im->fallback_data = xcalloc(1, sizeof *data); - } result = (levels == 5 ? data->shade5 : data->shade8); if (result != NULL) return (result); @@ -354,8 +355,7 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, u_char key; columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2); - rows = (detail == IMAGE_GLYPH_HALF ? 2 : - detail == IMAGE_GLYPH_QUADRANT ? 2 : 3); + rows = (detail == IMAGE_GLYPH_SEXTANT ? 3 : 2); i = 0; for (sy = 0; sy < rows; sy++) { for (sx = 0; sx < columns; sx++) { @@ -399,8 +399,7 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, void image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, - const struct grid_cell *gc, struct grid_cell *out, - __unused const struct tty_style_ctx *style_ctx) + const struct grid_cell *gc, struct grid_cell *out) { static const char ascii[] = " .:-=+*#%@"; static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT, diff --git a/image-kitty.c b/image-kitty.c index 5e1853b7a..0ab842900 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -25,6 +25,8 @@ #include "tmux.h" +#define KITTY_CHUNK_SIZE 3072 + static const uint32_t kitty_diacritics[] = { 0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F, 0x0346, 0x034A, 0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357, @@ -398,9 +400,9 @@ kitty_place(struct tty *tty, struct kitty_image_cache *entry, entry->placements = placement; tty_cursor(tty, destination_x, destination_y); xsnprintf(control, sizeof control, - "\033_Ga=p,i=%u,p=%llu,x=%u,y=%u,w=%u,h=%u,c=%u,r=%u,z=%d," + "\033_Ga=p,i=%u,p=%u,x=%u,y=%u,w=%u,h=%u,c=%u,r=%u,z=%d," "C=1,q=2\033\\", entry->kitty_id, - (unsigned long long)placement->id, px, py, pwidth, pheight, width, + placement->id, px, py, pwidth, pheight, width, height, z); tty_puts(tty, control); } @@ -412,16 +414,23 @@ kitty_upload(struct tty *tty, struct image *im) struct kitty_output *ko = kitty_get_output(tty); struct kitty_image_cache *entry; char control[128], encoded[4097]; - u_char raw[3072]; const u_char *pixels; u_char *padded; - size_t offset, size, copied, row, column; - size_t available, stride, image_size; + size_t offset, size, row, stride, image_size; int encodedlen; u_int id, width, height; u_int canvas_width, canvas_height; u_int upload_width, upload_height; + image_get_size(im, &width, &height); + image_get_canvas_size(im, &canvas_width, &canvas_height); + if (canvas_width > UINT_MAX - 2 || canvas_height > UINT_MAX - 2) + return (NULL); + upload_width = canvas_width + 2; + upload_height = canvas_height + 2; + if ((uint64_t)upload_width * upload_height * 4 > IMAGE_SIZE_LIMIT) + return (NULL); + for (entry = ko->images; entry != NULL; entry = entry->next) { if (entry->server_id != image_get_id(im)) continue; @@ -448,14 +457,6 @@ kitty_upload(struct tty *tty, struct image *im) entry->next_placement = 0; pixels = image_get_pixels(im, &stride, &image_size); - image_get_size(im, &width, &height); - image_get_canvas_size(im, &canvas_width, &canvas_height); - if (canvas_width > UINT_MAX - 2 || canvas_height > UINT_MAX - 2) - return (NULL); - upload_width = canvas_width + 2; - upload_height = canvas_height + 2; - if ((uint64_t)upload_width * upload_height * 4 > IMAGE_SIZE_LIMIT) - return (NULL); /* * Pad the upload with duplicate edge pixels. Kitty linearly filters scaled * textures against transparent border pixels, which otherwise darkens the @@ -481,21 +482,11 @@ kitty_upload(struct tty *tty, struct image *im) width = upload_width; height = upload_height; image_size = (size_t)width * height * 4; - stride = (size_t)width * 4; for (offset = 0; offset < image_size; offset += size) { size = image_size - offset; - if (size > sizeof raw) - size = sizeof raw; - for (copied = 0; copied < size; copied += available) { - row = (offset + copied) / ((size_t)width * 4); - column = (offset + copied) % ((size_t)width * 4); - available = (size_t)width * 4 - column; - if (available > size - copied) - available = size - copied; - memcpy(raw + copied, pixels + row * stride + - column, available); - } - encodedlen = b64_ntop(raw, size, encoded, + if (size > KITTY_CHUNK_SIZE) + size = KITTY_CHUNK_SIZE; + encodedlen = b64_ntop(pixels + offset, size, encoded, sizeof encoded); if (encodedlen < 0) { free(padded); @@ -519,8 +510,7 @@ kitty_upload(struct tty *tty, struct image *im) /* Draw an image rectangle using the Kitty graphics protocol. */ void -kitty_draw_rect(struct tty *tty, const struct image_rect *rectangle, - __unused const struct tty_style_ctx *style_ctx) +kitty_draw_rect(struct tty *tty, const struct image_rect *rectangle) { struct kitty_image_cache *entry; struct image *im; @@ -1059,8 +1049,7 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, */ struct image * kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, - u_int ypixel, u_int *image_id, u_int *replace_id, u_int *quiet, - char *action, char *delete, u_int *placement_id, int32_t *z, int *status) + u_int ypixel, struct kitty_parse_result *result) { struct kitty_context *kc = *state; struct kitty_state *ks; @@ -1078,14 +1067,8 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, kc = xcalloc(1, sizeof *kc); *state = kc; } - *image_id = 0; - *replace_id = 0; - *quiet = 0; - *action = '\0'; - *delete = '\0'; - *placement_id = 0; - *z = 0; - *status = KITTY_PARSE_ERROR; + memset(result, 0, sizeof *result); + result->status = KITTY_PARSE_ERROR; ks = kc->transfer; semi = memchr(buf, ';', len); controllen = (semi == NULL ? len : (size_t)(semi - buf)); @@ -1098,34 +1081,34 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, ks->medium = 'd'; } ks->more = 0; - *image_id = ks->image_id; - *quiet = ks->quiet; - *action = ks->action; - *delete = ks->delete; - *placement_id = ks->placement_id; - *z = ks->z; + result->image_id = ks->image_id; + result->quiet = ks->quiet; + result->action = ks->action; + result->delete = ks->delete; + result->placement_id = ks->placement_id; + result->z = ks->z; if (kitty_control(ks, buf, controllen) != 0 || ks->medium != 'd' || (payloadlen != 0 && kitty_append(ks, semi + 1, payloadlen) != 0)) goto fail; - *image_id = ks->image_id; - *quiet = ks->quiet; - *action = ks->action; - *delete = ks->delete; - *placement_id = ks->placement_id; - *z = ks->z; + result->image_id = ks->image_id; + result->quiet = ks->quiet; + result->action = ks->action; + result->delete = ks->delete; + result->placement_id = ks->placement_id; + result->z = ks->z; if (ks->more) { kc->transfer = ks; - *status = KITTY_PARSE_MORE; + result->status = KITTY_PARSE_MORE; return (NULL); } kc->transfer = NULL; if (ks->action == 'p') { source = kitty_source_get(kc, ks->image_id); if (source == NULL) { - *status = KITTY_PARSE_MISSING; + result->status = KITTY_PARSE_MISSING; im = NULL; } else if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); @@ -1134,16 +1117,16 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, kitty_virtual_set(kc, ks->image_id, im); image_free(image_get_id(im)); im = NULL; - *action = 'u'; - *status = KITTY_PARSE_OK; + result->action = 'u'; + result->status = KITTY_PARSE_OK; } } else { im = kitty_place_image(source, ks, xpixel, ypixel); image_free(image_get_id(source)); if (im != NULL) { - *replace_id = kitty_placement_set(kc, ks->image_id, + result->replace_id = kitty_placement_set(kc, ks->image_id, ks->placement_id, ks->z, im); - *status = KITTY_PARSE_OK; + result->status = KITTY_PARSE_OK; } } kitty_state_free(ks); @@ -1175,9 +1158,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, goto fail; if ((ks->delete == 'i' || ks->delete == 'I') && im == NULL) - *status = KITTY_PARSE_MISSING; + result->status = KITTY_PARSE_MISSING; else - *status = KITTY_PARSE_OK; + result->status = KITTY_PARSE_OK; kitty_state_free(ks); return (im); } @@ -1239,25 +1222,25 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, if (source == NULL) free(pixels); else { - *status = KITTY_PARSE_OK; + result->status = KITTY_PARSE_OK; if (ks->action != 'q') - *replace_id = kitty_source_set(kc, ks->image_id, source); + result->replace_id = kitty_source_set(kc, ks->image_id, source); if (ks->action == 'T' && !ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) - *status = KITTY_PARSE_ERROR; + 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 if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) - *status = KITTY_PARSE_ERROR; + result->status = KITTY_PARSE_ERROR; else { kitty_virtual_set(kc, ks->image_id, im); image_free(image_get_id(im)); im = NULL; - *action = 'u'; + result->action = 'u'; } } else { im = NULL; @@ -1330,8 +1313,7 @@ kitty_placeholder_index(uint32_t value, u_int *index) /* 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, - u_int grid_x, u_int grid_y, struct image **image, u_int *source_x, - u_int *source_y, u_int *image_id, u_int *placement_id, int32_t *z) + u_int grid_x, u_int grid_y, struct kitty_placeholder *placeholder) { struct kitty_context *kc = state; struct kitty_source *source; @@ -1392,15 +1374,15 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, if (x >= sx || y >= sy) return (0); - *image = im; - *source_x = x; - *source_y = y; - *image_id = id; + placeholder->image = im; + placeholder->source_x = x; + placeholder->source_y = y; + placeholder->image_id = id; if (gc->us & COLOUR_FLAG_RGB) - *placement_id = gc->us & 0xffffff; + placeholder->placement_id = gc->us & 0xffffff; else - *placement_id = 0; - *z = 0; + placeholder->placement_id = 0; + placeholder->z = 0; utf8_set(&gc->data, ' '); return (1); } diff --git a/image-sixel.c b/image-sixel.c index e37153566..1d814015c 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -524,7 +524,6 @@ sixel_size_in_cells(struct sixel_image *si, u_int *x, u_int *y) image_size_in_cells(si->sx, si->sy, si->cell_w, si->cell_h, x, y); } -#ifdef ENABLE_IMAGES /* Convert one HLS component to RGB. */ static double sixel_hue(double p, double q, double t) @@ -619,7 +618,6 @@ sixel_to_image(struct sixel_image *si) image_set_sixel(im, si); return (im); } -#endif /* Scale or crop an indexed SIXEL image. */ struct sixel_image * @@ -629,7 +627,8 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, struct sixel_image *new; u_int cx, cy, raster_sx, raster_sy; u_int pox, poy, psx, psy, tsx, tsy, px, py; - uint64_t x0, x1, y0, y1, tx0, tx1, ty0, ty1; + uint64_t source_left, source_right, source_top, source_bottom; + uint64_t target_left, target_right, target_top, target_bottom; u_int x, y, i; /* @@ -663,42 +662,42 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, * only the final partial cell to the raster. Dividing the raster evenly * between cells would stretch every complete cell and squash the last. */ - x0 = (uint64_t)ox * si->cell_w; - x1 = (uint64_t)(ox + sx) * si->cell_w; - y0 = (uint64_t)oy * si->cell_h; - y1 = (uint64_t)(oy + sy) * si->cell_h; - if (x1 > raster_sx) - x1 = raster_sx; - if (y1 > raster_sy) - y1 = raster_sy; - if (x1 <= x0 || y1 <= y0) + source_left = (uint64_t)ox * si->cell_w; + source_right = (uint64_t)(ox + sx) * si->cell_w; + source_top = (uint64_t)oy * si->cell_h; + source_bottom = (uint64_t)(oy + sy) * si->cell_h; + if (source_right > raster_sx) + source_right = raster_sx; + if (source_bottom > raster_sy) + source_bottom = raster_sy; + if (source_right <= source_left || source_bottom <= source_top) return (NULL); - pox = x0; - poy = y0; - psx = x1 - x0; - psy = y1 - y0; + pox = source_left; + poy = source_top; + psx = source_right - source_left; + psy = source_bottom - source_top; /* * Preserve any partial final source cell. The grid still covers whole * cells, but the SIXEL raster must end at the corresponding pixel offset * rather than stretching to the cell boundary. */ - tx1 = ((uint64_t)raster_sx * cell_w + si->cell_w - 1) / + target_right = ((uint64_t)raster_sx * cell_w + si->cell_w - 1) / si->cell_w; - ty1 = ((uint64_t)raster_sy * cell_h + si->cell_h - 1) / + target_bottom = ((uint64_t)raster_sy * cell_h + si->cell_h - 1) / si->cell_h; - if (tx1 > UINT_MAX || ty1 > UINT_MAX) + if (target_right > UINT_MAX || target_bottom > UINT_MAX) return (NULL); - tx0 = (uint64_t)ox * cell_w; - ty0 = (uint64_t)oy * cell_h; - if (tx0 >= tx1 || ty0 >= ty1) + target_left = (uint64_t)ox * cell_w; + target_top = (uint64_t)oy * cell_h; + if (target_left >= target_right || target_top >= target_bottom) return (NULL); - if ((uint64_t)(ox + sx) * cell_w < tx1) - tx1 = (uint64_t)(ox + sx) * cell_w; - if ((uint64_t)(oy + sy) * cell_h < ty1) - ty1 = (uint64_t)(oy + sy) * cell_h; - tsx = tx1 - tx0; - tsy = ty1 - ty0; + if ((uint64_t)(ox + sx) * cell_w < target_right) + target_right = (uint64_t)(ox + sx) * cell_w; + if ((uint64_t)(oy + sy) * cell_h < target_bottom) + target_bottom = (uint64_t)(oy + sy) * cell_h; + tsx = target_right - target_left; + tsy = target_bottom - target_top; if (tsx == 0 || tsy == 0) return (NULL); @@ -1214,15 +1213,14 @@ sixel_clamp_colour(int colour) /* Return a source pixel mapped to an output SIXEL pixel. */ static const u_char * -sixel_from_image_pixel(const struct sixel_source *source, u_int sourcex0, - u_int sourcey0, - u_int sourcewidth, u_int sourceheight, u_int sx, u_int sy, u_int x, - u_int y) +sixel_from_image_pixel(const struct sixel_source *source, u_int source_x, + u_int source_y, u_int source_width, u_int source_height, u_int output_sx, + u_int output_sy, u_int x, u_int y) { u_int sourcex, sourcey; - sourcex = sourcex0 + (uint64_t)x * sourcewidth / sx; - sourcey = sourcey0 + (uint64_t)y * sourceheight / sy; + sourcex = source_x + (uint64_t)x * source_width / output_sx; + sourcey = source_y + (uint64_t)y * source_height / output_sy; if (sourcex >= source->width) sourcex = source->width - 1; if (sourcey >= source->height) @@ -1232,7 +1230,7 @@ sixel_from_image_pixel(const struct sixel_source *source, u_int sourcex0, /* Render an image rectangle as an indexed SIXEL image. */ static struct sixel_image * -sixel_from_image(struct image *im, u_int ox, u_int oy, u_int cells_x, +sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, u_int cells_y, u_int cell_w, u_int cell_h) { struct sixel_image *si; @@ -1244,10 +1242,11 @@ sixel_from_image(struct image *im, u_int ox, u_int oy, u_int cells_x, int *current, *next, *tmp; int red_error, green_error, blue_error, alpha_error; u_int x, y, sx, sy, index, error_index; - u_int sourcex0, sourcey0, sourcewidth, sourceheight; + u_int source_x, source_y, source_width, source_height; u_int red, green, blue, alpha, colour, i, ncolours; uint64_t destination_width, destination_height; - uint64_t content_width, content_height, x0, x1, y0, y1; + uint64_t content_width, content_height; + uint64_t left, right, top, bottom; /* Work out the requested cell crop in destination pixel coordinates. */ source.pixels = image_get_pixels(im, &source.stride, NULL); @@ -1265,36 +1264,36 @@ sixel_from_image(struct image *im, u_int ox, u_int oy, u_int cells_x, source.canvas_height - 1) / source.canvas_height; /* Convert the requested cell rectangle to clipped output pixel bounds. */ - x0 = (uint64_t)ox * cell_w; - y0 = (uint64_t)oy * cell_h; - x1 = ((uint64_t)ox + cells_x) * cell_w; - y1 = ((uint64_t)oy + cells_y) * cell_h; - if (x1 > content_width) - x1 = content_width; - if (y1 > content_height) - y1 = content_height; - if (x1 <= x0 || y1 <= y0) + left = (uint64_t)cell_x * cell_w; + top = (uint64_t)cell_y * cell_h; + right = ((uint64_t)cell_x + cells_x) * cell_w; + bottom = ((uint64_t)cell_y + cells_y) * cell_h; + if (right > content_width) + right = content_width; + if (bottom > content_height) + bottom = content_height; + if (right <= left || bottom <= top) return (NULL); /* The clipped output bounds determine the SIXEL image dimensions. */ - sx = x1 - x0; - sy = y1 - y0; + sx = right - left; + sy = bottom - top; if (sx == 0 || sy == 0 || sx > SIXEL_WIDTH_LIMIT || sy > SIXEL_HEIGHT_LIMIT) return (NULL); /* Map the requested cell crop to the source image's pixel rectangle. */ - image_get_pixel_rect(im, ox, oy, cells_x, cells_y, &sourcex0, - &sourcey0, &sourcewidth, &sourceheight); - if (sourcewidth == 0 || sourceheight == 0) + image_get_pixel_rect(im, cell_x, cell_y, cells_x, cells_y, &source_x, + &source_y, &source_width, &source_height); + if (source_width == 0 || source_height == 0) return (NULL); /* Build an adaptive palette from the visible nontransparent pixels. */ hg = xcalloc(SIXEL_HISTOGRAM_SIZE, sizeof *hg); for (y = 0; y < sy; y++) { for (x = 0; x < sx; x++) { - pixel = sixel_from_image_pixel(&source, sourcex0, sourcey0, - sourcewidth, sourceheight, sx, sy, x, y); + pixel = sixel_from_image_pixel(&source, source_x, source_y, + source_width, source_height, sx, sy, x, y); if (pixel[3] == 0) continue; @@ -1338,8 +1337,8 @@ sixel_from_image(struct image *im, u_int ox, u_int oy, u_int cells_x, next = xcalloc(((size_t)sx + 2) * 4, sizeof *next); for (y = 0; y < sy; y++) { for (x = 0; x < sx; x++) { - pixel = sixel_from_image_pixel(&source, sourcex0, sourcey0, - sourcewidth, sourceheight, sx, sy, x, y); + pixel = sixel_from_image_pixel(&source, source_x, source_y, + source_width, source_height, sx, sy, x, y); error_index = (x + 1) * 4; /* SIXEL pixels are binary, so dither alpha separately. */ alpha = sixel_clamp_colour((int)pixel[3] + @@ -1488,15 +1487,17 @@ sixel_free_output(struct tty *tty, __unused int send) static struct sixel_image * sixel_render_image(struct image *im, u_int cell_w, u_int cell_h) { - struct sixel_image *original; + struct sixel_image *original, *si; u_int sx, sy; image_get_size_in_cells(im, &sx, &sy); /* Preserve SIXEL's original palette and indexed pixels when possible. */ original = image_get_sixel(im); if (original != NULL) - return (sixel_fit(original, cell_w, cell_h, sx, sy)); - return (sixel_from_image(im, 0, 0, sx, sy, cell_w, cell_h)); + si = sixel_fit(original, cell_w, cell_h, sx, sy); + else + si = sixel_from_image(im, 0, 0, sx, sy, cell_w, cell_h); + return (si); } /* Return a rendered image from the SIXEL output cache. */ @@ -1613,8 +1614,7 @@ sixel_flush_output(struct tty *tty) * other terminal output is reordered across a pending run. */ void -sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, - __unused const struct tty_style_ctx *style_ctx) +sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle) { struct sixel_output *so = sixel_get_output(tty); struct sixel_pending *sp = &so->pending; @@ -1659,38 +1659,3 @@ sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) tty_repeat_space(tty, sx); } } - -/* Convert a SIXEL image to a fallback screen. */ -struct screen * -sixel_to_screen(struct sixel_image *si) -{ - struct screen *s; - struct screen_write_ctx ctx; - struct grid_cell gc; - u_int x, y, sx, sy; - - sixel_size_in_cells(si, &sx, &sy); - - s = xmalloc(sizeof *s); - screen_init(s, sx, sy, 0); - - memcpy(&gc, &grid_default_cell, sizeof gc); - gc.attr |= (GRID_ATTR_CHARSET|GRID_ATTR_DIM); - utf8_set(&gc.data, '~'); - - screen_write_start(&ctx, s); - if (sx == 1 || sy == 1) { - for (y = 0; y < sy; y++) { - for (x = 0; x < sx; x++) - grid_view_set_cell(s->grid, x, y, &gc); - } - } else { - screen_write_box(&ctx, sx, sy, BOX_LINES_DEFAULT, NULL, NULL); - for (y = 1; y < sy - 1; y++) { - for (x = 1; x < sx - 1; x++) - grid_view_set_cell(s->grid, x, y, &gc); - } - } - screen_write_stop(&ctx); - return (s); -} diff --git a/image.c b/image.c index 92f52ecf8..6330989e2 100644 --- a/image.c +++ b/image.c @@ -28,12 +28,12 @@ #include "tmux.h" #define IMAGE_FLAG_NO_CURSOR 0x1 +#define IMAGE_FLAG_OWN_PIXELS 0x2 #define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2) /* A cell-aligned part of an image to draw at a terminal position. */ struct image_rect { struct image *image; - struct grid_cell cell; int32_t z; u_int source_x; u_int source_y; @@ -61,14 +61,17 @@ struct image_line { struct image_spans spans; }; -#define IMAGE_INPUT_SIXEL 0 -#define IMAGE_INPUT_KITTY 1 +enum image_input { + IMAGE_INPUT_ALL = -1, + IMAGE_INPUT_SIXEL, + IMAGE_INPUT_KITTY +}; /* One logical image placement, shared by all of its row spans. */ struct image_placement { struct image_store *store; struct image *image; - u_int input; + enum image_input input; u_int app_image_id; u_int app_placement_id; int32_t z; @@ -80,19 +83,36 @@ TAILQ_HEAD(image_placements, image_placement); /* Placements belonging to one grid. */ struct image_store { - struct grid *grid; uint64_t next_serial; struct image_placements placements; }; +struct image_move { + struct image_placement *placement; + u_int x; + u_int sx; + u_int source_x; + u_int source_y; +}; + +struct image_placement_map { + struct image_placement *source; + struct image_placement *destination; +}; + +struct image_copy_ctx { + struct grid *destination; + struct image_placement_map *maps; + size_t count; +}; + static struct images images = RB_INITIALIZER(&images); static u_int image_next_id; struct image_backend { const char *name; int flags; - void (*draw_rect)(struct tty *, - const struct image_rect *, const struct tty_style_ctx *); + void (*draw_rect)(struct tty *, const struct image_rect *); void (*free)(struct tty *, int); }; @@ -121,11 +141,7 @@ image_tty_find_backend(struct tty *tty) return (&image_backend_fallback); } -/* - * Update a terminal's image backend after its capabilities change. Returns 1 - * if the backend changed (so the caller knows a redraw is actually needed), - * 0 if not. - */ +/* Update the image backend and return whether it changed. */ int image_tty_update(struct tty *tty) { @@ -144,7 +160,7 @@ image_tty_update(struct tty *tty) return (1); } -/* Remove Kitty placements which will be replaced by a redraw. */ +/* Prepare the image backend to replace a redraw area. */ void image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, u_int height) @@ -156,13 +172,7 @@ image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, sixel_redraw_start(tty, x, y, width, height); } -/* - * Delete placements marked stale by image_redraw_start() - called once any - * replacement placements a redraw is making have already been created. Only - * meaningful for Kitty (see kitty_redraw_finish()) - SIXEL has no separate - * placement/data distinction for sixel_redraw_start()'s plain erase to - * leave dangling. - */ +/* Delete stale Kitty placements after their replacements have been drawn. */ void image_redraw_finish(struct tty *tty) { @@ -188,16 +198,7 @@ image_backend_flags(struct tty *tty) image_tty_update(tty); flags = tty->image_backend->flags; - /* - * There is no way to ask a terminal whether it moves SIXEL or Kitty - * image content along with the rest of a scrolling region, and it - * does not correlate with DECSLRM/margins support (confirmed by - * direct testing that mintty scrolls text within a margin-bounded - * region correctly but drops sixel content placed there, while - * WezTerm and Windows Terminal move it correctly) - so this is - * granted per terminal via the imagescroll terminal-feature, not - * assumed. - */ + /* Image scrolling is granted separately from text margin support. */ if ((tty->image_backend == &image_backend_sixel || tty->image_backend == &image_backend_kitty) && (tty->term->flags & TERM_IMAGESCROLL)) @@ -277,7 +278,6 @@ image_store_get(struct grid *gd) if (store == NULL) { store = xcalloc(1, sizeof *store); - store->grid = gd; TAILQ_INIT(&store->placements); gd->images = store; } @@ -300,7 +300,7 @@ image_line_get(struct grid_line *gl) /* Create a logical image placement. */ static struct image_placement * -image_placement_create(struct grid *gd, struct image *im, u_int input, +image_placement_create(struct grid *gd, struct image *im, enum image_input input, u_int app_image_id, u_int app_placement_id, int32_t z) { struct image_store *store = image_store_get(gd); @@ -373,11 +373,11 @@ image_span_free(struct image_span *span) /* * Remove a range from selected spans on a line - those of one placement if - * only is not NULL, otherwise those of the given input type (or all of them - * if that is -1). + * only is not NULL, otherwise those of the given input type. */ static void -image_line_remove(struct image_line *line, u_int x, u_int width, int input, +image_line_remove(struct image_line *line, u_int x, u_int width, + enum image_input input, struct image_placement *only) { struct image_span *span, *next; @@ -391,7 +391,7 @@ image_line_remove(struct image_line *line, u_int x, u_int width, int input, TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { if (only != NULL && span->placement != only) continue; - if (input != -1 && span->placement->input != (u_int)input) + if (input != IMAGE_INPUT_ALL && span->placement->input != input) continue; span_end = span->x + span->sx; if (span_end <= x || span->x >= end) @@ -431,9 +431,9 @@ image_store_prune(struct image_store *store) } } -/* Remove temporal image data overwritten by text. */ +/* Remove SIXEL spans overwritten by text. */ void -image_grid_damage(struct grid *gd, u_int x, u_int y, u_int width, +image_grid_remove_sixel_spans(struct grid *gd, u_int x, u_int y, u_int width, u_int height) { u_int row; @@ -486,11 +486,8 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, { struct image_line *line; struct image_span *span; - struct image_move { - struct image_placement *placement; - u_int x, sx, source_x, source_y; - } *moves = NULL; - size_t count = 0; + struct image_move *moves = NULL; + size_t count = 0, i; u_int start, end, span_end; if (gd->images == NULL || nx == 0 || px == dx || @@ -515,28 +512,47 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, moves[count].source_y = span->source_y; count++; } - image_line_remove(line, px, nx, -1, NULL); - image_line_remove(line, dx, nx, -1, NULL); - for (size_t i = 0; i < count; i++) + image_line_remove(line, px, nx, IMAGE_INPUT_ALL, NULL); + image_line_remove(line, dx, nx, IMAGE_INPUT_ALL, NULL); + for (i = 0; i < count; i++) image_span_add(line, moves[i].placement, moves[i].x, moves[i].sx, moves[i].source_x, moves[i].source_y); free(moves); image_store_prune(gd->images); } +/* Find or create the destination placement for a copied source placement. */ +static struct image_placement * +image_copy_placement(struct image_copy_ctx *ctx, + struct image_placement *source) +{ + struct image_placement *placement; + size_t i; + + for (i = 0; i < ctx->count; i++) { + if (ctx->maps[i].source == source) + return (ctx->maps[i].destination); + } + placement = image_placement_create(ctx->destination, source->image, + source->input, source->app_image_id, source->app_placement_id, + source->z); + ctx->maps = xreallocarray(ctx->maps, ctx->count + 1, + sizeof *ctx->maps); + ctx->maps[ctx->count].source = source; + ctx->maps[ctx->count].destination = placement; + ctx->count++; + return (placement); +} + /* Duplicate image spans alongside a group of grid lines. */ void image_grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, u_int ny) { - struct image_map { - struct image_placement *source; - struct image_placement *destination; - } *maps = NULL; + struct image_copy_ctx ctx = { .destination = dst }; struct image_placement *placement; struct image_line *source_line, *destination_line; struct image_span *span; - size_t count = 0, i; u_int row; for (row = 0; row < ny; row++) { @@ -545,29 +561,12 @@ image_grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, continue; destination_line = image_line_get(&dst->linedata[dy + row]); TAILQ_FOREACH(span, &source_line->spans, line_entry) { - placement = NULL; - for (i = 0; i < count; i++) { - if (maps[i].source == span->placement) { - placement = maps[i].destination; - break; - } - } - if (placement == NULL) { - placement = image_placement_create(dst, - span->placement->image, span->placement->input, - span->placement->app_image_id, - span->placement->app_placement_id, - span->placement->z); - maps = xreallocarray(maps, count + 1, sizeof *maps); - maps[count].source = span->placement; - maps[count].destination = placement; - count++; - } + placement = image_copy_placement(&ctx, span->placement); image_span_add(destination_line, placement, span->x, span->sx, span->source_x, span->source_y); } } - free(maps); + free(ctx.maps); } /* Copy clipped image spans between grid areas. */ @@ -576,14 +575,10 @@ image_grid_copy_area(struct grid *dst, u_int destination_x, u_int destination_y, struct grid *src, u_int source_x, u_int source_y, u_int sx, u_int sy) { - struct image_map { - struct image_placement *source; - struct image_placement *destination; - } *maps = NULL; + struct image_copy_ctx ctx = { .destination = dst }; struct image_placement *placement; struct image_line *source_line, *destination_line; struct image_span *span; - size_t count = 0, i; u_int row, start, end, span_end; if (dst == src || sx == 0 || sy == 0) @@ -613,31 +608,13 @@ image_grid_copy_area(struct grid *dst, u_int destination_x, if (span_end > end) span_end = end; - placement = NULL; - for (i = 0; i < count; i++) { - if (maps[i].source == span->placement) { - placement = maps[i].destination; - break; - } - } - if (placement == NULL) { - placement = image_placement_create(dst, - span->placement->image, span->placement->input, - span->placement->app_image_id, - span->placement->app_placement_id, - span->placement->z); - maps = xreallocarray(maps, count + 1, - sizeof *maps); - maps[count].source = span->placement; - maps[count].destination = placement; - count++; - } + placement = image_copy_placement(&ctx, span->placement); image_span_add(destination_line, placement, destination_x + start - source_x, span_end - start, span->source_x + start - span->x, span->source_y); } } - free(maps); + free(ctx.maps); } /* Return whether a grid line contains any image spans. */ @@ -649,7 +626,7 @@ image_grid_line_has_images(const struct grid_line *gl) /* Return whether a grid rectangle contains any image spans. */ int -image_grid_check_area(struct grid *gd, u_int x, u_int y, u_int width, +image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int width, u_int height) { struct image_line *line; @@ -820,10 +797,11 @@ image_make_cells(struct image *im) struct image * image_find(u_int id) { - struct image find; + struct image find, *im; find.id = id; - return (RB_FIND(images, &images, &find)); + im = RB_FIND(images, &images, &find); + return (im); } /* Return an image's server ID. */ @@ -903,13 +881,6 @@ image_rect_get_image(const struct image_rect *rectangle) return (rectangle->image); } -/* Return the source grid cell for a drawing rectangle. */ -const struct grid_cell * -image_rect_get_cell(const struct image_rect *rectangle) -{ - return (&rectangle->cell); -} - /* Return the source and destination coordinates of a drawing rectangle. */ void image_rect_get_coords(const struct image_rect *rectangle, @@ -933,7 +904,7 @@ image_rect_get_z(const struct image_rect *rectangle) /* Create and register an immutable image. */ static struct image * -image_create1(u_int width, u_int height, u_int canvas_width, +image_alloc(u_int width, u_int height, u_int canvas_width, u_int canvas_height, u_int sx, u_int sy, size_t stride, u_char *pixels) { struct image *im; @@ -979,8 +950,9 @@ 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); - im = image_create1(width, height, canvas_width, canvas_height, sx, sy, + im = image_alloc(width, height, canvas_width, canvas_height, sx, sy, (size_t)width * 4, pixels); + im->flags |= IMAGE_FLAG_OWN_PIXELS; return (im); } @@ -1004,7 +976,7 @@ image_create_view(struct image *source, u_int x, u_int y, u_int width, return (NULL); if (x_offset == 0 && y_offset == 0) { - im = image_create1(width, height, canvas_width, canvas_height, + im = image_alloc(width, height, canvas_width, canvas_height, sx, sy, source->stride, source->pixels + (size_t)y * source->stride + (size_t)x * 4); } else { @@ -1022,11 +994,10 @@ image_create_view(struct image *source, u_int x, u_int y, u_int width, source->pixels + (size_t)(y + yy) * source->stride + (size_t)x * 4, (size_t)width * 4); } - im = image_create1(padded_width, padded_height, canvas_width, + im = image_alloc(padded_width, padded_height, canvas_width, canvas_height, sx, sy, (size_t)padded_width * 4, pixels); + im->flags |= IMAGE_FLAG_OWN_PIXELS; } - if (im == NULL) - return (NULL); im->parent_id = source->id; im->source_id = source->source_id; image_ref(source->id); @@ -1059,12 +1030,13 @@ image_free(u_int id) log_debug("%s: freeing image %u", __func__, id); RB_REMOVE(images, &images, im); - if (im->parent_id == 0) + if (im->flags & IMAGE_FLAG_OWN_PIXELS) free(im->pixels); - else + if (im->parent_id != 0) image_free(im->parent_id); if (im->sixel != NULL) sixel_free(im->sixel); + image_free_fallback(im); free(im->cells); free(im); } @@ -1083,8 +1055,7 @@ image_get_cell(struct image *im, u_int x, u_int y) /* Return one for a fallback cell, minus one to continue along an image line. */ int image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, - const struct grid_cell *gc, struct grid_cell *out, - const struct tty_style_ctx *style_ctx) + const struct grid_cell *gc, struct grid_cell *out) { struct image_line *line; struct image_span *span, *found = NULL; @@ -1111,8 +1082,7 @@ image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, return (-1); } image_get_fallback_cell(tty, placement->image, - found->source_x + x - found->x, found->source_y, gc, out, - style_ctx); + found->source_x + x - found->x, found->source_y, gc, out); return (1); } @@ -1122,7 +1092,7 @@ image_get_pixel_rect(const struct image *im, u_int x, u_int y, u_int width, u_int height, u_int *px, u_int *py, u_int *pwidth, u_int *pheight) { - u_int x1, y1; + u_int left, top, right, bottom; *px = *py = *pwidth = *pheight = 0; if (im == NULL || x >= im->sx || y >= im->sy || width == 0 || @@ -1133,26 +1103,28 @@ image_get_pixel_rect(const struct image *im, u_int x, u_int y, if (height > im->sy - y) height = im->sy - y; - *px = (uint64_t)x * im->canvas_width / im->sx; - *py = (uint64_t)y * im->canvas_height / im->sy; - x1 = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) / + left = (uint64_t)x * im->canvas_width / im->sx; + top = (uint64_t)y * im->canvas_height / im->sy; + right = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) / im->sx; - y1 = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) / + bottom = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) / im->sy; - if (*px >= im->width || *py >= im->height) { + if (left >= im->width || top >= im->height) { *px = *py = 0; return; } - if (x1 <= *px) - x1 = *px + 1; - if (y1 <= *py) - y1 = *py + 1; - if (x1 > im->width) - x1 = im->width; - if (y1 > im->height) - y1 = im->height; - *pwidth = x1 - *px; - *pheight = y1 - *py; + if (right <= left) + right = left + 1; + if (bottom <= top) + bottom = top + 1; + if (right > im->width) + right = im->width; + if (bottom > im->height) + bottom = im->height; + *px = left; + *py = top; + *pwidth = right - left; + *pheight = bottom - top; } /* Calculate the cell dimensions required for pixel dimensions. */ @@ -1302,16 +1274,7 @@ image_clear_kitty(struct screen_write_ctx *ctx, char how, u_int image_id, ctx->wp->flags |= PANE_REDRAW; } -/* - * Redraw image layers in a screen area. Reports damage for just this - * area (translated from screen-relative to window coordinates) rather - * than marking the whole pane with PANE_REDRAW - a pane can be much - * taller than the area actually disturbed (e.g. a small scroll region, - * or a single line insert/delete far from where an image sits), and the - * old whole-pane flag caused every image anywhere in the pane to be - * erased and retransmitted regardless of whether it was anywhere near - * the affected area. - */ +/* Record window damage for image layers in a screen area. */ void image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, u_int ny) @@ -1320,7 +1283,7 @@ image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, if (wp == NULL) return; - if (!image_grid_check_area(ctx->s->grid, px, ctx->s->grid->hsize + py, + if (!image_grid_area_has_images(ctx->s->grid, px, ctx->s->grid->hsize + py, nx, ny)) return; redraw_damage_window(wp->window, wp->xoff + px, wp->yoff + py, nx, @@ -1335,22 +1298,16 @@ image_redraw_all(struct screen_write_ctx *ctx) screen_size_y(ctx->s)); } -/* - * Redraw images after a scrolling operation. Scoped to the current scroll - * region (screen->rupper..rlower), not the whole pane - a scroll only - * disturbs what is inside its region, and the region is very often - * smaller than the pane (e.g. a pager with a fixed header/footer, or - * $PAGER's status line). - */ +/* Record image damage within the current scroll region. */ void -image_redraw_scroll(struct screen_write_ctx *ctx, __unused u_int lines) +image_redraw_scroll(struct screen_write_ctx *ctx) { - struct screen *s = ctx->s; - struct window_pane *wp = ctx->wp; + struct screen *s = ctx->s; + struct window_pane *wp = ctx->wp; if (wp == NULL) return; - if (!image_grid_check_area(s->grid, 0, s->grid->hsize + s->rupper, + if (!image_grid_area_has_images(s->grid, 0, s->grid->hsize + s->rupper, screen_size_x(s), s->rlower - s->rupper + 1)) return; redraw_damage_window_scroll(wp->window, wp->xoff, @@ -1361,15 +1318,13 @@ image_redraw_scroll(struct screen_write_ctx *ctx, __unused u_int lines) /* Draw a clipped part of one image span. */ static void image_draw_span(const struct image_backend *backend, struct tty *tty, - struct screen *s, struct image_span *span, u_int start, u_int end, - u_int px, u_int py, u_int atx, u_int aty, - const struct tty_style_ctx *style_ctx) + struct image_span *span, u_int start, u_int end, + u_int px, u_int atx, u_int aty) { struct image_placement *placement = span->placement; struct image_rect rectangle; rectangle.image = placement->image; - grid_view_get_cell(s->grid, start, py, &rectangle.cell); if (placement->input == IMAGE_INPUT_SIXEL) rectangle.z = 0; else if (placement->z >= 0 && placement->z < INT32_MAX) @@ -1382,7 +1337,7 @@ image_draw_span(const struct image_backend *backend, struct tty *tty, rectangle.sy = 1; rectangle.destination_x = atx + start - px; rectangle.destination_y = aty; - backend->draw_rect(tty, &rectangle, style_ctx); + backend->draw_rect(tty, &rectangle); } /* Return whether a cell contains a glyph or text decoration. */ @@ -1400,8 +1355,7 @@ image_cell_has_text(struct grid *gd, u_int x, u_int y) /* Draw a span's graphical image layers before or after its text. */ void image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, - u_int nx, u_int atx, u_int aty, int before, - const struct tty_style_ctx *style_ctx) + u_int nx, u_int atx, u_int aty, int before) { const struct image_backend *backend; struct image_line *line; @@ -1441,8 +1395,8 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, if (span_end > end) span_end = end; if (!blank_only) { - image_draw_span(backend, tty, s, span, start, span_end, - px, py, atx, aty, style_ctx); + image_draw_span(backend, tty, span, start, span_end, + px, atx, aty); continue; } while (start < span_end) { @@ -1454,8 +1408,8 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, !image_cell_has_text(s->grid, draw_end, py)) draw_end++; if (start < draw_end) - image_draw_span(backend, tty, s, span, start, - draw_end, px, py, atx, aty, style_ctx); + image_draw_span(backend, tty, span, start, + draw_end, px, atx, aty); start = draw_end; } } @@ -1537,7 +1491,7 @@ image_line_cover(struct image_line *line, struct image_placement *placement, if (!image_cell_covers(placement->image, source_x, source_y, old->image, span->source_x + x - span->x, span->source_y)) continue; - image_line_remove(line, x, 1, -1, old); + image_line_remove(line, x, 1, IMAGE_INPUT_ALL, old); } } @@ -1575,8 +1529,9 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) struct image_line *line; struct image_span *span; struct image_placement *placement; - struct image_placement *seen[64]; - u_int nseen, i, row, cx, end_x, avail; + struct image_placement **seen = NULL; + size_t nseen, i; + u_int row, cx, end_x, avail; u_int source_y; int found; @@ -1597,8 +1552,11 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) break; } } - if (!found && nseen < nitems(seen)) + if (!found) { + seen = xreallocarray(seen, nseen + 1, + sizeof *seen); seen[nseen++] = span->placement; + } } for (i = 0; i < nseen; i++) { @@ -1629,12 +1587,13 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) avail); } } + free(seen); } /* Place an image at the cursor using the supplied input semantics. */ static void image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, - u_int input, u_int app_image_id, u_int app_placement_id, int32_t z) + enum image_input input, u_int app_image_id, u_int app_placement_id, int32_t z) { struct screen *s = ctx->s; struct grid *gd = s->grid; diff --git a/input.c b/input.c index 89ab9b7af..d126da73f 100644 --- a/input.c +++ b/input.c @@ -2802,58 +2802,56 @@ input_enter_apc(struct input_ctx *ictx) ictx->flags &= ~INPUT_LAST; } -/* APC terminator (ST) received. */ #ifdef ENABLE_IMAGES +/* Handle a Kitty graphics command. */ static int input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) { struct screen_write_ctx *sctx = &ictx->ctx; struct window_pane *wp = ictx->wp; struct image *im; - u_int image_id = 0, replace_id = 0, quiet = 0; - u_int placement_id = 0; - int32_t z = 0; - char action = '\0', delete = '\0'; - int status; + struct kitty_parse_result result; if (wp == NULL) return (0); im = kitty_parse_image(&ictx->kitty_state, buf, len, - wp->window->xpixel, - wp->window->ypixel, &image_id, &replace_id, &quiet, &action, - &delete, &placement_id, &z, &status); - if (status == KITTY_PARSE_MORE) + wp->window->xpixel, wp->window->ypixel, &result); + if (result.status == KITTY_PARSE_MORE) return (1); - if (status != KITTY_PARSE_OK) { - if (quiet < 2 && action != '\0') { - if (status == KITTY_PARSE_MISSING) + if (result.status != KITTY_PARSE_OK) { + if (result.quiet < 2 && result.action != '\0') { + if (result.status == KITTY_PARSE_MISSING) input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\", - image_id); + result.image_id); else input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\", - image_id); + result.image_id); } return (1); } - if (replace_id != 0) - image_clear(sctx, replace_id); + if (result.replace_id != 0) + image_clear(sctx, result.replace_id); if (im != NULL) { - if (action == 'd') - image_clear_kitty(sctx, delete, image_id, placement_id, z); + 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, image_id, - placement_id, z); + image_write_kitty(sctx, im, ictx->cell.cell.bg, + result.image_id, result.placement_id, result.z); image_free(image_get_id(im)); - if (quiet == 0 && image_id != 0) - input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id); - } else if (action == 'd') - image_clear_kitty(sctx, delete, image_id, placement_id, z); - else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0) - input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id); + if (result.quiet == 0 && result.image_id != 0) + input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", result.image_id); + } else if (result.action == 'd') + image_clear_kitty(sctx, result.delete, result.image_id, + result.placement_id, result.z); + else if ((result.action == 't' || result.action == 'q' || + result.action == 'u') && result.quiet == 0) + input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", result.image_id); return (1); } #endif +/* APC terminator (ST) received. */ static void input_exit_apc(struct input_ctx *ictx) { @@ -2933,9 +2931,8 @@ input_top_bit_set(struct input_ctx *ictx) struct utf8_data *ud = &ictx->utf8data; #ifdef ENABLE_IMAGES struct grid_cell gc; - struct image *im; - u_int x, source_x, source_y, image_id, placement_id; - int32_t z; + struct kitty_placeholder placeholder; + u_int x; #endif ictx->flags &= ~INPUT_LAST; @@ -2969,11 +2966,12 @@ input_top_bit_set(struct input_ctx *ictx) x = sctx->s->cx - 1; /* cx-1 is the cell just written. */ grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc); if (kitty_placeholder_to_image(ictx->kitty_state, - sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x, - &source_y, &image_id, &placement_id, &z)) { + 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, im, x, sctx->s->cy, source_x, - source_y, image_id, placement_id, z); + image_place_cell_kitty(sctx, placeholder.image, x, + sctx->s->cy, placeholder.source_x, + placeholder.source_y, placeholder.image_id, + placeholder.placement_id, placeholder.z); } } #endif diff --git a/layout-custom.c b/layout-custom.c index a0b50608b..d7f8f7e9f 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -543,7 +543,7 @@ layout_append(struct layout_cell *lcroot, struct layout_string *ls, int flags) if (flags & LAYOUT_CUSTOM_OLD_FORMAT) { if (!layout_cell_is_tiled(lcroot) && !layout_cell_has_tiled_child(lcroot)) - return (0); /* no tiled cell anywhere: empty v1 body */ + return (-1); lccompat = layout_custom_create_compat(lcroot); result = layout_append_v1(lccompat, ls); layout_custom_free_compat(lccompat); diff --git a/regress/image-border-status-wipe.sh b/regress/image-border-status-wipe.sh index baee6bee2..1a2290ec9 100644 --- a/regress/image-border-status-wipe.sh +++ b/regress/image-border-status-wipe.sh @@ -1,29 +1,7 @@ #!/bin/sh -# Regression test for a border-status caching bug: window_make_pane_status() -# (window-border.c) gates the physical redraw of a pane's border-status -# title on a logical content diff (grid_compare against a cached copy), not -# on whether the physical screen cells were disturbed by something else in -# the meantime - such as a floating pane's own border, drawn on top of a -# tiled pane's border-status row, sliding across it and then away again. -# See tmux-image-redraw-known-bugs.md ("border-status text cache ignores -# physical damage") for the full write-up. -# -# Reproduction: a tiled pane with pane-border-status on has a floating pane -# dragged, by mouse, from directly over its border-status row to somewhere -# else. The tiled pane's title should reappear once the floating pane has -# moved off it; without the fix it stays blank. -# -# The drag starts and ends away from row 0 rather than grabbing the -# floating pane's border while it is already sitting on row 0: when a -# floating pane's own border-status row exactly coincides with the tiled -# pane's row 0, mouse hit-testing on that row attributes clicks to the -# tiled pane, not the floating one on top of it (an unrelated tmux quirk, -# not what this test is about). That only matters for the initial press, -# though - once a drag is under way, further motion events go straight to -# the already-bound per-pane callback without re-resolving which pane owns -# the coordinates, so starting the grab on an unambiguous row and dragging -# through row 0 works fine. +# A tiled pane's border title must reappear after a floating pane moves away. +# Start the drag below row zero so its initial press hits the floating pane. PATH=/bin:/usr/bin TERM=screen diff --git a/regress/image-floating-content-wipe.sh b/regress/image-floating-content-wipe.sh index 59891bca2..0f5b2a89e 100644 --- a/regress/image-floating-content-wipe.sh +++ b/regress/image-floating-content-wipe.sh @@ -1,22 +1,7 @@ #!/bin/sh -# Regression test for a fixed bug: on a client detected as SIXEL-capable, a -# pane-content redraw (PANE_REDRAW) erased the image backend's rectangle -# using the redrawing pane's raw nominal geometry (wp->xoff/yoff/sx/sy) -# instead of the cells it actually owns in the current scene. When a -# floating pane occluded part of that rectangle, the erase blanked the -# floating pane's on-screen area, and since the subsequent text redraw -# correctly only refills cells the redrawing pane owns, nothing repainted -# it back in - the floating pane's content stayed blank. -# -# Fixed in screen-redraw.c:redraw_draw_pane_lines() by erasing only the -# cell ranges the pane actually owns per line (its REDRAW_SPAN_PANE spans), -# not its raw rectangle. See tmux-sixel-erase-ignores-occlusion.md and -# IMAGE-REDRAW-DISCUSSION.md for the full write-up. -# -# Confirmed by direct A/B test against this exact sequence: reliably wipes -# the floating pane's content on the unfixed code, reliably leaves it -# intact on the fixed code. +# A SIXEL pane redraw must erase only cells owned by that pane, preserving +# content in an overlapping floating pane when leaving copy mode. PATH=/bin:/usr/bin TERM=screen diff --git a/regress/image-movepane-drag-noflash.sh b/regress/image-movepane-drag-noflash.sh index 90eb876ef..d12f0a7d1 100755 --- a/regress/image-movepane-drag-noflash.sh +++ b/regress/image-movepane-drag-noflash.sh @@ -1,21 +1,6 @@ #!/bin/sh -# Regression test: Alt-dragging a floating pane by its body (move-pane -M, -# bound by default to M-MouseDrag1Pane/M-MouseDrag1Border) must not -# retransmit images in other panes of the same window. -# -# cmd_join_pane_mouse_move() (cmd-join-pane.c) is a second, separate -# implementation of "drag to move a floating pane" - parallel to the one in -# cmd-resize-pane.c already fixed for plain (non-Alt) border drags - and -# used to call server_redraw_window() unconditionally on every motion -# event, wiping and retransmitting every image in the window on each step -# of the drag even though only the floating pane's own rectangle actually -# moved. See tmux-image-redraw-known-bugs.md for the full write-up. -# -# This is checked by counting DCS (\033P) sequences in the client's raw -# output while a stationary tiled pane's image is present and an unrelated -# floating pane is Alt-dragged elsewhere in the window: with the fix, none -# should appear. +# Alt-dragging a floating pane must not retransmit images in other panes. . ./image-noflash-common.inc diff --git a/regress/image-placeholder-reflow.sh b/regress/image-placeholder-reflow.sh new file mode 100644 index 000000000..3440c661e --- /dev/null +++ b/regress/image-placeholder-reflow.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Kitty placeholder rows stay cell-aligned when a client becomes narrower. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Limage-placeholder-outer$$ -f/dev/null" +TMUX2="$TEST_TMUX -u -Limage-placeholder-inner$$ -f/dev/null" +TMP=$(mktemp) +trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15 + +$TMUX2 new-session -d -x 10 -y 4 " + i=0 + while [ \$i -lt 10 ]; do + printf '\\364\\216\\273\\256\\314\\205' + i=\$((i + 1)) + done + sleep 30" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX new-session -d -x 10 -y 4 "$TMUX2 attach-session" || exit 1 +$TMUX set -g status off || exit 1 +sleep 1 + +$TMUX resize-window -x 5 -y 4 || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 +[ -n "$(sed -n 1p $TMP)" ] || exit 1 +[ -z "$(sed -n 2p $TMP)" ] || exit 1 +$TMUX resize-window -x 10 -y 4 || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 +[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1 +[ -z "$(sed -n 2p $TMP)" ] || exit 1 + +exit 0 diff --git a/regress/image-resize-many-placements.sh b/regress/image-resize-many-placements.sh new file mode 100644 index 000000000..485ed55f7 --- /dev/null +++ b/regress/image-resize-many-placements.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Widening a pane restores clipped columns for every image placement. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Limage-resize-outer$$ -f/dev/null" +TMUX2="env -u TMUX LC_ALL=C $TEST_TMUX -Limage-resize-inner$$ -f/dev/null" +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. +$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\\\\' + 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\\\\' + sleep 30" || exit 1 +[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0 +$TMUX2 set -g status off || exit 1 +$TMUX new-session -d -x 8 -y 4 "$TMUX2 attach-session" || exit 1 +$TMUX set -g status off || exit 1 +sleep 1 + +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +[ "$(cat $TMP)" = '........' ] || exit 1 +$TMUX resize-window -x 16 -y 4 || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +[ "$(cat $TMP)" = '................' ] || exit 1 + +exit 0 diff --git a/regress/image-resize-width-recovery.sh b/regress/image-resize-width-recovery.sh index 17c47a32c..0dc8c009d 100755 --- a/regress/image-resize-width-recovery.sh +++ b/regress/image-resize-width-recovery.sh @@ -1,29 +1,8 @@ #!/bin/sh -# Regression test: an image wider than the pane at the time it was -# displayed must show more of itself once the pane grows wide enough, -# instead of staying clipped to its original width forever. -# -# image_write() (image.c) clips an image's width to whatever fit in the -# pane when it was first displayed, and never revisits that decision - -# unlike height, which recovers naturally through ordinary scrollback, -# there is no "scroll right", so the clipped columns were permanently -# discarded. window_pane_resize() (window.c) now calls -# image_grid_resize_width() after a pane grows wider, which extends each -# existing placement's spans - using the image's own retained, immutable -# pixel data - up to whichever is smaller: the image's full width or the -# new pane width. See tmux-image-redraw-known-bugs.md for the full -# write-up. -# -# This is checked via the SIXEL raster widths reported in the client's raw -# output before and after widening the window. The redraw triggered by the -# resize is damage-based (only the newly-uncovered columns are dirtied), so -# it does not redraw the whole row as one wider raster - it sends the -# already-correct clipped portion's width again untouched, plus a *second*, -# separate raster covering just the newly-added columns. So rather than -# looking for a single wider raster, this checks that the widths seen -# across both redraws, added together, account for the fixture's full -# pixel width - i.e. the previously-clipped remainder actually appeared. +# Widening a pane must recover image columns clipped at placement time. +# A damage redraw can send the newly visible columns as a separate raster, +# so compare the combined raster widths with the original image width. PATH=/bin:/usr/bin TERM=screen diff --git a/regress/image-scroll-off-recovery.sh b/regress/image-scroll-off-recovery.sh index 013d00910..f7ae3b8a0 100755 --- a/regress/image-scroll-off-recovery.sh +++ b/regress/image-scroll-off-recovery.sh @@ -1,29 +1,6 @@ #!/bin/sh -# Regression test: an image taller than the pane, at the moment it is first -# displayed, must remain visible when scrolling back up through history - -# not show blank space for the part that was scrolled off screen before the -# user ever got to see it. -# -# image_write() (image.c) handles this case by scrolling the screen up -# (via screen_write_scrollup()) to make room, then only ever wrote spans -# for the rows that ended up on screen afterwards - the rows that were -# immediately scrolled off (origin_y of them) were pushed into history as -# blank, spanless rows and their image data was discarded. Unlike width, -# which has no "scroll right" to recover a permanent clip, height already -# has ordinary scrollback, so this was pure waste. image_write() now also -# calls image_extend_row() for those origin_y history rows (capped to -# gd->hsize, since screen_write_scrollup() may not have pushed a real -# history row for every one of them - see tmux-uint-subtraction-underflow- -# care.md), so scrolling back up recovers the image instead of showing -# empty space. See tmux-image-redraw-known-bugs.md for the full write-up. -# -# This is checked by comparing how many separate SIXEL rasters the client -# receives right after entering copy mode (a baseline - copy mode itself -# triggers a redraw of the still-visible rows) against how many it -# receives after scrolling to the very top of history: with the fix, more -# rasters appear (covering the newly-revealed, previously-scrolled-off -# rows); without it, scrolling reveals nothing new. +# Images scrolled off-screen must reappear when copy mode scrolls back up. PATH=/bin:/usr/bin TERM=screen diff --git a/regress/image-selection-noflash.sh b/regress/image-selection-noflash.sh index 8951170cb..9a1ce8698 100755 --- a/regress/image-selection-noflash.sh +++ b/regress/image-selection-noflash.sh @@ -1,22 +1,6 @@ #!/bin/sh -# Regression test: extending a copy-mode selection by cursor movement, with -# the view otherwise unmoved (no scrolling), must not retransmit an image -# whose row the cursor passes through. -# -# window_copy_write_one() (window-copy.c) used to write text/highlight -# styling directly over image-covered cells, which - since a character -# write typically clears whatever pixel content a terminal was showing -# there - erased the image with nothing to redraw it back in. Separately, -# window_copy_write_line()'s call to image_redraw_area() used to fire -# unconditionally on every redraw, so even after fixing the erasure, the -# image would still be needlessly recomposited (and briefly flash) on -# every single cursor step even though nothing about it had changed. See -# tmux-image-redraw-known-bugs.md for the full write-up. -# -# This is checked by counting DCS (\033P) sequences in the client's raw -# output during the cursor movement: with the fix, extending a selection -# without scrolling never touches the image, so none should appear. +# Moving a copy-mode selection cursor must not retransmit unchanged images. . ./image-noflash-common.inc diff --git a/regress/image-splitwindow-resize-noflash.sh b/regress/image-splitwindow-resize-noflash.sh index 8acc9f371..cdb14b785 100755 --- a/regress/image-splitwindow-resize-noflash.sh +++ b/regress/image-splitwindow-resize-noflash.sh @@ -1,23 +1,7 @@ #!/bin/sh -# Regression test: interactively sizing a new floating pane by dragging -# (new-pane -M, bound by default to C-MouseDrag1Pane/C-MouseDrag1Empty) -# must not retransmit images in other panes of the same window on ongoing -# drag steps. -# -# cmd_split_window_mouse_resize() (cmd-split-window.c) used to call -# server_redraw_window() unconditionally on every motion event while -# sizing the new floating pane, wiping and retransmitting every image in -# the window on each step even though only the new pane's own rectangle -# actually changed. See tmux-image-redraw-known-bugs.md for the full -# write-up. -# -# Creating the floating pane itself causes one legitimate, unrelated -# redraw (pane creation always redraws the session - see -# server_redraw_session() in cmd-split-window.c), so this checks DCS -# (\033P) counts only for the *ongoing* drag-motion steps after that -# initial creation, once c->tty.mouse_drag_update is already bound to the -# resize callback: with the fix, none should appear there. +# Sizing a new floating pane must not retransmit images in other panes. +# Wait for the initial creation redraw before checking subsequent drag steps. . ./image-noflash-common.inc diff --git a/regress/image-support.sh b/regress/image-support.sh index 3986bc778..8f66b7daf 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -4,10 +4,12 @@ PATH=/bin:/usr/bin TERM=screen +LC_ALL=C +export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -Limage$$ -f/dev/null" -TMUX2="$TEST_TMUX -Limage-client$$ -f/dev/null" +TMUX2="env -u TMUX LC_ALL=C $TEST_TMUX -Limage-client$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null @@ -24,6 +26,7 @@ $TMUX new-session -d -x 20 -y 8 " sleep 1 [ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 +[ "$($TMUX display-message -p '#{sixel_support}')" = 1 ] || exit 1 # Images scroll as grid cells, while capture output contains ordinary spaces. [ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] || exit 1 @@ -187,27 +190,9 @@ $TMUX capture-pane -pS0 -E3 >$TMP || exit 1 [ "$(sed -n 2p $TMP)" = "@@@@@" ] || exit 1 [ -z "$(sed -n 3p $TMP)" ] || exit 1 -# Kitty virtual placements use U+10EEEE placeholder cells. Keep their rows at -# fixed coordinates when narrowing the terminal, clipping instead of reflowing -# the second half onto the following row. +# Restore the wider client for the remaining placement checks. $TMUX resize-window -x 10 -y 4 || exit 1 sleep 1 -$TMUX2 new-window -d " - printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205' - printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205' - sleep 10" || exit 1 -$TMUX2 select-window -t:4 || exit 1 -sleep 1 -$TMUX resize-window -x 5 -y 4 || exit 1 -sleep 1 -$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 -[ -n "$(sed -n 1p $TMP)" ] || exit 1 -[ -z "$(sed -n 2p $TMP)" ] || exit 1 -$TMUX resize-window -x 10 -y 4 || exit 1 -sleep 1 -$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 -[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1 -[ -z "$(sed -n 2p $TMP)" ] || exit 1 # A placement ID supplied with transmit-and-place is reused by a later place. # This is the sequence used by chawan: the newline moves the cursor down before diff --git a/resize.c b/resize.c index 76caec352..a286264a4 100644 --- a/resize.c +++ b/resize.c @@ -154,11 +154,8 @@ clients_calculate_size(int type, int current, struct client *c, } if (w != NULL && type == WINDOW_SIZE_MANUAL) { /* - * Manual mode has no separate pixel size to apply, so keep the - * window's own - otherwise this always differs from the real - * w->xpixel/ypixel, and recalculate_size() can never tell a - * later call apart from an actual change, resizing (and - * re-firing window-resized) on every recalculation. + * Keep the window's pixel size in manual mode to avoid scheduling + * a resize on every recalculation. */ *xpixel = w->xpixel; *ypixel = w->ypixel; diff --git a/screen-redraw.c b/screen-redraw.c index 6f5e5830d..f110f75bd 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1332,7 +1332,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx, #ifdef ENABLE_IMAGES if (phase != REDRAW_TEXT) { image_draw_line(tty, s, px, py, n, x, y, - phase == REDRAW_IMAGES_BEFORE, &style_ctx); + phase == REDRAW_IMAGES_BEFORE); return; } #endif diff --git a/screen-write.c b/screen-write.c index 63920641c..1b0080eb3 100644 --- a/screen-write.c +++ b/screen-write.c @@ -404,7 +404,7 @@ 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_damage(ctx->s->grid, x, ctx->s->grid->hsize + y, + image_grid_remove_sixel_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_damage(s->grid, 0, s->grid->hsize, + image_grid_remove_sixel_spans(s->grid, 0, s->grid->hsize, screen_size_x(s), screen_size_y(s)); #endif @@ -1909,7 +1909,7 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg) } #ifdef ENABLE_IMAGES - image_redraw_scroll(ctx, 1); + image_redraw_scroll(ctx); #endif grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); @@ -1981,7 +1981,7 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) #ifdef ENABLE_IMAGES if (rlower == screen_size_y(s) - 1) - image_redraw_scroll(ctx, 1); + image_redraw_scroll(ctx); else image_redraw_area(ctx, 0, rupper, screen_size_x(s), rlower - rupper); @@ -2011,7 +2011,7 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg) } #ifdef ENABLE_IMAGES - image_redraw_scroll(ctx, lines); + image_redraw_scroll(ctx); #endif for (i = 0; i < lines; i++) { @@ -2039,7 +2039,7 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) lines = s->rlower - s->rupper + 1; #ifdef ENABLE_IMAGES - image_redraw_scroll(ctx, lines); + image_redraw_scroll(ctx); #endif for (i = 0; i < lines; i++) diff --git a/style.c b/style.c index 484b2a59c..55d08fe75 100644 --- a/style.c +++ b/style.c @@ -30,8 +30,7 @@ /* Default style. */ static struct style style_default = { - { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 - }, + { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 }, 0, 0, diff --git a/tmux.1 b/tmux.1 index dc7ae74b9..c8161bf89 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5110,6 +5110,8 @@ Supports OSC 8 hyperlinks. Allows image fallbacks to use Unicode quadrant block characters. .It image-sextants Allows image fallbacks to use Unicode sextant block characters. +.It imagescroll +Moves image content along with scrolling regions. .It ignorefkeys Ignore function keys from .Xr terminfo 5 @@ -5117,9 +5119,7 @@ and use the .Nm internal set only. .It kitty -Supports Kitty graphics using Unicode placeholders. -The terminal must support the Kitty Unicode placeholder extension, not only -the basic Kitty graphics protocol. +Supports the Kitty graphics protocol. .It margins Supports DECSLRM margins. .It mouse @@ -7350,6 +7350,7 @@ The following variables are available, where appropriate: .It Li "hook_window_name" Ta "" Ta "Name of window where hook was run, if any" .It Li "host" Ta "#H" Ta "Hostname of local host" .It Li "host_short" Ta "#h" Ta "Hostname of local host (no domain name)" +.It Li "image_support" Ta "" Ta "1 if server has support for images" .It Li "insert_flag" Ta "" Ta "Pane insert flag" .It Li "key_string" Ta "" Ta "String representation of the key binding" .It Li "key_repeat" Ta "" Ta "1 if key binding is repeatable" diff --git a/tmux.h b/tmux.h index 58b96bf32..d2796665f 100644 --- a/tmux.h +++ b/tmux.h @@ -868,7 +868,7 @@ struct colour_palette { struct grid_cell { struct utf8_data data; u_short attr; - u_short flags; + u_char flags; int fg; int bg; int us; @@ -879,7 +879,7 @@ struct grid_cell { struct grid_extd_entry { utf8_char data; u_short attr; - u_short flags; + u_char flags; int fg; int bg; int us; @@ -897,7 +897,7 @@ struct grid_cell_entry { u_char data; } data; }; - u_short flags; + u_char flags; } __packed; /* OSC 133 data for a grid line. */ @@ -1096,6 +1096,33 @@ struct image { }; RB_HEAD(images, image); #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) + +enum kitty_parse_status { + KITTY_PARSE_ERROR = -1, + KITTY_PARSE_OK, + KITTY_PARSE_MORE, + KITTY_PARSE_MISSING +}; + +struct kitty_parse_result { + u_int image_id; + u_int replace_id; + u_int placement_id; + u_int quiet; + int32_t z; + char action; + char delete; + enum kitty_parse_status status; +}; + +struct kitty_placeholder { + struct image *image; + u_int source_x; + u_int source_y; + u_int image_id; + u_int placement_id; + int32_t z; +}; #endif /* Cursor style. */ @@ -1956,9 +1983,6 @@ struct tty_ctx { size_t size; } sel; -#ifdef ENABLE_IMAGES - struct image *image; -#endif }; /* @@ -4321,7 +4345,7 @@ u_char *image_png_decode(const u_char *, size_t, size_t, u_int *, void image_redraw_area(struct screen_write_ctx *, u_int, u_int, u_int, u_int); void image_redraw_all(struct screen_write_ctx *); -void image_redraw_scroll(struct screen_write_ctx *, u_int); +void image_redraw_scroll(struct screen_write_ctx *); void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void image_redraw_finish(struct tty *); void image_draw_flush(struct tty *); @@ -4330,25 +4354,22 @@ int image_tty_update(struct tty *); void image_tty_geometry_changed(struct tty *); void image_tty_free(struct tty *, int); void image_draw_line(struct tty *, struct screen *, u_int, u_int, - u_int, u_int, u_int, int, const struct tty_style_ctx *); + u_int, u_int, u_int, int); void image_get_fallback_cell(struct tty *, struct image *, u_int, - u_int, const struct grid_cell *, struct grid_cell *, - const struct tty_style_ctx *); + u_int, const struct grid_cell *, struct grid_cell *); const struct image_cell *image_get_cell(struct image *, u_int, u_int); void image_free_fallback(struct image *); int image_get_fallback_at(struct tty *, struct screen *, u_int, - u_int, const struct grid_cell *, struct grid_cell *, - const struct tty_style_ctx *); + u_int, const struct grid_cell *, struct grid_cell *); struct image *image_rect_get_image(const struct image_rect *); -const struct grid_cell *image_rect_get_cell( - const struct image_rect *); 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_damage(struct grid *, u_int, u_int, u_int, u_int); + void image_grid_remove_sixel_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, @@ -4359,35 +4380,29 @@ void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, u_int, u_int, u_int, u_int); void image_grid_resize_width(struct grid *, u_int); int image_grid_line_has_images(const struct grid_line *); -int image_grid_check_area(struct grid *, u_int, u_int, u_int, + 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 *); 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); -#define KITTY_PARSE_ERROR -1 -#define KITTY_PARSE_OK 0 -#define KITTY_PARSE_MORE 1 -#define KITTY_PARSE_MISSING 2 +/* image-kitty.c */ struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, - u_int, u_int *, u_int *, u_int *, char *, char *, u_int *, - int32_t *, int *); + u_int, struct kitty_parse_result *); int kitty_placeholder_to_image(void *, struct grid *, - struct grid_cell *, u_int, u_int, struct image **, u_int *, - u_int *, u_int *, u_int *, int32_t *); + struct grid_cell *, u_int, u_int, + struct kitty_placeholder *); void kitty_free_state(void *); void kitty_draw_rect(struct tty *, - const struct image_rect *, const struct tty_style_ctx *); + const struct image_rect *); void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void kitty_redraw_finish(struct tty *); void kitty_free_output_state(struct tty *, int); -#endif -#ifdef ENABLE_IMAGES /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024 void sixel_draw_rect(struct tty *, - const struct image_rect *, const struct tty_style_ctx *); + const struct image_rect *); void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void sixel_free_output(struct tty *, int); void sixel_flush_output(struct tty *); @@ -4400,7 +4415,6 @@ struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int, u_int, u_int, u_int, int); char *sixel_print(struct sixel_image *, struct sixel_image *, size_t *); -struct screen *sixel_to_screen(struct sixel_image *); struct image *sixel_to_image(struct sixel_image *); #endif diff --git a/tools/sixel-scroll-region-test.sh b/tools/sixel-scroll-region-test.sh index eb758db80..d311cf5b2 100755 --- a/tools/sixel-scroll-region-test.sh +++ b/tools/sixel-scroll-region-test.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Visual test for sixel-region-scrolling: does this terminal actually move +# Visual test for imagescroll: does this terminal actually move # SIXEL pixels along with tmux's own scroll-region escapes (DECSTBM/DECSLRM # + IND/RI/RIN), or does tmux need to fall back to redrawing the image on # every scroll? @@ -19,7 +19,7 @@ # looks identical to "off", this terminal is fine. If a band freezes, # duplicates, tears, or drifts out of sync with the counter, this # terminal does not scroll SIXEL regions correctly and -# sixel-region-scrolling should stay off for it. +# imagescroll should stay off for it. # # Usage: sh tools/sixel-scroll-region-test.sh [on|off] @@ -32,10 +32,13 @@ on|off) ;; *) echo "usage: $0 [on|off]" >&2; exit 1 ;; esac -tmux set -s sixel-region-scrolling "$MODE" +case "$MODE" in +on) tmux set -as terminal-features ',*:imagescroll' ;; +off) tmux set -as terminal-features ',*:imagescroll@' ;; +esac tmux set -as terminal-features ',*:sixel' -echo "Testing with sixel-region-scrolling=$MODE. Watch the coloured bands" >&2 +echo "Testing with imagescroll=$MODE. Watch the coloured bands" >&2 echo "in the floating pane - they should stay locked to the counter." >&2 sleep 2 diff --git a/tty-draw.c b/tty-draw.c index 277508c11..b63407249 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -258,7 +258,7 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, gcp = &grid_default_cell; #ifdef ENABLE_IMAGES image_status = image_get_fallback_at(tty, s, px + i, - py, gcp, &image_gc, style_ctx); + py, gcp, &image_gc); if (image_status == 1) { gcp = &image_gc; empty = 0; @@ -273,15 +273,13 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, gcp = &gc; #ifdef ENABLE_IMAGES if (image_get_fallback_at(tty, s, px + i, py, &gc, - &image_gc, style_ctx) == 1) + &image_gc) == 1) gcp = &image_gc; #endif /* Work out empty cells. */ empty = tty_draw_line_get_empty(gcp, &last, nx - i); - if (empty != 0) - ; - else { + if (empty == 0) { /* Update for codeset if needed. */ gcp = tty_check_codeset(tty, gcp); diff --git a/tty-features.c b/tty-features.c index 462e22673..0eef43511 100644 --- a/tty-features.c +++ b/tty-features.c @@ -399,14 +399,8 @@ static const struct tty_feature tty_feature_image_sextants = { }; /* - * Terminal moves SIXEL or Kitty image content along with the rest of a - * scrolling region, rather than needing it redrawn after every scroll. - * There is no way to ask a terminal this, and it does not correlate with - * DECSLRM/margins support - confirmed by direct testing that mintty - * scrolls text within a margin-bounded region correctly but drops sixel - * content placed there, while WezTerm and Windows Terminal move it - * correctly - so this is granted per terminal individually rather than - * assumed from any other capability. + * Terminal moves images with a scrolling region. Text margin support does + * not imply this capability. */ static const struct tty_feature tty_feature_imagescroll = { "imagescroll", diff --git a/tty-term.c b/tty-term.c index 85e9bcaa6..1ac078cd4 100644 --- a/tty-term.c +++ b/tty-term.c @@ -531,7 +531,6 @@ tty_term_apply_overrides(struct tty_term *term) tty_term_validate(term); } -/* Mark terminals whose Ms capability does not contain a clipboard argument. */ static void tty_term_validate(struct tty_term *term) { diff --git a/tty.c b/tty.c index cbb620c7a..9f84a0688 100644 --- a/tty.c +++ b/tty.c @@ -585,12 +585,8 @@ tty_update_features(struct tty *tty) tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC)); /* - * Features might have changed since the first draw during attach, for - * example on a DA response - but only redraw if something actually - * did change, since this is called for every DA answer (which are - * usually confirming what is already known) and a redraw repaints - * from tmux's own grid, discarding anything the pane put on the - * terminal that tmux does not model (e.g. a DCS-passthrough image). + * Redraw only for changed features; repeated DA replies must not erase + * images sent through DCS passthrough. */ if (!changed) return; diff --git a/utf8.c b/utf8.c index 9d15b7a1a..a00c1bd6d 100644 --- a/utf8.c +++ b/utf8.c @@ -374,10 +374,6 @@ utf8_add_to_width_cache(const char *s) wc_end = wc_start; } - /* - * wc_end may be WCHAR_MAX, so incrementing wc past it would - * overflow (wc_start <= wc_end is already guaranteed above). - */ wc = wc_start; for (;;) { utf8_insert_width_cache(wc, width); diff --git a/window-copy.c b/window-copy.c index 6bef8c679..eeca36432 100644 --- a/window-copy.c +++ b/window-copy.c @@ -5246,7 +5246,7 @@ window_copy_write_one(struct window_mode_entry *wme, * screen_write_cell() (its image-damage call would * re-damage the image on every redraw for nothing). */ - if (image_grid_check_area(gd, fx, fy, gc.data.width, + if (image_grid_area_has_images(gd, fx, fy, gc.data.width, 1)) { grid_view_set_cell(ctx->s->grid, px + fx, py, &gc); @@ -5610,9 +5610,12 @@ window_copy_visible_has_images(struct window_copy_mode_data *data) { struct grid *gd = data->backing->grid; u_int sy = screen_size_y(&data->screen); + int has_images; - return (image_grid_check_area(gd, 0, screen_hsize(data->backing) - - data->oy, screen_size_x(&data->screen), sy)); + has_images = image_grid_area_has_images(gd, 0, + screen_hsize(data->backing) - data->oy, + screen_size_x(&data->screen), sy); + return (has_images); } #endif