Add an image attribute to popup_create() / popup_setoptions() that renders a pixel buffer inside the popup window. Terminal backends emit the buffer as DEC sixel DCS sequences (FEAT_IMAGE_SIXEL); on the MS-Windows GUI the same data is BitBlt'd through a GDI device bitmap (FEAT_IMAGE_GDI). The popup auto-sizes its cell box from the image's pixel dimensions, so the caller does not have to compute minwidth/maxwidth/minheight/maxheight by hand.
Vim itself does not link against libpng, libjpeg, libwebp or any image-decoding library. The image attribute takes an already-decoded raw RGB / RGBA pixel buffer; format decoding is left to the script caller, who can pipe the file through any external tool (GraphicsMagick, ImageMagick, ffmpeg, custom converter, ...) and pass the resulting bytes via a Blob. This keeps Vim's build footprint unchanged while letting users render PNG, JPEG, GIF, WebP, AVIF or proprietary formats with whatever decoder they already have.
The image dict accepts data (a Blob of width*height*3 for RGB or width*height*4 for RGBA), width and height. RGBA buffers are composited over the popup's background. A companion script function getbgcolor() returns the current background colour as [r, g, b] (from the terminal's OSC 11 response, or the GUI's Normal highlight) so scripts that want to pre-composite their pixels can match the popup's actual backdrop -- handy when generating an image with anti-aliased edges that need to blend cleanly into the editor.
Minimal RGB example — a 4×4 red square popped at the cursor:
let pixels = repeat([0xff, 0x00, 0x00], 4 * 4)->list2blob() call popup_create('', #{ \ image: #{ data: pixels, width: 4, height: 4 }, \ line: 'cursor+1', col: 'cursor', \ })
Real-world example — pipe a PNG through GraphicsMagick and pop the resulting raw RGB bytes:
let png = 'cat.png' let dim = split(system('gm identify -format "%w %h" ' .. shellescape(png))) let [w, h] = [str2nr(dim[0]), str2nr(dim[1])] call system('gm convert ' .. shellescape(png) .. ' -depth 8 rgb:/tmp/cat.rgb') let blob = readblob('/tmp/cat.rgb') call popup_create('', #{ \ image: #{ data: blob, width: w, height: h }, \ line: 1, col: 1, border: [], padding: [0, 0, 0, 0], \ })
Sixel encoding is deferred to popup_adjust_position() so the encoder knows the final winrow and can crop the bottom row of the image to one cell above the screen edge, avoiding sixel-induced terminal scrolling. popup_getoptions() returns the same dict back -- data is a fresh blob copy independent of the popup's internal buffer.
Other refinements:
has('image'), has('image_sixel'), has('image_gdi') are registered as known feature names so the test framework's CheckFeature works and has('image') is no longer a runtime no-op.Tests in test_popupwin.vim cover image acceptance for both RGB and RGBA, blob round-trip through popup_getoptions, the validation paths (wrong data length / missing keys), and the existing dimension-update case (whose blob lengths were corrected -- the previous test silently no-op'd because has('image') returned 0).
With this in place, Vim's expressive range grows considerably: anything that can be rendered to an RGB/RGBA buffer can now sit next to your text, anchored to a textprop and tracking the buffer as it scrolls. A few demos of what this enables straight from a markdown buffer:
PlantUML diagram preview, anchored to a fenced ```plantuml block:
https://github.com/user-attachments/assets/8c6e3973-ff16-4605-91be-51bfe77e4308
LaTeX math rendered through the CodeCogs API and pinned next to the formula:
image.png (view on web)QR code generated for the URL under the cursor, ready to scan with a phone:
https://github.com/user-attachments/assets/352c8398-78b0-4331-8aca-a8d6c010b85e
And -- because the popup just consumes a raw pixel buffer -- you can generate each frame yourself in Vim script and feed it back via popup_setoptions() on a timer. That alone is enough to build an xeyes-style dynamic application, drawn pixel by pixel and updated in place, running entirely inside Vim.
https://github.com/user-attachments/assets/de5ca08a-b0f0-47db-9ac3-ad9ae9d7c89a
https://github.com/vim/vim/pull/20136
(24 files)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Wow, this is amazing work
do you plan on supporting the kitty graphics protocol? Thanks
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 2 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman Now supported kitty renderer.
https://github.com/user-attachments/assets/fe2b6354-d13c-4b78-883d-aecaee368cef
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I just took a quick look at the commits, but it seems that detection for the kitty graphics protocol is done via environment variables. However support for it can be queried from the terminal: https://sw.kovidgoyal.net/kitty/graphics-protocol/#querying-support-and-available-transmission-mediums. Not sure if that is feasible though
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Now supported cairo renderer (gtk3)
image.png (view on web)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 28 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman commented on this pull request.
In src/os_unix.c:
> @@ -4527,6 +4527,76 @@ mch_get_shellsize(void)
return OK;
}
+/*
+ * Synchronously ask the terminal for its window pixel dimensions via the
+ * xterm CSI 14 t query and parse the CSI 4 ; H ; W t response. Returns OK
+ * and fills *win_w and *win_h on success. Returns FAIL on any error
+ * (non-tty, no response within ~200ms, malformed reply).
+ */
+ static int
+query_terminal_pixel_size(int *win_w, int *win_h)
Is it possible to do this asynchronously? Although I'm guessing it will be very complicated
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + } + buf[n] = 0; + tcsetattr(fd_in, TCSANOW, &old_t); + + // expected: ESC [ 4 ; H ; W t + p = (char *)vim_strchr((char_u *)buf, '\033'); + if (p == NULL || p[1] != '[' || p[2] != '4' || p[3] != ';') + return FAIL; + p += 4; + semi = (char *)vim_strchr((char_u *)p, ';'); + if (semi == NULL) + return FAIL; + end = (char *)vim_strchr((char_u *)semi, 't'); + if (end == NULL) + return FAIL; + hpx = atoi(p);
maybe use strtol instead of atoi?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman commented on this pull request.
In src/popupwin.c:
> + return FALSE;
+ new_t = old_t;
+ new_t.c_lflag &= ~(ICANON | ECHO);
+ new_t.c_cc[VMIN] = 0;
+ new_t.c_cc[VTIME] = 3; // 300ms grace per read()
+ if (tcsetattr(fd_in, TCSANOW, &new_t) != 0)
+ return FALSE;
+
+ if (write(fd_out, query, sizeof(query) - 1) != sizeof(query) - 1)
+ {
+ tcsetattr(fd_in, TCSANOW, &old_t);
+ return FALSE;
+ }
+
+ // Read until the DA1 reply (`\e[?...c`) lands or we time out.
+ while (n < (int)sizeof(buf) - 1)
Is it possible to check for the response later when it is received, and during the meantime just make it so that kitty graphics protocol isn't available?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
> + return ok;
+}
+# endif
+
+/*
+ * Pick a terminal image backend. Cached on first call. Returns
+ * IMAGE_BACKEND_KITTY when the host terminal advertises kitty graphics
+ * support, otherwise IMAGE_BACKEND_SIXEL.
+ *
+ * Detection order:
+ * 1. $VIM_IMAGE_BACKEND override ("kitty" | "sixel")
+ * 2. $KITTY_WINDOW_ID (kitty itself)
+ * 3. $GHOSTTY_BIN_DIR / $GHOSTTY_RESOURCES_DIR (Ghostty)
+ * 4. $WEZTERM_EXECUTABLE / $WEZTERM_PANE (WezTerm)
+ * 5. $TERM contains "kitty" / "ghostty" / "wezterm"
+ * 6. $TERM_PROGRAM equals one of the above
I don't think detecting backends based on environment variables is a good idea. Either we can add an option like 'keyprotocol' or just rely on querying the terminal instead. It also seems like Konsole supports the kitty graphics protocol as well.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn commented on this pull request.
In src/os_unix.c:
> @@ -4527,6 +4527,76 @@ mch_get_shellsize(void)
return OK;
}
+/*
+ * Synchronously ask the terminal for its window pixel dimensions via the
+ * xterm CSI 14 t query and parse the CSI 4 ; H ; W t response. Returns OK
+ * and fills *win_w and *win_h on success. Returns FAIL on any error
+ * (non-tty, no response within ~200ms, malformed reply).
+ */
+ static int
+query_terminal_pixel_size(int *win_w, int *win_h)
The query is lazy and cached — it only runs the first time something asks for cell pixel size, and the 200 ms timeout bounds the worst case, so it's a one-time cost on the first image popup.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn commented on this pull request.
In src/popupwin.c:
> + return FALSE;
+ new_t = old_t;
+ new_t.c_lflag &= ~(ICANON | ECHO);
+ new_t.c_cc[VMIN] = 0;
+ new_t.c_cc[VTIME] = 3; // 300ms grace per read()
+ if (tcsetattr(fd_in, TCSANOW, &new_t) != 0)
+ return FALSE;
+
+ if (write(fd_out, query, sizeof(query) - 1) != sizeof(query) - 1)
+ {
+ tcsetattr(fd_in, TCSANOW, &old_t);
+ return FALSE;
+ }
+
+ // Read until the DA1 reply (`\e[?...c`) lands or we time out.
+ while (n < (int)sizeof(buf) - 1)
Same answer as the CSI 14 t one — the probe is lazy and one-shot per session, cached on first call to popup_image_backend(). Async would be a worthwhile follow-up if the cost actually shows up.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 19 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 20 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 5 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
Now popup image support clipwindow
https://github.com/user-attachments/assets/2dbe7168-52de-41ff-9a25-d269b1d2f7e5
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@github-advanced-security[bot] commented on this pull request.
In src/popupwin.c:
> +
+ VIM_CLEAR(wp->w_popup_image_seq);
+
+ // The sixel/kitty encoders read data tightly packed as width*height
+ // pixels. When the source row width changes (left or right clipped),
+ // or when rows must be skipped from the top, build a contiguous cropped
+ // buffer. A pure bottom crop (target_h reduced, full source width) can
+ // reuse the source buffer as-is.
+ if (crop_left_px > 0 || crop_right_px > 0 || crop_top_px > 0)
+ {
+ int bpp = wp->w_popup_image_alpha ? 4 : 3;
+ int src_stride = wp->w_popup_image_w * bpp;
+ int dst_stride = target_w * bpp;
+ int y;
+
+ crop_buf = alloc(target_h * dst_stride);
Multiplication result may overflow 'int' before it is converted to 'size_t'.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 25 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 29 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 29 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@chrisbra commented on this pull request.
In src/testdir/test_popupwin.vim:
> @@ -5482,4 +5482,124 @@ func Test_popupwin_close_copen_redraw()
delfunc s:PopupFilter
endfunc
+func Test_popup_image_update()
+ if !has('image')
⬇️ Suggested change
- if !has('image')
+ CheckFeature image
In src/cairo.c:
> + p[x] = (0xffu << 24) + | ((unsigned int)r << 16) + | ((unsigned int)g << 8) + | (unsigned int)b; + } + } + } + cairo_surface_mark_dirty(surf); +} + +/* + * Build the cached cairo_image_surface_t for "wp" from its RGB / RGBA + * pixel buffer. Existing cache is freed first. Returns TRUE on + * success, FALSE on bad input or out-of-memory. + */ + int
bool?
In src/cairo.c:
> + if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS)
+ {
+ cairo_surface_destroy(surf);
+ return FALSE;
+ }
+ cairo_popup_image_fill_surface(wp, surf);
+ wp->w_popup_image_surface = surf;
+ return TRUE;
+}
+
+/*
+ * Same-size pixel swap: refill the existing cached surface in place
+ * with new bytes from wp->w_popup_image_data. Returns FALSE when
+ * there is no cache yet (caller should fall back to a full rebuild).
+ */
+ int
bool?
In src/evalfunc.c:
> + *
+ * Returns a list [r, g, b] (0..255) describing the current background
+ * colour: in GUI mode the "Normal" highlight group's bg, in terminal
+ * mode the value reported by the OSC 11 response (see v:termrbgresp).
+ * Returns an empty list when no value is available, e.g. before the
+ * terminal has answered the query, or when "Normal" has no bg colour.
+ */
+ static void
+f_getbgcolor(typval_T *argvars UNUSED, typval_T *rettv)
+{
+ if (rettv_list_alloc(rettv) == FAIL)
+ return;
+
+#if defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS)
+ {
+ int try_hl = FALSE;
bool
In src/gui_w32.c:
> @@ -2728,6 +2732,207 @@ gui_mch_clear_block(
clear_rect(&rc);
}
+#ifdef FEAT_IMAGE_GDI
+/*
+ * Convert the popup's RGB(A) pixel buffer into a 32-bit BGRX buffer.
+ * "dst" must be large enough for iw * ih * 4 bytes. When has_alpha is
+ * TRUE, the source has 4 bytes per pixel and the alpha channel is
+ * flattened onto the GUI window background colour (pre-multiplied blend).
+ */
+ static int
bool?
In src/gui_w32.c:
> + d[1] = s[1]; // G + d[2] = s[0]; // R + } + d[3] = 0; + s += src_bpp; + d += 4; + } + } + return TRUE; +} + +/* + * Build a top-down 32-bit DIB section for the popup's RGB buffer and cache + * both the bitmap and a memory DC on the window for fast BitBlt redraws. + */ + static int
bool?
In src/gui_w32.c:
> +gui_mch_free_popup_image(win_T *wp)
+{
+ if (wp->w_popup_image_hdc != NULL)
+ {
+ DeleteDC((HDC)wp->w_popup_image_hdc);
+ wp->w_popup_image_hdc = NULL;
+ }
+ if (wp->w_popup_image_hbitmap != NULL)
+ {
+ DeleteObject((HBITMAP)wp->w_popup_image_hbitmap);
+ wp->w_popup_image_hbitmap = NULL;
+ }
+ wp->w_popup_image_bits = NULL;
+}
+
+ int
same here
In src/os_mswin.c:
> @@ -579,6 +579,14 @@ mch_new_shellsize(void)
// never used
}
+ void
+mch_calc_cell_size(struct cellsize *cs_out)
+{
+ // never used
+ cs_out->cs_xpixel = -1;
+ cs_out->cs_ypixel = -1;
+}
+
void mch_calc_cell_size(struct cellsize *cs_out UNUSED) { // never used }
In src/os_win32.c:
> + if (restored) + SetConsoleMode(hIn, mode_in_old); + + // expected: ESC [ 4 ; H ; W t + p = (char *)vim_strchr((char_u *)buf, '\033'); + if (p == NULL || p[1] != '[' || p[2] != '4' || p[3] != ';') + return FAIL; + p += 4; + semi = (char *)vim_strchr((char_u *)p, ';'); + if (semi == NULL) + return FAIL; + end = (char *)vim_strchr((char_u *)semi, 't'); + if (end == NULL) + return FAIL; + hpx = atoi(p); + wpx = atoi(semi + 1);
Can you use vim_str2nr() here ?
In src/popupwin.c:
> + * + * Returns TRUE on a positive `OK` response. Network/permission errors + * and timeouts are treated as "not supported". + * + * [1] https://sw.kovidgoyal.net/kitty/graphics-protocol/ + * #querying-support-and-available-transmission-mediums + */ + static int +popup_kitty_probe(void) +{ + struct termios old_t, new_t; + int fd_in = read_cmd_fd; + int fd_out = 1; + char buf[256]; + int n = 0; + int ok = FALSE;
drop it
In src/popupwin.c:
> + * ^^^^ kitty query ^^^^ ^^^^ DA1 sentinel + * + * The kitty spec [1] says the terminal will respond with + * `\e_Gi=31;OK\e\\` (or an `_Gi=31;<error>\e\\`) + * to the kitty query, followed by its own `\e[?...c` answer to DA1. + * We send DA1 as a sentinel: any conforming terminal must reply, so + * we have a guaranteed point at which to stop reading; if the kitty + * reply did not arrive before then, kitty is not supported. + * + * Returns TRUE on a positive `OK` response. Network/permission errors + * and timeouts are treated as "not supported". + * + * [1] https://sw.kovidgoyal.net/kitty/graphics-protocol/ + * #querying-support-and-available-transmission-mediums + */ + static int
bool
In src/popupwin.c:
> + if (c == 'c' && n >= 3 && buf[n - 2] != '\033')
+ {
+ int i;
+ for (i = n - 2; i > 0; --i)
+ if (buf[i] == '?' && buf[i - 1] == '[')
+ break;
+ if (i > 0)
+ break;
+ }
+ }
+ buf[n] = NUL;
+ tcsetattr(fd_in, TCSANOW, &old_t);
+
+ // A positive kitty reply contains the literal "_Gi=31;OK".
+ if (strstr(buf, "_Gi=31;OK") != NULL)
+ ok = TRUE;
return true;
In src/sixel.c:
> + *npal_out = used; + *idx_out = idx; + return OK; + +too_many: + return FAIL; +} + +/* + * Fallback for RGBA when the unique-colour count exceeds the dynamic + * palette: quantize to the same fixed 6x6x6 + 24-grayscale palette as + * rgb_to_paletted_fixed, but assign palette index 0 to alpha==0 pixels. + * Always succeeds; produces 240 palette entries. + */ + static int +rgba_to_paletted_fixed(
can this be merged with rgb_to_paletted_fixed() ?
In src/popupwin.c:
> +
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+ if (gui.in_use)
+ {
+ int src_x, src_y, draw_w, draw_h;
+
+ popup_image_gui_clip(wp, &row, &col,
+ &src_x, &src_y, &draw_w, &draw_h);
+ if (row < 0 || col < 0 || draw_w <= 0 || draw_h <= 0)
+ return;
+ gui_mch_draw_popup_image(wp, row, col,
+ src_x, src_y, draw_w, draw_h);
+ return;
+ }
+# endif
+# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
do we need the if (gui.in_use) here as well? I am thinking of e.g. the Motif Gui.
In src/popupwin.c:
> + cy = cs.cs_ypixel; + } + } + cw = (iw + cx - 1) / cx; + ch = (ih + cy - 1) / cy; + wp->w_minwidth = cw; + wp->w_maxwidth = cw; + wp->w_minheight = ch; + wp->w_maxheight = ch; + } +# endif + } + } + } + else + semsg(_(e_invalid_value_for_argument_str_str), "image",
according to the docs, an empty image key should clear the image.
In src/testdir/test_functions.vim:
> @@ -4384,7 +4387,7 @@ func Test_getcellpixels_for_unix()
call term_sendkeys(buf, ":redi END\<CR>")
call term_sendkeys(buf, "P")
- call WaitForAssert({-> assert_match("\[\d+, \d+\]", term_getline(buf, 3))}, 1000)
+ call WaitForAssert({-> assert_match('\[\(\d\+,\s*\d\+\)\?\]', term_getline(buf, 3))}, 1000)
Hm, this allows now to return an empty list, doesn't this brake getcellpixels()?
In src/testdir/test_functions.vim:
> @@ -4371,7 +4371,10 @@ endfunc
I am missing a test for getbgcolor()
In src/popupwin.c:
> + cs.cs_ypixel = -1;
+# if defined(UNIX) || defined(MSWIN)
+ mch_calc_cell_size(&cs);
+# endif
+ if (cs.cs_xpixel > 0 && cs.cs_ypixel > 0)
+ {
+ cx = cs.cs_xpixel;
+ cy = cs.cs_ypixel;
+ }
+ }
+ cw = (iw + cx - 1) / cx;
+ ch = (ih + cy - 1) / cy;
+ wp->w_minwidth = cw;
+ wp->w_maxwidth = cw;
+ wp->w_minheight = ch;
+ wp->w_maxheight = ch;
If I read this correctly, this overwrites user specified minwidth/maxwidth/minheight/maxheight values?
In src/popupwin.c:
> + // (the image will be re-blitted on top by popup_emit_image),
+ // so a UPD_VALID redraw is enough -- no need to repaint every
+ // cell under the image like UPD_NOT_VALID would.
+ int same_size_update = (wp->w_popup_image_data != NULL
+ && wp->w_popup_image_w == iw
+ && wp->w_popup_image_h == ih
+ && wp->w_popup_image_alpha == has_alpha);
+
+ if (same_size_update)
+ mch_memmove(wp->w_popup_image_data, b->bv_ga.ga_data,
+ (size_t)blen);
+ else
+ {
+ VIM_CLEAR(wp->w_popup_image_data);
+ wp->w_popup_image_data = vim_memsave(b->bv_ga.ga_data,
+ (size_t)blen);
this could return NULL, so we need to check the that w_popup_image_data is not NULL.
In src/popupwin.c:
> @@ -6595,6 +7351,18 @@ update_popups(void (*win_update)(win_T *wp)) // Undo the topoff shift applied before drawing the borders so the // next iteration sees the popup's logical winrow. wp->w_winrow -= wp->w_popup_topoff; + +#ifdef FEAT_IMAGE + // Emit the popup image after the topoff shift is undone so + // popup_emit_image() sees the popup's logical winrow. Otherwise the + // clip-top adjustment overshoots by topoff and the image lands below + // its correct row, on top of (or beside) the late re-emit done from + // update_popup_images(). +# ifdef FEAT_GUI + if (!gui.in_use) +# endif + popup_emit_image(wp);
Won't this draw the popup images twice? once from here and once from update_popup_images()?
In src/popupwin.c:
> + {
+#ifdef FEAT_IMAGE_KITTY
+ // Kitty placements need to be deleted explicitly before
+ // the popup goes hidden -- see popup_hide().
+ popup_image_clear_kitty(wp);
+#endif
+ popup_hide_for_textprop(wp);
+ // Force a full redraw of every window plus a popup mask
+ // rebuild so the cells previously covered by the popup
+ // are re-emitted. Without this, screen_line() may skip
+ // cells whose ScreenLines content is unchanged, leaving
+ // stale drawing on the terminal -- in particular sixel
+ // image pixels persist until the cell is rewritten.
+ if (wp->w_winrow + popup_height(wp) >= cmdline_row)
+ clear_cmdline = TRUE;
+ redraw_all_later(UPD_NOT_VALID);
should those be done conditionally for image popups only like this:
if (wp->w_popup_image_data != NULL) { redraw_all_later(UPD_NOT_VALID); status_redraw_all(); popup_mask_refresh = TRUE; }
In src/sixel.c:
> +typedef struct
+{
+ unsigned int *keys; // hash keys for dynamic palette lookup
+ char_u *vals; // hash values for dynamic palette lookup
+ int hash_cap; // allocated slots in keys/vals
+ char_u *idx; // width * height paletted image
+ size_t idx_len; // allocated bytes in idx
+ char_u *pal; // dynamic palette storage (RGB triples)
+ int pal_len; // allocated bytes in pal
+ sixel_band_T band; // reusable band scratch
+} sixel_state_T;
+
+static char_u sixel_fixed_palette[240 * 3];
+static char_u sixel_rgb_cube_idx[256];
+static int sixel_fixed_tables_ready = FALSE;
+static sixel_state_T sixel_state;
can you add a section to free_all_mem() that frees all the static allocated sixel structs states on exit (EXITFREE)
In src/os_unix.c:
> @@ -4527,6 +4527,76 @@ mch_get_shellsize(void)
return OK;
}
+/*
+ * Synchronously ask the terminal for its window pixel dimensions via the
+ * xterm CSI 14 t query and parse the CSI 4 ; H ; W t response. Returns OK
+ * and fills *win_w and *win_h on success. Returns FAIL on any error
+ * (non-tty, no response within ~200ms, malformed reply).
+ */
+ static int
+query_terminal_pixel_size(int *win_w, int *win_h)
But I think it can swallow characters coming the input buffer (e.g. something the user has typed, while Vim is processing the terminal responses) (similar thing in popup_kitty_probe())
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Copilot commented on this pull request.
This PR adds support for rendering raw RGB/RGBA pixel buffers inside popup windows via a new image option on popup_create() / popup_setoptions(), with terminal backends (DEC sixel, kitty graphics protocol) and GUI backends (Windows GDI, GTK/Cairo). It also adds getbgcolor() for scripts to query the current background color and updates docs/features/tests accordingly.
Changes:
image option plumbing, screen redraw integration, and backend selection/encoding for sixel/kitty and GDI/Cairo.getbgcolor() and extend has() / :version feature reporting for +image* features.Copilot reviewed 40 out of 40 changed files in this pull request and generated 5 comments.
Show a summary per file| File | Description |
|---|---|
| src/window.c | Frees popup image buffers/caches during popup teardown. |
| src/version.c | Registers +image* feature strings for :version. |
| src/testdir/test_popupwin.vim | Adds tests for popup image set/update/getoptions/validation. |
| src/testdir/test_functions.vim | Makes getcellpixels() terminal test accept empty results. |
| src/term.c | Tracks terminal background color (OSC 11) and exposes it via term_get_bg_color(). |
| src/structs.h | Adds popup image fields to win_T and introduces image_rgb_T. |
| src/sixel.c | Adds a self-contained RGB(A) → sixel encoder and resize helper. |
| src/proto/term.pro | Updates term_get_bg_color() prototype to return status. |
| src/proto/sixel.pro | Adds prototypes for sixel helpers. |
| src/proto/popupwin.pro | Adds prototypes for popup image repaint helpers. |
| src/proto/os_win32.pro | Adds mch_calc_cell_size() prototype on Win32. |
| src/proto/os_mswin.pro | Adds mch_calc_cell_size() prototype on MS-Windows (stub impl). |
| src/proto/kitty.pro | Adds prototypes for kitty encoder/delete helpers. |
| src/proto/gui_w32.pro | Adds GUI popup image cache/update/draw prototypes for Win32 GUI. |
| src/proto/gui_gtk_x11.pro | Adds GUI popup image cache/update/draw prototypes for GTK GUI. |
| src/proto/cairo.pro | Adds prototypes for shared Cairo popup image backend. |
| src/proto.h | Includes new proto headers based on FEAT_IMAGE_*. |
| src/popupwin.c | Implements image option parsing, backend selection/probing, encoding/cropping, and repaint scheduling. |
| src/os_win32.c | Adds Win32 mch_calc_cell_size() with CSI 14t fallback and caching. |
| src/os_unix.c | Adds CSI 14t fallback/caching for cell pixel size; avoids blocking in mch_report_winsize(). |
| src/os_mswin.c | Adds a stub mch_calc_cell_size() for platforms where it’s unused. |
| src/Makefile | Adds new source/object/proto entries for sixel/kitty/cairo backends. |
| src/Make_vms.mms | Adds sixel/kitty/cairo sources/objects for VMS builds. |
| src/Make_mvc.mak | Adds sixel/kitty/cairo objects for MSVC builds. |
| src/Make_cyg_ming.mak | Adds sixel/kitty/cairo objects for Cygwin/MinGW builds. |
| src/Make_ami.mak | Adds sixel/kitty/cairo sources for Amiga build. |
| src/kitty.c | Adds RGB(A) → kitty graphics protocol APC encoder and delete sequence builder. |
| src/gui.c | Repairs GDI GUI redraw damage by repainting overlapping popup images. |
| src/gui_w32.c | Implements Windows GUI popup image cache/update/draw using DIBSection + BitBlt. |
| src/gui_gtk_x11.c | Adds GTK wrappers calling shared Cairo popup image backend. |
| src/feature.h | Introduces FEAT_IMAGE* feature flags and documents backends. |
| src/evalfunc.c | Adds getbgcolor() and registers has('image*') feature names. |
| src/drawscreen.c | Re-emits popup images at the end of screen redraw. |
| src/cairo.c | Implements shared Cairo surface cache/update/composite for popup images. |
| runtime/doc/various.txt | Documents new +image* features. |
| runtime/doc/usr_41.txt | Lists getbgcolor() under GUI functions. |
| runtime/doc/tags | Adds tags for +image*, getbgcolor(), and popup-image. |
| runtime/doc/popup.txt | Documents popup_create()/popup_setoptions() image option and popup-image section. |
| runtime/doc/builtin.txt | Documents getbgcolor() and has('image*') feature names. |
| Filelist | Adds new source/proto files to distribution file list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
In src/popupwin.c:
> +#ifdef FEAT_IMAGE
+ di = dict_find(dict, (char_u *)"image", -1);
+ if (di != NULL && di->di_tv.v_type == VAR_DICT
+ && di->di_tv.vval.v_dict != NULL)
+ {
+ dict_T *idict = di->di_tv.vval.v_dict;
+ dictitem_T *id = dict_find(idict, (char_u *)"data", -1);
+ int iw = (int)dict_get_number(idict, "width");
+ int ih = (int)dict_get_number(idict, "height");
+
+ if (id != NULL && id->di_tv.v_type == VAR_BLOB
+ && id->di_tv.vval.v_blob != NULL
+ && iw > 0 && ih > 0)
+ {
+ blob_T *b = id->di_tv.vval.v_blob;
+ long blen = blob_len(b);
+ int has_alpha = (blen == (long)iw * ih * 4);
+
+ if (has_alpha || blen == (long)iw * ih * 3)
+ {
+ // Detect "same-size image swap": replacing the pixel buffer
+ // without changing the popup's pixel dimensions or pixel
+ // format. In that case the cached device bitmap can be
+ // refreshed in place and the popup's screen cells stay valid
+ // (the image will be re-blitted on top by popup_emit_image),
+ // so a UPD_VALID redraw is enough -- no need to repaint every
+ // cell under the image like UPD_NOT_VALID would.
+ int same_size_update = (wp->w_popup_image_data != NULL
popup_setoptions(..., #{image: {}}) is documented as removing a previously set image, but this code path never clears w_popup_image_data unless a valid {data,width,height} blob is provided. With an empty dict (or dict missing required keys) the existing image will remain attached to the popup. Consider explicitly treating an empty dict as "clear image": free w_popup_image_data/w_popup_image_seq, reset dimensions/alpha, free any GUI caches, and schedule a redraw so the underlying cells are repainted.
In src/window.c:
> +# ifdef FEAT_IMAGE + vim_free(win->w_popup_image_data); +# ifdef FEAT_IMAGE_SIXEL + vim_free(win->w_popup_image_seq); +# endif +# ifdef FEAT_IMAGE_GDI + gui_mch_free_popup_image(win); +# endif
win_free_popup() frees GUI image caches only under FEAT_IMAGE_GDI, but GTK/Cairo builds also allocate a cached surface (w_popup_image_surface) via gui_mch_*_popup_image() and will leak it here. Use a guard that also covers the Cairo backend (or just #ifdef FEAT_GUI if gui_mch_free_popup_image() is available there) so all GUI image caches are released when a popup is freed.
In src/sixel.c:
> + if (npal + 1 > band->color_len)
+ {
+ int *seen = ALLOC_MULT(int, npal + 1);
+ int *used = ALLOC_MULT(int, npal + 1);
+ int *xmin = ALLOC_MULT(int, npal + 1);
+ int *xmax = ALLOC_MULT(int, npal + 1);
+
+ if (seen == NULL || used == NULL || xmin == NULL || xmax == NULL)
+ {
+ vim_free(seen);
+ vim_free(used);
+ vim_free(xmin);
+ vim_free(xmax);
+ return FAIL;
+ }
+ if (band->color_len > 0)
+ {
+ mch_memmove(seen, band->seen,
+ (size_t)band->color_len * sizeof(*seen));
+ mch_memmove(used, band->used,
+ (size_t)band->color_len * sizeof(*used));
+ mch_memmove(xmin, band->xmin,
+ (size_t)band->color_len * sizeof(*xmin));
+ mch_memmove(xmax, band->xmax,
+ (size_t)band->color_len * sizeof(*xmax));
+ }
+ vim_free(band->seen);
+ vim_free(band->used);
+ vim_free(band->xmin);
+ vim_free(band->xmax);
+ band->seen = seen;
+ band->used = used;
+ band->xmin = xmin;
+ band->xmax = xmax;
+ band->color_len = npal + 1;
+ }
+ return OK;
sixel_ensure_band_capacity() allocates/extends the per-color arrays (seen, xmin, xmax, etc.) with ALLOC_MULT(), which does not zero-initialize memory. Newly added elements can contain garbage, and if a garbage seen[pix] happens to equal the current generation, the band loop will treat that color as already-initialized and use uninitialized xmin/xmax and bitmasks. Ensure the newly allocated portion is initialized (or allocate with alloc_clear() / explicitly vim_memset() the arrays) and reset seen_gen as needed to make the generation check reliable.
In src/kitty.c:
> + 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;
+
+ ga_append(ga, kitty_b64_table[(triple >> 18) & 0x3f]);
+ ga_append(ga, kitty_b64_table[(triple >> 12) & 0x3f]);
+ ga_append(ga, (i + 1 < len)
+ ? kitty_b64_table[(triple >> 6) & 0x3f] : '=');
+ ga_append(ga, (i + 2 < len)
+ ? kitty_b64_table[triple & 0x3f] : '=');
+ }
+}
+
+/*
+ * Encode an RGB(A) image into a kitty graphics protocol APC sequence.
+ * Returns a malloced char_u* containing the full sequence
+ * (one or more `\e_G...\e\\` 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().
+ */
+ char_u *
+kitty_encode(image_rgb_T *img, int id)
+{
+ 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;
+
+ 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)
+ {
+ long this_chunk = b64_total - offset;
+ int more;
+
+ if (this_chunk > 4096)
+ this_chunk = 4096;
+ more = (offset + this_chunk < b64_total);
+
+ if (first)
+ {
+ if (id != 0)
+ vim_snprintf((char *)hdr, sizeof(hdr),
+ "\033_Ga=T,f=%d,s=%d,v=%d,i=%d,q=2,m=%d;",
+ fmt, img->width, img->height, id, more ? 1 : 0);
+ else
+ vim_snprintf((char *)hdr, sizeof(hdr),
+ "\033_Ga=T,f=%d,s=%d,v=%d,q=2,m=%d;",
+ fmt, img->width, img->height, more ? 1 : 0);
+ first = FALSE;
+ }
+ else
+ {
+ vim_snprintf((char *)hdr, sizeof(hdr),
+ "\033_Gm=%d;", more ? 1 : 0);
+ }
+ ga_concat(&ga, hdr);
+
+ // 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;
+
+ if (src_offset + src_len > payload_len)
+ src_len = payload_len - src_offset;
+ kitty_b64_append(&ga, img->data + src_offset, src_len);
+ }
+
+ ga_concat(&ga, (char_u *)"\033\\");
+
+ offset += this_chunk;
+ }
+
+ ga_append(&ga, NUL);
+ return (char_u *)ga.ga_data;
+}
kitty_encode()/kitty_b64_append() claim to return NULL on OOM, but they build the sequence using ga_concat() (void, silently no-ops on allocation failure) and ignore ga_append() return values. In low-memory situations this can return a truncated/invalid kitty APC sequence instead of NULL. To match the function contract (and avoid emitting malformed escape sequences), track allocation failures by using helpers that propagate ga_grow() errors (similar to ga_concat_bytes() in sixel.c) and abort/return NULL when any append fails.
In src/feature.h:
> @@ -1086,6 +1086,38 @@ # define FEAT_PROP_POPUP #endif +/* + * +image RGB image rendering inside popup windows. + * +image_sixel terminal backend: emit DEC sixel DCS sequences. + * +image_kitty terminal backend: emit kitty graphics protocol APC + * sequences. Selected at runtime when the host + * terminal advertises kitty graphics support and + * falls back to sixel otherwise. + * +image_gdi Windows GUI backend: StretchDIBits onto the GUI canvas.
The +image_gdi feature comment says the backend uses StretchDIBits, but the implementation builds a DIB section and draws it with BitBlt(). Updating the comment would avoid confusion when tracing rendering behavior/perf characteristics.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 34 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 9 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 2 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
Addressed your review comments. CI is green; the remaining CodeQL alerts are pre-existing sprintf warnings in src/strings.c which this PR doesn't touch.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 46 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 48 commits.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
All review comments addressed. The last one, merging rgba_to_paletted_fast() into the RGB fast path via a has_alpha parameter, is in 515ea7d.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Will be supported in gvim?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Yes, gvim is already supported. The GTK GUI (gvim built with GTK) renders the image through Cairo (FEAT_IMAGE_CAIRO), and the MS-Windows GUI through GDI (FEAT_IMAGE_GDI). See the Cairo screenshot above.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Okay, thank you. Can you please squash it into a single commit?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Squashed into a single commit. Thanks!
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()