patch 9.2.0633: MS-Windows: No support for kitty graphics support in terminal
Commit:
https://github.com/vim/vim/commit/bf5235bdc97cbb2e9efb9cfc4c2083db30aeae91
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Sat Jun 13 18:12:08 2026 +0000
patch 9.2.0633: MS-Windows: No support for kitty graphics support in terminal
Problem: MS-Windows: No support for kitty graphics support in terminal
Solution: Add mch_kitty_probe() to os_win32.c, reading the reply from
the console input handle in VT input mode, with a short grace
period after the DA1 answer because ConPTY answers DA1 by
itself. The response parsing is shared with the UNIX probe via
the new kitty_probe_parse() in kitty.c. (Yasuhiro Matsumoto).
closes: #20497
Signed-off-by: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/kitty.c b/src/kitty.c
index 183d17e58..2b46b1454 100644
--- a/src/kitty.c
+++ b/src/kitty.c
@@ -172,6 +172,74 @@ fail:
return NULL;
}
+/*
+ * Shared tail of the kitty-graphics-support probe (see popup_kitty_probe()
+ * in popupwin.c for the UNIX side and mch_kitty_probe() in os_win32.c for
+ * the Windows console side). "buf[n]" holds the raw, NUL-terminated bytes
+ * read back from the terminal after sending the ` _Gi=31...a=q` query
+ * followed by the DA1 (` [c`) sentinel.
+ *
+ * Push everything except the kitty (APC _G... \) and DA1 ( [?...c)
+ * response chunks back into the input buffer so user keystrokes typed
+ * during the probe are not swallowed. Scan the buffer linearly, emitting
+ * any byte that is not inside a recognised response range.
+ *
+ * Returns TRUE when the buffer contains the positive "_Gi=31;OK" reply.
+ */
+ int
+kitty_probe_parse(char *buf, int n)
+{
+ int i = 0;
+ int seg_start = 0;
+
+ while (i < n)
+ {
+ int response_end = -1;
+
+ if (buf[i] == ' ' && i + 1 < n)
+ {
+ if (buf[i + 1] == '_')
+ {
+ // Kitty APC reply: scan to terminating ESC '\'.
+ int j;
+ for (j = i + 2; j + 1 < n; ++j)
+ if (buf[j] == ' ' && buf[j + 1] == '\')
+ {
+ response_end = j + 2;
+ break;
+ }
+ }
+ else if (buf[i + 1] == '[' && i + 2 < n && buf[i + 2] == '?')
+ {
+ // DA1 reply: ESC [ ? ... c (the '?' distinguishes the
+ // primary device-attributes answer from an arrow key
+ // sequence like ESC [ A that the user might have typed).
+ int j;
+ for (j = i + 3; j < n; ++j)
+ if (buf[j] == 'c')
+ {
+ response_end = j + 1;
+ break;
+ }
+ }
+ }
+ if (response_end > 0)
+ {
+ if (i > seg_start)
+ add_to_input_buf((char_u *)(buf + seg_start), i - seg_start);
+ i = response_end;
+ seg_start = i;
+ }
+ else
+ ++i;
+ }
+ if (n > seg_start)
+ add_to_input_buf((char_u *)(buf + seg_start), n - seg_start);
+
+ // A positive kitty reply contains the literal "_Gi=31;OK".
+ 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
diff --git a/src/os_win32.c b/src/os_win32.c
index c76c2a95f..5e11de231 100644
--- a/src/os_win32.c
+++ b/src/os_win32.c
@@ -4866,6 +4866,128 @@ mch_calc_cell_size(struct cellsize *cs_out)
cs_out->cs_ypixel = csi14_cell_y;
}
}
+
+# if defined(FEAT_IMAGE_KITTY) || defined(PROTO)
+/*
+ * Synchronously probe the host terminal for kitty graphics protocol
+ * support. Windows console counterpart of popup_kitty_probe() in
+ * popupwin.c: sends the same
+ * _Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA \ [c
+ * query (kitty answer + DA1 sentinel) and feeds the reply to the shared
+ * kitty_probe_parse(). The reply is read from the console input handle
+ * with ENABLE_VIRTUAL_TERMINAL_INPUT set, the same technique as
+ * query_terminal_pixel_size_w32() above.
+ *
+ * Returns TRUE on a positive `_Gi=31;OK` response. Missing handles,
+ * a non-VT console and timeouts (~500ms) all yield FALSE.
+ */
+ int
+mch_kitty_probe(void)
+{
+ HANDLE hOut = g_hConOut;
+ HANDLE hIn = g_hConIn;
+ DWORD mode_in_old = 0;
+ int restored = 0;
+ DWORD nWritten = 0;
+ char buf[256];
+ int n = 0;
+ DWORD deadline;
+ DWORD da1_deadline;
+ static const char query[] =
+ " _Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA \ [c";
+
+# ifdef VIMDLL
+ if (gui.in_use)
+ return FALSE;
+# endif
+ if (hOut == INVALID_HANDLE_VALUE || hIn == INVALID_HANDLE_VALUE)
+ return FALSE;
+ // Without VT output processing the query would be echoed onto the
+ // legacy console as raw text, and the kitty APC image sequences could
+ // not reach a terminal anyway.
+ if (!vtp_working)
+ return FALSE;
+
+ if (GetConsoleMode(hIn, &mode_in_old))
+ {
+ DWORD mode_new = mode_in_old;
+
+ // Read raw bytes; deliver the terminal responses as VT input.
+ mode_new &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
+ | ENABLE_PROCESSED_INPUT);
+ mode_new |= ENABLE_VIRTUAL_TERMINAL_INPUT;
+ if (SetConsoleMode(hIn, mode_new))
+ restored = 1;
+ }
+
+ if (!WriteFile(hOut, query, sizeof(query) - 1, &nWritten, NULL)
+ || nWritten != sizeof(query) - 1)
+ {
+ if (restored)
+ SetConsoleMode(hIn, mode_in_old);
+ return FALSE;
+ }
+
+ // Read until the kitty reply lands or we time out. Unlike the UNIX
+ // probe, the DA1 reply cannot serve as a hard "no kitty support"
+ // sentinel here: under ConPTY the DA1 answer is generated by conhost
+ // itself, while the kitty APC query has to round-trip through the
+ // attached terminal (possibly over ssh), so the DA1 answer usually
+ // arrives first. Treat DA1 as "wrap up soon" instead and keep
+ // reading for a short grace period after it.
+ deadline = GetTickCount() + 500;
+ da1_deadline = 0; // 0: DA1 reply not seen yet
+ while (n < (int)sizeof(buf) - 1)
+ {
+ DWORD now = GetTickCount();
+ DWORD until = deadline;
+ INPUT_RECORD ir;
+ DWORD count = 0;
+
+ if (da1_deadline != 0 && (int)(da1_deadline - until) < 0)
+ until = da1_deadline;
+ if ((int)(until - now) <= 0)
+ break;
+ if (WaitForSingleObject(hIn, until - now) != WAIT_OBJECT_0)
+ break;
+ if (!ReadConsoleInputW(hIn, &ir, 1, &count) || count != 1)
+ break;
+ if (ir.EventType != KEY_EVENT
+ || !ir.Event.KeyEvent.bKeyDown
+ || ir.Event.KeyEvent.uChar.AsciiChar == 0)
+ continue;
+ buf[n++] = ir.Event.KeyEvent.uChar.AsciiChar;
+ buf[n] = NUL;
+ // Stop as soon as the kitty APC reply is complete (terminated by
+ // ESC \) -- positive or negative, nothing later changes the verdict.
+ if (n >= 2 && buf[n - 1] == '\' && buf[n - 2] == ' '
+ && strstr(buf, "_Gi=31;") != NULL)
+ break;
+ // DA1 reply ends with a primary 'c' that is preceded by '?';
+ // once seen, allow a little more time for a passthrough kitty
+ // reply, then give up.
+ if (da1_deadline == 0
+ && buf[n - 1] == 'c' && n >= 3 && buf[n - 2] != ' ')
+ {
+ int i;
+
+ for (i = n - 2; i > 0; --i)
+ if (buf[i] == '?' && buf[i - 1] == '[')
+ break;
+ if (i > 0)
+ da1_deadline = GetTickCount() + 250;
+ }
+ }
+ buf[n] = NUL;
+
+ if (restored)
+ SetConsoleMode(hIn, mode_in_old);
+
+ // Filter the probe responses out of the read-back bytes (pushing user
+ // keystrokes back into the input buffer) and check for a positive reply.
+ return kitty_probe_parse(buf, n);
+}
+# endif
#endif
static BOOL
diff --git a/src/popupwin.c b/src/popupwin.c
index 8e6090d1a..183dff608 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -1815,62 +1815,9 @@ popup_kitty_probe(void)
buf[n] = NUL;
tcsetattr(fd_in, TCSANOW, &old_t);
- // Push everything except the kitty (APC _G... \) and DA1 ( [?...c)
- // response chunks back into the input buffer so user keystrokes typed
- // during the probe are not swallowed. Scan the buffer linearly,
- // emitting any byte that is not inside a recognised response range.
- {
- int i = 0;
- int seg_start = 0;
-
- while (i < n)
- {
- int response_end = -1;
-
- if (buf[i] == ' ' && i + 1 < n)
- {
- if (buf[i + 1] == '_')
- {
- // Kitty APC reply: scan to terminating ESC '\'.
- int j;
- for (j = i + 2; j + 1 < n; ++j)
- if (buf[j] == ' ' && buf[j + 1] == '\')
- {
- response_end = j + 2;
- break;
- }
- }
- else if (buf[i + 1] == '[' && i + 2 < n && buf[i + 2] == '?')
- {
- // DA1 reply: ESC [ ? ... c (the '?' distinguishes the
- // primary device-attributes answer from an arrow key
- // sequence like ESC [ A that the user might have typed).
- int j;
- for (j = i + 3; j < n; ++j)
- if (buf[j] == 'c')
- {
- response_end = j + 1;
- break;
- }
- }
- }
- if (response_end > 0)
- {
- if (i > seg_start)
- add_to_input_buf((char_u *)(buf + seg_start),
- i - seg_start);
- i = response_end;
- seg_start = i;
- }
- else
- ++i;
- }
- if (n > seg_start)
- add_to_input_buf((char_u *)(buf + seg_start), n - seg_start);
- }
-
- // A positive kitty reply contains the literal "_Gi=31;OK".
- return strstr(buf, "_Gi=31;OK") != NULL;
+ // Filter the probe responses out of the read-back bytes (pushing user
+ // keystrokes back into the input buffer) and check for a positive reply.
+ return kitty_probe_parse(buf, n);
}
# endif
@@ -1878,8 +1825,9 @@ popup_kitty_probe(void)
* Pick a terminal image backend. Cached on first call. Returns
* IMAGE_BACKEND_KITTY when the host terminal advertises kitty graphics
* support via an active ` _Gi=31...a=q` probe followed by ` [c`
- * (see popup_kitty_probe), otherwise IMAGE_BACKEND_SIXEL. The probe
- * works regardless of $TERM, vendor env vars, or terminal name, so it
+ * (see popup_kitty_probe for UNIX, mch_kitty_probe in os_win32.c for the
+ * Windows console), otherwise IMAGE_BACKEND_SIXEL. The probe works
+ * regardless of $TERM, vendor env vars, or terminal name, so it
* handles Konsole, WSL-from-Ghostty, ssh, and any future terminal that
* adopts the protocol.
*/
@@ -1898,6 +1846,11 @@ popup_image_backend(void)
// (~300ms worst case) on first call.
if (popup_kitty_probe())
detected = IMAGE_BACKEND_KITTY;
+# elif defined(FEAT_IMAGE_KITTY) && defined(MSWIN)
+ // Same probe, but reading the reply needs Windows console plumbing
+ // (console handles, VT input mode) that lives in os_win32.c.
+ if (mch_kitty_probe())
+ detected = IMAGE_BACKEND_KITTY;
# endif
return detected;
}
diff --git a/src/proto/
kitty.pro b/src/proto/
kitty.pro
index 11e2ab2f4..8c2970c7d 100644
--- a/src/proto/
kitty.pro
+++ b/src/proto/
kitty.pro
@@ -1,4 +1,5 @@
/* kitty.c */
char_u *kitty_encode(image_rgb_T *img, int id, int zindex);
+int kitty_probe_parse(char *buf, int n);
char_u *kitty_delete(int id);
/* vim: set ft=c : */
diff --git a/src/proto/
os_win32.pro b/src/proto/
os_win32.pro
index 344161355..4850ea02f 100644
--- a/src/proto/
os_win32.pro
+++ b/src/proto/
os_win32.pro
@@ -49,6 +49,7 @@ void mch_set_shellsize(void);
void mch_new_shellsize(void);
void mch_set_winsize_now(void);
void mch_calc_cell_size(struct cellsize *cs_out);
+int mch_kitty_probe(void);
int mch_call_shell(char_u *cmd, int options);
void win32_build_env(dict_T *env, garray_T *gap, int is_terminal);
char_u *mch_get_cmd_output_direct(char **argv, char_u *infile, int flags, int *ret_len);
diff --git a/src/version.c b/src/version.c
index 5e1648127..a32296a6d 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 633,
/**/
632,
/**/