patch 9.2.0819: MS-Windows: sixel image shown as raw text in the console
Commit:
https://github.com/vim/vim/commit/5af893b2cd43c5ccf8eea60bfca4198b28536e1b
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Mon Jul 20 17:44:27 2026 +0000
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) <
nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/os_win32.c b/src/os_win32.c
index 5e11de231..4797eed42 100644
--- a/src/os_win32.c
+++ b/src/os_win32.c
@@ -7645,6 +7645,52 @@ sgrn2cn(
return p && p[0] == 0x0a && p[1] == '�' ? ++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 5e4bc5221..67babab1b 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("\<Esc>P0;1;8q\"1;1;8;8#1;2;100;0;0#1~~~~~~~~-\<Esc>\")
+ 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 a1318f49a..ae605f243 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,
/**/