patch 9.2.0962: popup images are not using the kitty protocol properly
Commit:
https://github.com/vim/vim/commit/89caa43b7882e34067e0ea8e3d64a7d29581744a
Author: Foxe Chen <
chen...@gmail.com>
Date: Mon Aug 17 20:21:28 2026 +0000
patch 9.2.0962: popup images are not using the kitty protocol properly
Problem: The kitty image backend re-encodes and retransmits the whole
image on every redraw, since the transmit and the placement are
sent as a single "a=T" sequence built by popup_encode_image().
Solution: Transmit the image once with "a=t" and send only a placement
"a=p" on each redraw, cropping via the protocol's x/y/w/h
instead of re-cropping the pixel data (Foxe Chen).
closes: #21018
Signed-off-by: Foxe Chen <
chen...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/kitty.c b/src/kitty.c
index 2b46b1454..279eba842 100644
--- a/src/kitty.c
+++ b/src/kitty.c
@@ -13,163 +13,136 @@
* The popup's image bytes are sent in 4096-byte chunks of base64
* inside ` _G...;<chunk> \` envelopes.
* Spec:
https://sw.kovidgoyal.net/kitty/graphics-protocol/
- * No external dependency; the base64 alphabet is inlined here.
+ * Base64 encoding is shared with misc2 base64_encode()/decode()
*/
#include "vim.h"
#if defined(FEAT_IMAGE_KITTY) || defined(PROTO)
-static const char_u kitty_b64_table[] =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+// Max base64 chars per envelope, per the kitty graphics protocol.
+#define KITTY_CHUNK_B64 4096
+// Source bytes that encode into KITTY_CHUNK_B64 base64 chars.
+#define KITTY_CHUNK_SRC (KITTY_CHUNK_B64 * 3 / 4)
+// header + base64 chunk + " \" trailer + NUL. Use 128 extra bytes padding
+// for header, should be more than enough.
+#define KITTY_BUF_SIZE (128 + KITTY_CHUNK_B64 + 2 + 1)
/*
- * Append a NUL-terminated string to "ga". Returns OK / FAIL so the caller
- * can abort on allocation failure (unlike ga_concat(), which silently no-ops
- * and would leave a truncated, invalid kitty APC sequence behind).
+ * Return the kitty image id to use for window id "id". Kitty image ids are
+ * global to the terminal, so mix in the process id: another Vim in the same
+ * terminal would otherwise use the same ids and its "a=d,d=I" would free our
+ * image data.
*/
static int
-kitty_ga_concat(garray_T *ga, const char_u *s)
+kitty_image_id(int id)
{
- int len = (int)STRLEN(s);
+ static int base = 0;
- if (len == 0)
- return OK;
- if (ga_grow(ga, len) == FAIL)
- return FAIL;
- mch_memmove((char_u *)ga->ga_data + ga->ga_len, s, (size_t)len);
- ga->ga_len += len;
- return OK;
+ if (base == 0)
+ base = (((int)mch_get_pid() & 0x7fff) + 1) << 16;
+ return base | (id & 0xffff);
}
/*
- * Append base64-encoded bytes from "src[len]" to growarray "ga".
- * Returns OK / FAIL so the caller can propagate OOM.
+ * Transmit an RGB(A) image to the terminal (does not display it!). It will
+ * have an id of "id", so that it can be placed later.
*/
- static int
-kitty_b64_append(garray_T *ga, char_u *src, long len)
+ int
+kitty_transmit(image_rgb_T *img, int id)
{
- long i;
- long out_len = ((len + 2) / 3) * 4;
- char_u *dst;
-
- if (out_len == 0)
- return OK;
- if (ga_grow(ga, (int)out_len) == FAIL)
- return FAIL;
- dst = (char_u *)ga->ga_data + ga->ga_len;
- for (i = 0; i < len; i += 3)
- {
- unsigned a = src[i];
- unsigned b = (i + 1 < len) ? src[i + 1] : 0;
- unsigned c = (i + 2 < len) ? src[i + 2] : 0;
- unsigned triple = (a << 16) | (b << 8) | c;
-
- *dst++ = kitty_b64_table[(triple >> 18) & 0x3f];
- *dst++ = kitty_b64_table[(triple >> 12) & 0x3f];
- *dst++ = (i + 1 < len)
- ? kitty_b64_table[(triple >> 6) & 0x3f] : '=';
- *dst++ = (i + 2 < len)
- ? kitty_b64_table[triple & 0x3f] : '=';
- }
- ga->ga_len += (int)out_len;
- return OK;
-}
+ static char buf[KITTY_BUF_SIZE];
-/*
- * Encode an RGB(A) image into a kitty graphics protocol APC sequence.
- * Returns a malloced char_u* containing the full sequence
- * (one or more ` _G... \` envelopes), or NULL on OOM.
- *
- * The sequence is emitted with `a=T` (transmit + display), `q=2` (no
- * status responses), `f=24` for RGB or `f=32` for RGBA, and chunked
- * via `m=1`/`m=0` so the per-envelope payload stays under kitty's
- * 4096-byte limit. When "id" is non-zero it is sent as `i=<id>` so
- * the resulting placement can later be removed via kitty_delete().
- * "zindex" is sent as `z=<zindex>` so overlapping placements stack in
- * popup zindex order no matter in which order they were (re)created.
- */
- char_u *
-kitty_encode(image_rgb_T *img, int id, int zindex)
-{
- garray_T ga;
long pix_bytes;
long payload_len;
- long b64_total;
long offset = 0;
int fmt;
int first = TRUE;
- char_u hdr[80];
if (img == NULL || img->data == NULL || img->width <= 0 || img->height <= 0)
- return NULL;
+ return FAIL;
pix_bytes = img->has_alpha ? 4 : 3;
payload_len = (long)img->width * img->height * pix_bytes;
- b64_total = ((payload_len + 2) / 3) * 4;
fmt = img->has_alpha ? 32 : 24;
- ga_init2(&ga, 1, (int)b64_total + 256);
-
- // Emit one envelope per 4096 base64 chars. The first envelope
- // carries the full geometry/format header; later envelopes only
- // need the chunk-continuation marker `m=`.
- while (offset < b64_total)
+ // Emit one envelope per KITTY_CHUNK_SRC source bytes (= 4096 base64
+ // chars). The first envelope carries the full geometry/format
+ // header; later envelopes only need the chunk-continuation marker
+ // `m=`.
+ while (offset < payload_len)
{
- long this_chunk = b64_total - offset;
+ long this_chunk = payload_len - offset;
int more;
+ int hdr_len;
+ long b64_len;
- if (this_chunk > 4096)
- this_chunk = 4096;
- more = (offset + this_chunk < b64_total);
+ if (this_chunk > KITTY_CHUNK_SRC)
+ this_chunk = KITTY_CHUNK_SRC;
+ more = (offset + this_chunk < payload_len);
if (first)
{
- if (id != 0)
- vim_snprintf((char *)hdr, sizeof(hdr),
- " _Ga=T,f=%d,s=%d,v=%d,i=%d,z=%d,q=2,m=%d;",
- fmt, img->width, img->height, id, zindex,
- more ? 1 : 0);
- else
- vim_snprintf((char *)hdr, sizeof(hdr),
- " _Ga=T,f=%d,s=%d,v=%d,z=%d,q=2,m=%d;",
- fmt, img->width, img->height, zindex, more ? 1 : 0);
+ hdr_len = vim_snprintf(buf, sizeof(buf),
+ " _Ga=t,i=%d,f=%d,s=%d,v=%d,q=2,m=%d;",
+ kitty_image_id(id), fmt, img->width,
+ img->height, more ? 1 : 0);
first = FALSE;
}
else
- {
- vim_snprintf((char *)hdr, sizeof(hdr),
- " _Gm=%d;", more ? 1 : 0);
- }
- if (kitty_ga_concat(&ga, hdr) == FAIL)
- goto fail;
+ hdr_len = vim_snprintf(buf, sizeof(buf), " _Gm=%d;",
+ more ? 1 : 0);
- // Encode the matching slice of the source bytes. Each base64
- // chunk consumes (this_chunk / 4) base64 quartets, which means
- // (this_chunk * 3 / 4) source bytes.
- {
- long src_offset = offset * 3 / 4;
- long src_len = this_chunk * 3 / 4;
+ b64_len = base64_encode_buf((char_u *)buf + hdr_len,
+ img->data + offset, this_chunk);
- if (src_offset + src_len > payload_len)
- src_len = payload_len - src_offset;
- if (kitty_b64_append(&ga, img->data + src_offset, src_len) == FAIL)
- goto fail;
- }
-
- if (kitty_ga_concat(&ga, (char_u *)" \") == FAIL)
- goto fail;
+ buf[hdr_len + b64_len] = ' ';
+ buf[hdr_len + b64_len + 1] = '\';
+ buf[hdr_len + b64_len + 2] = NUL;
+ out_str((char_u *)buf);
offset += this_chunk;
}
- if (ga_append(&ga, NUL) == FAIL)
- goto fail;
- return (char_u *)ga.ga_data;
+ out_flush();
+ return OK;
+}
+
+/*
+ * Place the image with the given id, which should have already been
+ * transmitted. Its placement id will always be its image id, so that the image
+ * is moved if it was previously placed.
+ */
+ void
+kitty_place(int id, int row, int col, int src_x, int src_y, int w, int h, int z)
+{
+ vim_snprintf((char *)IObuff, IOSIZE,
+ " _Ga=p,i=%d,p=%d,x=%d,y=%d,w=%d,h=%d,z=%d,q=2 \",
+ kitty_image_id(id), kitty_image_id(id), src_x, src_y, w, h, z);
+
+ term_windgoto(row, col);
+ out_str((char_u *)IObuff);
+ screen_start();
+ setcursor_mayforce(TRUE);
+ out_flush();
+}
+
+/*
+ * Delete image placement with image id "id" (which is also its placement id).
+ * If "del_data" is true, then its data will be freed by the terminal (see
+ *
https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images).
+ */
+ void
+kitty_delete(int id, bool del_data)
+{
+ char d_key = del_data ? 'I' : 'i';
+
+ vim_snprintf((char *)IObuff, IOSIZE,
+ " _Ga=d,d=%c,i=%d,p=%d,q=2 \", d_key, kitty_image_id(id),
+ kitty_image_id(id));
-fail:
- ga_clear(&ga);
- return NULL;
+ out_str((char_u *)IObuff);
+ out_flush();
}
/*
@@ -240,25 +213,4 @@ kitty_probe_parse(char *buf, int n)
return strstr(buf, "_Gi=31;OK") != NULL;
}
-/*
- * Build a kitty "delete image" APC sequence for the placement created
- * by kitty_encode() with the matching `id`. The caller must
- * vim_free() the returned buffer. Returns NULL on OOM or id <= 0.
- *
- * Sequence: ` _Ga=d,i=<id>,q=2 \`
- * a=d -> action: delete
- * i= -> image id (target placement)
- * q=2 -> suppress status reply
- */
- char_u *
-kitty_delete(int id)
-{
- char_u buf[40];
-
- if (id <= 0)
- return NULL;
- vim_snprintf((char *)buf, sizeof(buf), " _Ga=d,i=%d,q=2 \", id);
- return vim_strsave(buf);
-}
-
#endif // FEAT_IMAGE_KITTY || PROTO
diff --git a/src/popupwin.c b/src/popupwin.c
index 07e0b90e2..cbfe72f97 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -119,7 +119,7 @@ static void redraw_overlapped_opacity_popups(int winrow, int wincol,
int height, int width, int leftoff, int zindex);
static void redraw_win_under_opacity_popup(win_T *wp);
#ifdef FEAT_IMAGE_KITTY
-static void popup_image_clear_kitty(win_T *wp);
+static void popup_image_clear_kitty(win_T *wp, bool del_data);
#endif
// GDI and cairo paint the image straight into the window, so the area has to
// be redrawn when the popup goes away. GTK4 keeps a list of images to render
@@ -974,7 +974,7 @@ apply_general_options(win_T *wp, dict_T *dict)
|| wp->w_popup_image_w > 0 || wp->w_popup_image_h > 0)
{
# ifdef FEAT_IMAGE_KITTY
- popup_image_clear_kitty(wp);
+ popup_image_clear_kitty(wp, true);
# endif
# ifdef FEAT_IMAGE_GDK
if (gui.in_use)
@@ -993,7 +993,6 @@ apply_general_options(win_T *wp, dict_T *dict)
wp->w_popup_image_seq_crop_y = 0;
wp->w_popup_image_seq_cells_w = 0;
wp->w_popup_image_seq_cells_h = 0;
- wp->w_popup_image_emit_valid = false;
# endif
# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) || defined(FEAT_IMAGE_GDK)
# ifdef FEAT_GUI
@@ -1054,9 +1053,6 @@ apply_general_options(win_T *wp, dict_T *dict)
# ifdef FEAT_IMAGE_SIXEL
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = -1;
-# endif
-# ifdef FEAT_IMAGE_KITTY
- wp->w_popup_image_emit_valid = false;
# endif
if (wp->w_popup_image_data != NULL)
{
@@ -1771,7 +1767,7 @@ popup_compute_clip(win_T *wp, popup_clip_T *cl)
cl->eff_width = cl->eff_left_extra + w + cl->eff_right_extra;
}
-#ifdef FEAT_IMAGE_SIXEL
+#if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
/*
* Re-encode the popup's sixel image so its pixel rows fit above the bottom of
* the screen. Many sixel-capable terminals scroll the screen when an image
@@ -1896,12 +1892,12 @@ popup_image_backend(void)
return detected;
}
+# ifdef FEAT_IMAGE_SIXEL
static void
popup_encode_image(win_T *wp)
{
image_rgb_T si;
int target_w, target_h;
- int backend;
int cell_x = 8;
int cell_y = 16;
int crop_top_px, crop_bot_px;
@@ -1912,30 +1908,28 @@ popup_encode_image(win_T *wp)
if (wp->w_popup_image_data == NULL
|| wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0)
return;
-# ifdef FEAT_GUI
+# ifdef FEAT_GUI
// The GUI backend renders the image from the device bitmap, not from a
// terminal escape sequence -- skip the encoder entirely in that case.
if (gui.in_use)
return;
-# endif
-
- backend = popup_image_backend();
+# endif
-# if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
+# if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
{
struct cellsize cs;
cs.cs_xpixel = -1;
cs.cs_ypixel = -1;
-# if defined(UNIX) || defined(MSWIN)
+# if defined(UNIX) || defined(MSWIN)
mch_calc_cell_size(&cs);
-# endif
+# endif
if (cs.cs_xpixel > 0)
cell_x = cs.cs_xpixel;
if (cs.cs_ypixel > 0)
cell_y = cs.cs_ypixel;
}
-# endif
+# endif
// For "clipwindow" popups, crop the image to the portion that lies inside
// the host window. popup_compute_clip() turns topoff/bottomoff/leftclip/
@@ -1953,7 +1947,6 @@ popup_encode_image(win_T *wp)
{
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = 0;
- wp->w_popup_image_emit_valid = false;
return;
}
@@ -1981,11 +1974,10 @@ popup_encode_image(win_T *wp)
target_h = host_avail_h;
}
- if (backend != IMAGE_BACKEND_KITTY)
{
// Reserve the bottom-most cell row to keep the sixel image away
// from the edge that triggers scrolling on terminals with sixel-
- // scrolling enabled. Kitty has no such scroll trigger.
+ // scrolling enabled.
int sixel_cells = Rows - 1 - img_top_row;
int sixel_cap = sixel_cells > 0 ? sixel_cells * cell_y : 0;
@@ -2005,19 +1997,17 @@ popup_encode_image(win_T *wp)
{
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = 0;
- wp->w_popup_image_emit_valid = false;
return;
}
+ // already encoded for this geometry
if (wp->w_popup_image_seq != NULL
&& wp->w_popup_image_seq_w == target_w
&& wp->w_popup_image_seq_h == target_h
&& wp->w_popup_image_seq_crop_x == crop_left_px
- && wp->w_popup_image_seq_crop_y == crop_top_px
- && wp->w_popup_image_seq_zindex == wp->w_zindex)
- return; // already encoded for this geometry and zindex
+ && wp->w_popup_image_seq_crop_y == crop_top_px)
+ return;
VIM_CLEAR(wp->w_popup_image_seq);
- wp->w_popup_image_emit_valid = false;
// The sixel/kitty encoders read data tightly packed as width*height
// pixels. When the source row width changes (left or right clipped),
@@ -2052,15 +2042,8 @@ popup_encode_image(win_T *wp)
si.width = target_w;
si.height = target_h;
si.has_alpha = wp->w_popup_image_alpha;
-# ifdef FEAT_IMAGE_KITTY
- if (backend == IMAGE_BACKEND_KITTY)
- // Use the popup's window-id as the kitty image id so that
- // popup_image_clear_kitty() can target the placement when the
- // popup is later hidden or closed.
- wp->w_popup_image_seq = kitty_encode(&si, wp->w_id, wp->w_zindex);
- else
-# endif
- wp->w_popup_image_seq = sixel_encode(&si);
+
+ wp->w_popup_image_seq = sixel_encode(&si);
vim_free(crop_buf);
@@ -2072,7 +2055,6 @@ popup_encode_image(win_T *wp)
wp->w_popup_image_seq_crop_y = crop_top_px;
wp->w_popup_image_seq_cells_w = (target_w + cell_x - 1) / cell_x;
wp->w_popup_image_seq_cells_h = (target_h + cell_y - 1) / cell_y;
- wp->w_popup_image_seq_zindex = wp->w_zindex;
}
else
{
@@ -2081,7 +2063,8 @@ popup_encode_image(win_T *wp)
wp->w_popup_image_seq_cells_h = 0;
}
}
-#endif
+# endif // FEAT_IMAGE_SIXEL
+#endif // FEAT_IMAGE_SIXEL || FEAT_IMAGE_KITTY
#ifdef FEAT_IMAGE
/*
@@ -2420,7 +2403,7 @@ popup_adjust_position(win_T *wp)
#ifdef FEAT_IMAGE_KITTY
// Kitty placements need to be deleted explicitly before
// the popup goes hidden -- see popup_hide().
- popup_image_clear_kitty(wp);
+ popup_image_clear_kitty(wp, false);
#endif
#ifdef FEAT_IMAGE_GDK
if (gui.in_use)
@@ -2962,7 +2945,7 @@ popup_adjust_position(win_T *wp)
{
#ifdef FEAT_IMAGE_KITTY
// delete the kitty placement before hiding, like popup_hide()
- popup_image_clear_kitty(wp);
+ popup_image_clear_kitty(wp, false);
#endif
#ifdef FEAT_IMAGE_GDK
if (gui.in_use)
@@ -4344,7 +4327,7 @@ popup_hide(win_T *wp)
// Sixel pixels disappear when the cells underneath are redrawn, but
// a kitty placement persists until explicitly deleted -- send the
// delete APC before hiding so the image goes away with the popup.
- popup_image_clear_kitty(wp);
+ popup_image_clear_kitty(wp, false);
#endif
#ifdef FEAT_IMAGE_GDK
if (gui.in_use)
@@ -4556,7 +4539,7 @@ popup_free(win_T *wp)
#ifdef FEAT_IMAGE_KITTY
// Remove the kitty placement before win_free_popup() invalidates wp.
- popup_image_clear_kitty(wp);
+ popup_image_clear_kitty(wp, true);
#endif
#ifdef FEAT_IMAGE_GDK
if (gui.in_use)
@@ -6940,7 +6923,7 @@ popup_image_gui_clip(
|| defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) \
|| defined(FEAT_IMAGE_GDK)
static void
-popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl)
+popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl UNUSED)
{
int old_row, old_col, old_cells_w, old_cells_h;
int new_row = 0, new_col = 0, new_cells_w = 0, new_cells_h = 0;
@@ -6978,7 +6961,7 @@ popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl)
new_cells_h = (draw_h + cell_y - 1) / cell_y;
}
# endif
-# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
+# if defined(FEAT_IMAGE_SIXEL)
# if defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)) \
|| defined(FEAT_IMAGE_GDK)
else
@@ -7066,6 +7049,7 @@ popup_emit_image(win_T *wp)
// leaving the image stuck on screen until the cell is overwritten.
if (wp->w_popup_flags & POPF_HIDDEN)
return;
+
row = wp->w_winrow + wp->w_popup_border[0] + wp->w_popup_padding[0];
col = wp->w_wincol + wp->w_popup_border[3] + wp->w_popup_padding[3];
@@ -7097,14 +7081,92 @@ popup_emit_image(win_T *wp)
return;
}
# endif
-# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
-# ifdef FEAT_GUI
+# ifdef FEAT_GUI
// GUI builds without a GUI image backend (e.g. Motif) reach here when
// gui.in_use is true; emitting sixel/kitty escape sequences via out_str()
// would print them as raw text on the GUI canvas, so bail out.
if (gui.in_use)
return;
+# endif
+# ifdef FEAT_IMAGE_KITTY
+ if (popup_image_backend() == IMAGE_BACKEND_KITTY)
+ {
+ popup_clip_T cl;
+ int cell_x = 8;
+ int cell_y = 16;
+ int visible_w;
+ int visible_h;
+ int src_x, src_y, w, h;
+
+ if (row < 0 || col < 0)
+ return;
+
+# if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
+ {
+ struct cellsize cs;
+
+ cs.cs_xpixel = -1;
+ cs.cs_ypixel = -1;
+# if defined(UNIX) || defined(MSWIN)
+ mch_calc_cell_size(&cs);
+# endif
+ if (cs.cs_xpixel > 0)
+ cell_x = cs.cs_xpixel;
+ if (cs.cs_ypixel > 0)
+ cell_y = cs.cs_ypixel;
+ }
# endif
+ popup_compute_clip(wp, &cl);
+
+ visible_w = wp->w_width - cl.clip_left_content - cl.clip_right_content;
+ visible_h = wp->w_height - cl.clip_top_content - cl.clip_bot_content;
+
+ if (visible_w <= 0 || visible_h <= 0)
+ return;
+
+ row += cl.clip_top_content;
+ col += cl.clip_left_content;
+
+ src_x = cl.clip_left_content * cell_x;
+ src_y = cl.clip_top_content * cell_y;
+ w = wp->w_popup_image_w - src_x - cl.clip_right_content * cell_x;
+ h = wp->w_popup_image_h - src_y - cl.clip_bot_content * cell_y;
+
+ // Clamp to the popup's actual visible cell box in pixels, so the
+ // crop can never claim more cells than the popup has
+ if (w > visible_w * cell_x)
+ w = visible_w * cell_x;
+ if (h > visible_h * cell_y)
+ h = visible_h * cell_y;
+
+ if (w <= 0 || h <= 0)
+ return;
+
+ // Transmit the image to the terminal if it hasn't already
+ if (!wp->w_popup_image_transmit)
+ {
+ image_rgb_T si;
+
+ si.data = wp->w_popup_image_data;
+ si.width = wp->w_popup_image_w;
+ si.height = wp->w_popup_image_h;
+ si.has_alpha = wp->w_popup_image_alpha;
+
+ if (kitty_transmit(&si, wp->w_id) == FAIL)
+ return;
+ wp->w_popup_image_transmit = true;
+ }
+ kitty_place(wp->w_id, row, col, src_x, src_y, w, h, wp->w_zindex);
+
+ wp->w_popup_image_emit_row = row;
+ wp->w_popup_image_emit_col = col;
+ wp->w_popup_image_emit_cells_w = (w + cell_x - 1) / cell_x;
+ wp->w_popup_image_emit_cells_h = (h + cell_y - 1) / cell_y;
+ wp->w_popup_image_px_dirty = false;
+ return;
+ }
+# endif
+# ifdef FEAT_IMAGE_SIXEL
if (wp->w_popup_image_seq == NULL)
return;
// For "clipwindow" popups the encoded sequence already covers only the
@@ -7120,18 +7182,6 @@ popup_emit_image(win_T *wp)
}
if (row < 0 || col < 0)
return;
-# ifdef FEAT_IMAGE_KITTY
- // A kitty placement persists on the terminal and is drawn above the
- // text layer, so when it is already showing at this position there is
- // nothing to repair: skip the (potentially multi-MB) retransmission.
- // The flag is reset when the image is re-encoded, the placement is
- // deleted, or the terminal screen is cleared.
- if (popup_image_backend() == IMAGE_BACKEND_KITTY
- && wp->w_popup_image_emit_valid
- && wp->w_popup_image_emit_row == row
- && wp->w_popup_image_emit_col == col)
- return;
-# endif
// Hide the cursor across the move + image emit, then restore it to
// the current text-cursor position before showing it; otherwise the
// cursor can briefly flicker below its scrolled-to position because
@@ -7153,33 +7203,28 @@ popup_emit_image(win_T *wp)
// including cells that a higher zindex popup draws on top of this image.
// Invalidate those cells in ScreenLines so the higher popup's draw,
// later in this same update_popups() walk, actually rewrites them to
- // the terminal instead of skipping them as unchanged. Not needed for
- // kitty, where the placement is layered by its z= value instead.
-# ifdef FEAT_IMAGE_KITTY
- if (popup_image_backend() != IMAGE_BACKEND_KITTY)
-# endif
+ // the terminal instead of skipping them as unchanged.
+
+ for (int rr = row; rr < row + wp->w_popup_image_seq_cells_h; ++rr)
{
- for (int rr = row; rr < row + wp->w_popup_image_seq_cells_h; ++rr)
- {
- if (rr < 0 || rr >= screen_Rows)
- continue;
+ if (rr < 0 || rr >= screen_Rows)
+ continue;
- int off_base = LineOffset[rr];
+ int off_base = LineOffset[rr];
- for (int cc = col; cc < col + wp->w_popup_image_seq_cells_w; ++cc)
- {
- if (cc < 0 || cc >= screen_Columns)
- continue;
- if (popup_mask[rr * screen_Columns + cc] <= wp->w_zindex)
- continue;
+ for (int cc = col; cc < col + wp->w_popup_image_seq_cells_w; ++cc)
+ {
+ if (cc < 0 || cc >= screen_Columns)
+ continue;
+ if (popup_mask[rr * screen_Columns + cc] <= wp->w_zindex)
+ continue;
- int off = off_base + cc;
+ int off = off_base + cc;
- ScreenLines[off] = ' ';
- if (enc_utf8 && ScreenLinesUC != NULL)
- ScreenLinesUC[off] = 0;
- ScreenAttrs[off] = -1;
- }
+ ScreenLines[off] = ' ';
+ if (enc_utf8 && ScreenLinesUC != NULL)
+ ScreenLinesUC[off] = 0;
+ ScreenAttrs[off] = -1;
}
}
@@ -7192,39 +7237,32 @@ popup_emit_image(win_T *wp)
wp->w_popup_image_emit_col = col;
wp->w_popup_image_emit_cells_w = wp->w_popup_image_seq_cells_w;
wp->w_popup_image_emit_cells_h = wp->w_popup_image_seq_cells_h;
- wp->w_popup_image_emit_valid = true;
wp->w_popup_image_px_dirty = false;
# endif
}
# ifdef FEAT_IMAGE_KITTY
/*
- * Send a kitty `a=d,i=<id>` APC to remove the placement made for "wp"
- * by an earlier kitty_encode(). Called when the popup goes away (via
- * popup_hide / popup_close / textprop scrolling out of view), because
- * unlike sixel pixels -- which the next text overwrite clears -- kitty
- * placements persist until explicitly deleted.
+ * Remove the kitty image placement for "wp". Called when the popup goes
+ * away.
+ * When "del_data" is true the terminal also frees the transmitted image.
*/
static void
-popup_image_clear_kitty(win_T *wp)
+popup_image_clear_kitty(win_T *wp, bool del_data)
{
- char_u *seq;
-
# ifdef FEAT_GUI
if (gui.in_use)
return;
# endif
+
if (wp == NULL || wp->w_popup_image_data == NULL || wp->w_id <= 0)
return;
+
if (popup_image_backend() != IMAGE_BACKEND_KITTY)
return;
- seq = kitty_delete(wp->w_id);
- if (seq == NULL)
- return;
- out_str(seq);
- out_flush();
- vim_free(seq);
- wp->w_popup_image_emit_valid = false;
+ kitty_delete(wp->w_id, del_data);
+ if (del_data)
+ wp->w_popup_image_transmit = false;
}
# endif
@@ -7251,27 +7289,6 @@ popup_image_clear_gui(win_T *wp)
}
# endif
-# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
-/*
- * Called after the terminal screen has been cleared: kitty deletes
- * placements that intersect the erased area, so the cached "already on
- * screen" state no longer holds and the next popup_emit_image() must
- * retransmit.
- */
- void
-popup_images_invalidate(void)
-{
- win_T *wp;
- tabpage_T *tp;
-
- FOR_ALL_POPUPWINS(wp)
- wp->w_popup_image_emit_valid = false;
- FOR_ALL_TABPAGES(tp)
- FOR_ALL_POPUPWINS_IN_TAB(tp, wp)
- wp->w_popup_image_emit_valid = false;
-}
-# endif
-
/*
* Re-paint every popup's image after the rest of the screen update has
* settled. Only needed for the GUI, where the cursor redraw and other
diff --git a/src/proto/
kitty.pro b/src/proto/
kitty.pro
index 8c2970c7d..001e79c31 100644
--- a/src/proto/
kitty.pro
+++ b/src/proto/
kitty.pro
@@ -1,5 +1,6 @@
/* kitty.c */
-char_u *kitty_encode(image_rgb_T *img, int id, int zindex);
+int kitty_transmit(image_rgb_T *img, int id);
+void kitty_place(int id, int row, int col, int src_x, int src_y, int w, int h, int z);
+void kitty_delete(int id, bool del_data);
int kitty_probe_parse(char *buf, int n);
-char_u *kitty_delete(int id);
/* vim: set ft=c : */
diff --git a/src/proto/
popupwin.pro b/src/proto/
popupwin.pro
index 758695c35..f29aef9d0 100644
--- a/src/proto/
popupwin.pro
+++ b/src/proto/
popupwin.pro
@@ -60,7 +60,6 @@ void may_update_popup_mask(int type);
void may_update_popup_position(void);
int popup_get_base_screen_cell(int row, int col, schar_T *linep, int *attrp, u8char_T *ucp);
void popup_set_base_screen_cell(int row, int col, schar_T line, int attr, u8char_T uc);
-void popup_images_invalidate(void);
void update_popup_images(void);
void update_popup_images_rect(int left, int top, int right, int bottom);
void update_popups(void (*win_update)(win_T *wp));
diff --git a/src/screen.c b/src/screen.c
index b4cf42ee6..662bc0ed7 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -3636,11 +3636,6 @@ screenclear2(int doclear)
if (suppressed_cells != NULL)
vim_memset(suppressed_cells, 0,
(size_t)suppressed_rows * suppressed_cols);
-#endif
-#if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
- // Clearing the display removes kitty image placements; force the
- // next redraw to retransmit popup images.
- popup_images_invalidate();
#endif
}
else
diff --git a/src/structs.h b/src/structs.h
index fdba4ee83..d41cd182b 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4277,7 +4277,7 @@ struct window_S
// visible under the new frame's transparent pixels.
bool w_popup_image_px_dirty;
# ifdef FEAT_IMAGE_SIXEL
- char_u *w_popup_image_seq; // cached sixel DCS sequence (terminal)
+ char_u *w_popup_image_seq; // cached sixel DCS sequence
int w_popup_image_seq_w; // pixel width of cached seq
int w_popup_image_seq_h; // pixel height used for cached seq;
// -1 means cache is invalid
@@ -4285,10 +4285,10 @@ struct window_S
int w_popup_image_seq_crop_y; // pixel offset (top) into source
int w_popup_image_seq_cells_w; // cell width spanning seq pixels
int w_popup_image_seq_cells_h; // cell height spanning seq pixels
- int w_popup_image_seq_zindex; // zindex encoded into seq (kitty z=)
- bool w_popup_image_emit_valid; // true while the kitty placement
- // emitted at w_popup_image_emit_*
- // is still on the terminal
+# endif
+# ifdef FEAT_IMAGE_KITTY
+ bool w_popup_image_transmit; // If image has been transmitted to
+ // terminal
# endif
# ifdef FEAT_IMAGE_GDI
// Pre-built Windows GUI image cache. The bitmap is a 32-bit top-down
diff --git a/src/version.c b/src/version.c
index 2033abfa4..a5c38c915 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 962,
/**/
961,
/**/