From 75b606c3df81a458051879d683fb32fe37ac008f Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Thu, 6 Aug 2026 00:06:59 +0100 Subject: [PATCH] The two Chawan symptoms now have matching fixes: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Layout corruption: C=1 is respected. - Lingering remnants: tmux now tracks Kitty’s (image ID, placement ID) identity, replaces moved placements, and honors placement-specific d=i,...,p=... deletion. --- image-kitty.c | 136 ++++++++++++++++++++++++++++++++++++--- input.c | 7 +- regress/image-support.sh | 26 ++++++++ tmux.h | 2 +- 4 files changed, 159 insertions(+), 12 deletions(-) diff --git a/image-kitty.c b/image-kitty.c index 2cec1bbe8..045645cb9 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -81,6 +81,7 @@ struct kitty_state { u_int columns; u_int rows; u_int image_id; + u_int placement_id; u_int quiet; int no_cursor; int virtual; @@ -91,10 +92,17 @@ struct kitty_state { size_t encodedlen; }; +struct kitty_placement { + u_int placement_id; + u_int server_id; + 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; }; @@ -441,6 +449,7 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'c': case 'r': case 'i': + case 'p': case 'q': case 'm': case 'S': @@ -460,6 +469,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 'p': ks->placement_id = number; break; case 'q': ks->quiet = number; break; case 'm': ks->more = (number != 0); break; case 'S': ks->data_size = number; break; @@ -481,6 +491,29 @@ kitty_state_free(struct kitty_state *ks) free(ks); } +static void +kitty_placements_free(struct kitty_source *source) +{ + struct kitty_placement *placement, *next; + + for (placement = source->placements; placement != NULL; + placement = next) { + next = placement->next; + image_free(placement->server_id); + free(placement); + } + source->placements = NULL; +} + +static void +kitty_placements_free_all(struct kitty_context *kc) +{ + struct kitty_source *source; + + for (source = kc->sources; source != NULL; source = source->next) + kitty_placements_free(source); +} + void kitty_free_state(void *state) { @@ -492,6 +525,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); @@ -512,13 +546,14 @@ kitty_source_find(struct kitty_context *kc, u_int id) return (NULL); } -static void +static u_int kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) { struct kitty_source *source; + u_int old_id = 0; if (id == 0) - return; + return (0); source = kitty_source_find(kc, id); if (source == NULL) { source = xcalloc(1, sizeof *source); @@ -526,6 +561,8 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) source->next = kc->sources; kc->sources = source; } else { + old_id = source->server_id; + kitty_placements_free(source); if (source->virtual_id != 0) { image_free(source->virtual_id); source->virtual_id = 0; @@ -534,6 +571,65 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) } image_ref(im->id); source->server_id = im->id; + return (old_id); +} + +static u_int +kitty_placement_set(struct kitty_context *kc, u_int image_id, + u_int placement_id, 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); + if (source == NULL) + return (0); + for (placement = source->placements; placement != NULL; + placement = placement->next) { + if (placement->placement_id == placement_id) + break; + } + if (placement == NULL) { + placement = xcalloc(1, sizeof *placement); + placement->placement_id = placement_id; + placement->next = source->placements; + source->placements = placement; + } + old_id = placement->server_id; + image_ref(im->id); + placement->server_id = im->id; + if (old_id != 0) + image_free(old_id); + return (old_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(im->id); + *pp = placement->next; + image_free(placement->server_id); + free(placement); + return (im); + } + return (NULL); } static void @@ -563,6 +659,7 @@ kitty_source_remove(struct kitty_context *kc, u_int id) if (im != NULL) image_ref(im->id); *pp = source->next; + kitty_placements_free(source); if (source->virtual_id != 0) image_free(source->virtual_id); image_free(source->server_id); @@ -733,7 +830,8 @@ 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 *quiet, char *action, int *status) + u_int ypixel, u_int *image_id, u_int *replace_id, u_int *quiet, + char *action, int *status) { struct kitty_context *kc = *state; struct kitty_state *ks; @@ -745,12 +843,14 @@ 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; if (kc == NULL) { kc = xcalloc(1, sizeof *kc); *state = kc; } *image_id = 0; + *replace_id = 0; *quiet = 0; *action = '\0'; *status = KITTY_PARSE_ERROR; @@ -802,18 +902,33 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, } else { im = kitty_place_image(source, ks, xpixel, ypixel); image_free(source->id); - if (im != NULL) + if (im != NULL) { + *replace_id = kitty_placement_set(kc, ks->image_id, + ks->placement_id, im); *status = KITTY_PARSE_OK; + } } kitty_state_free(ks); return (im); } if (ks->action == 'd') { - if (ks->delete == 'a' || ks->delete == 'A') + if (ks->delete == 'a' || ks->delete == 'A') { + kitty_placements_free_all(kc); im = NULL; - else if (ks->delete == 'i') - im = kitty_source_get(kc, ks->image_id); - else if (ks->delete == 'I') + } 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 goto fail; @@ -885,11 +1000,14 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, else { *status = KITTY_PARSE_OK; if (ks->action != 'q') - kitty_source_set(kc, ks->image_id, source); + *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; + else if (ks->placement_id != 0) + (void)kitty_placement_set(kc, ks->image_id, + ks->placement_id, im); } else if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) diff --git a/input.c b/input.c index cbbd6f1f3..55e1d7283 100644 --- a/input.c +++ b/input.c @@ -2817,7 +2817,7 @@ 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, quiet = 0; + u_int image_id = 0, replace_id = 0, quiet = 0; char action = '\0'; int status; @@ -2825,7 +2825,8 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) return (0); im = kitty_parse_image(&ictx->kitty_state, buf, len, wp->window->xpixel, - wp->window->ypixel, &image_id, &quiet, &action, &status); + wp->window->ypixel, &image_id, &replace_id, &quiet, &action, + &status); if (status == KITTY_PARSE_MORE) return (1); if (status != KITTY_PARSE_OK) { @@ -2839,6 +2840,8 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) } return (1); } + if (replace_id != 0) + image_clear(sctx, replace_id); if (im != NULL) { if (action == 'd') image_clear(sctx, im->id); diff --git a/regress/image-support.sh b/regress/image-support.sh index 1fbaa44ce..636b0b32b 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -192,6 +192,32 @@ $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 nonzero Kitty placement ID identifies one placement of an image. Reusing +# the same image and placement IDs moves it rather than leaving the old cells. +PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " + printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=11;/////w==\033\\' + printf '\033[2;2H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\' + printf '\033[4;6H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\' + sleep 10") || exit 1 +$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 +[ -z "$(sed -n 2p $TMP)" ] || exit 1 +[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1 + +# Deleting one placement ID leaves other placements of the image intact. +PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " + printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=12;/////w==\033\\' + printf '\033[2;2H\033_Ga=p,q=2,C=1,i=12,p=7,c=2,r=1\033\\' + printf '\033[4;6H\033_Ga=p,q=2,C=1,i=12,p=8,c=2,r=1\033\\' + printf '\033_Ga=d,d=i,q=2,i=12,p=7\033\\' + sleep 10") || exit 1 +$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 +[ -z "$(sed -n 2p $TMP)" ] || exit 1 +[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1 + # A weighted median at the maximum channel level must still leave colours on # both sides of the split. This skewed black, grey and white image used to stop # palette generation after one colour instead of producing three. diff --git a/tmux.h b/tmux.h index cb6eba04a..aac55aba8 100644 --- a/tmux.h +++ b/tmux.h @@ -4327,7 +4327,7 @@ void image_clear(struct screen_write_ctx *, u_int); #define KITTY_PARSE_MORE 1 #define KITTY_PARSE_MISSING 2 struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, - u_int, u_int *, u_int *, char *, int *); + u_int, u_int *, u_int *, u_int *, char *, int *); int kitty_placeholder_to_cell(void *, struct grid_cell *, const struct grid_cell *); void kitty_free_state(void *);