]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0819: MS-Windows: sixel image shown as raw text in the console v9.2.0819
authorHirohito Higashi <h.east.727@gmail.com>
Mon, 20 Jul 2026 17:44:27 +0000 (17:44 +0000)
committerChristian Brabandt <cb@256bit.org>
Mon, 20 Jul 2026 17:44:27 +0000 (17:44 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/os_win32.c
src/testdir/test_popupwin.vim
src/version.c

index 5e11de231c4ea2dd88142d955ebab81c9dfc47bb..4797eed426240dd731637970bf08372321775cf3 100644 (file)
@@ -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
index 5e4bc5221fd5fe28591e9748c576cdb3aa241cfb..67babab1b221d3ead25eb2304b9a71bd70841fb4 100644 (file)
@@ -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
 
index a1318f49ad38410171f4fa5b98cb6afdd8966887..ae605f243146776711cd1694d94eeb64c89b007e 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    819,
 /**/
     818,
 /**/