From: Hirohito Higashi Date: Mon, 20 Jul 2026 17:44:27 +0000 (+0000) Subject: patch 9.2.0819: MS-Windows: sixel image shown as raw text in the console X-Git-Tag: v9.2.0819^0 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5af893b2cd43c5ccf8eea60bfca4198b28536e1b;p=thirdparty%2Fvim.git patch 9.2.0819: MS-Windows: sixel image shown as raw text in the console Problem: On the MS-Windows console a popup image is displayed as the raw sixel escape sequence instead of the image, unless 'termguicolors' is set (Bakudankun). Solution: Write a DCS sequence with the console API that lets the console parse it, no matter which write path the current colors select. Keep track of the terminator across writes, since a long sequence is split up (Hirohito Higashi). fixes: #20795 closes: #20797 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/src/os_win32.c b/src/os_win32.c index 5e11de231c..4797eed426 100644 --- a/src/os_win32.c +++ b/src/os_win32.c @@ -7645,6 +7645,52 @@ sgrn2cn( return p && p[0] == 0x0a && p[1] == '\033' ? ++p : NULL; } +static bool in_dcs = false; // inside a DCS (sixel) sequence +static bool dcs_esc = false; // last DCS byte written was ESC + +/* + * Write "len" bytes at "s" with WriteConsoleW, so that the console's own VT + * parser processes them. Needed for DCS sequences when USE_VTP is off. + */ + static void +write_vt(char_u *s, int len) +{ + int wlen; + WCHAR *ws; + DWORD written; + + wlen = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)s, len, NULL, 0); + if (wlen <= 0) + return; + ws = ALLOC_MULT(WCHAR, wlen); + if (ws == NULL) + return; + MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)s, len, ws, wlen); + WriteConsoleW(g_hConOut, ws, wlen, &written, NULL); + vim_free(ws); +} + +/* + * Return the pointer just after the ST (ESC '\') that ends a DCS sequence. + * Returns "end" when the ST is not in this chunk; "in_dcs" then stays set so + * that the next mch_write() call continues the scan. + */ + static char_u * +skip_dcs(char_u *s, char_u *end) +{ + for (char_u *p = s; p < end; ++p) + { + if (dcs_esc && *p == '\\') + { + dcs_esc = false; + in_dcs = false; + return p + 1; + } + dcs_esc = *p == ESC; + } + return end; +} + /* * mch_write(): write the output buffer to the screen, translating ESC * sequences into calls to console output routines. @@ -7681,6 +7727,18 @@ mch_write( return; } + if (in_dcs) + { + // Continuation of a DCS sequence split over several writes. + int l = (int)(skip_dcs(s, end) - s); + + if (vtp_working) + write_vt(s, l); + len -= l - 1; + s += l; + continue; + } + while (s + ++prefix < end) { ch = s[prefix]; @@ -7985,6 +8043,20 @@ notsgr: len -= l - 1; s += l; } + else if (s[0] == ESC && len >= 2-1 && s[1] == 'P') + { + // DCS (e.g. sixel image): write_chars() would print it as raw text + // when USE_VTP is off, so let the console's VT parser handle it. + int l; + + in_dcs = true; + dcs_esc = false; + l = (int)(skip_dcs(s + 2, end) - s); + if (vtp_working) + write_vt(s, l); + len -= l - 1; + s += l; + } else { // Write a single character diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim index 5e4bc5221f..67babab1b2 100644 --- a/src/testdir/test_popupwin.vim +++ b/src/testdir/test_popupwin.vim @@ -5887,6 +5887,40 @@ func Test_popup_image_clear_with_empty_dict() call popup_close(winid) endfunc +" A popup image is emitted as a DCS (sixel) escape sequence. On the Windows +" console it has to go through the VT path, otherwise the escape sequence ends +" up on the screen as raw text when 'termguicolors' is off. Send the sequence +" with echoraw() so that only the console write is under test. See #20795. +func Test_dcs_not_written_as_text_windows_cui() + CheckFeature terminal + if !has('win32') || has('gui_running') + throw 'Skipped: only for the Windows CUI' + endif + + " Draw the text first, so nothing repairs the cells afterwards in case the + " sequence does land on the screen as text. + let lines =<< trim END + call setline(1, 'READY') + redraw + call echoraw("\P0;1;8q\"1;1;8;8#1;2;100;0;0#1~~~~~~~~-\\\") + END + call writefile(lines, 'XpopupDcs', 'D') + + let buf = term_start(GetVimCommandCleanTerm() .. '-S XpopupDcs', + \ #{term_rows: 12, term_cols: 40}) + call WaitForAssert({-> assert_equal('READY', + \ term_scrape(buf, 1)[:4]->map({_, v -> v['chars']})->join(''))}) + call TermWait(buf, 50) + + let screen = '' + for row in range(1, 12) + let screen ..= term_scrape(buf, row)->map({_, v -> v['chars']})->join('') + endfor + call assert_notmatch('0;1;8q', screen) + + call StopVimInTerminal(buf) +endfunc + func Test_popup_image_set_and_getoptions() CheckFeature image diff --git a/src/version.c b/src/version.c index a1318f49ad..ae605f2431 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 */ +/**/ + 819, /**/ 818, /**/