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 <[email protected]> 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 <[email protected]> Signed-off-by: Christian Brabandt <[email protected]> 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, /**/ -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1wYT7M-00DPP3-7U%40256bit.org.
