Fix Kitty image deletion, placeholders and placement geometry.

This commit is contained in:
Michael Grant
2026-10-09 10:44:11 +02:00
parent cd14c5fc1a
commit a0d5763515
12 changed files with 939 additions and 380 deletions

6
grid.c
View File

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

View File

@@ -18,6 +18,7 @@
#include <sys/types.h>
#include <limits.h>
#include <math.h>
#include <resolv.h>
#include <stdlib.h>
#include <string.h>
@@ -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);
}

317
image.c
View File

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

49
input.c
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

40
tmux.h
View File

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

View File

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

18
tty.c
View File

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