[PATCH v2 0/4] MR9973: Draft: conhost: add basic VT control sequence
This is a minimal approach to running MC in wineconsole. Wineconsole thus supports basic CSI control sequences for: - SGR (Select Graphic Rendition) and - CUP (Cursor Position) {width=900 height=507} -- v2: conhost: add basic CUP color handling conhost: improve basic SGR color handling conhost: add basic SGR color handling https://gitlab.winehq.org/wine/wine/-/merge_requests/9973
From: Thomas Csovcsity <thc.fr13nd@gmail.com> Remove Control Sequence Introducer(CSI) commands avoids console scrambling --- programs/conhost/conhost.c | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 8298743553f..6bf81f97407 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2107,6 +2107,9 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR { RECT update_rect; size_t i, j; + enum { + ST_START, ST_PARAM, ST_INTER, ST_FINAL + } state; TRACE( "%s\n", debugstr_wn(buffer, len) ); @@ -2143,6 +2146,63 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR case '\r': screen_buffer->cursor_x = 0; continue; + case '\e': + if ( buffer[i+1] == '[') + { + FIXME( "CSI sequences not supported fully yet, only skipping control sequences\n" ); + state = ST_PARAM; + i+=2; + } + else + { + ERR("Invalid CSI start sequence\n"); + break; + } + /* intermediate bytes 0x30 - 0x3f (0-?) */ + for (; i<len && (state == ST_PARAM); i++) + { + switch (buffer[i]) + { + case 0x30 ... 0x3b: + continue; + case 0x3c ... 0x3f: /* private for terminal manufacturers */ + WARN("This is a private -terminal manufacturer only- CSI sequence\n"); + continue; + default: + state = ST_INTER; + break; + } + break; + } + /* intermediate bytes 0x20 - 0x2f*/ + for (; i<len && (state == ST_INTER || state == ST_PARAM); i++) + { + switch (buffer[i]) + { + case 0x20 ... 0x2f: + continue; + default: + state = ST_FINAL; + break; + } + break; + } + if ( state == ST_START || state == ST_FINAL ) + { + switch (buffer[i]) + { + case 0x40 ... 0x6F: + break; + case 0x70 ... 0x7E: /* private for terminal manufacturers */ + WARN("This is a private -terminal manufacturer only- CSI sequence\n"); + break; + default: + ERR("This was no valid CSI sequence\n"); + break; + } + i++; + break; + } } } if (screen_buffer->cursor_x == screen_buffer->width && !(screen_buffer->mode & ENABLE_WRAP_AT_EOL_OUTPUT)) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9973
From: Thomas Csovcsity <thc.fr13nd@gmail.com> --- programs/conhost/conhost.c | 172 ++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 6bf81f97407..7993e466c1b 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2103,13 +2103,171 @@ static NTSTATUS set_output_info( struct screen_buffer *screen_buffer, return STATUS_SUCCESS; } +#define BLACK 0x0000 +#define BLUE 0x0001 +#define GREEN 0x0002 +#define CYAN BLUE | GREEN +#define RED 0x0004 +#define MAGENTA BLUE | RED +#define YELLOW GREEN | RED +#define WHITE BLUE | GREEN | RED + +static unsigned int next_csi_sgr_argument(const WCHAR *seq, unsigned int len, unsigned int *arg) +{ + unsigned int i=0, value = 0; + TRACE("next SGR arg with %d length and [%s] as sequence! seq_p %p\n",len, debugstr_wn( seq, len ), seq); + for ( i=0; i<len; i++ ) + { + if ( (seq[i] == ';') || (seq[i]=='m') ) + { + *arg = value; + return i+1; + } + value = 10 * value + seq[i]-'0'; + } + return i; +} + +static void process_csi_sequence_console( struct screen_buffer *screen_buffer, const WCHAR *seq, unsigned int len ) +{ + unsigned int colors[] = {BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE}; + unsigned int seq_len = 0, value = 0, *arg = &value; + unsigned int r = 0, g = 0, b = 0; + struct condrv_output_info_params info_params; + + switch (seq[len-1]) + { + case 'm': /* Select Graphic Rendition (SGR) */ + while (len > 0) + { + TRACE("This is an SGR seq with %d length and [%s] as payload! seq_p %p\n",len, debugstr_wn( seq, len ), seq); + seq_len = next_csi_sgr_argument(seq, len, arg); + switch (value) + { + case 30 ... 37: + { + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0xff00 ) | colors[*arg-30]; + break; + } + case 38: + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, arg); + if ( *arg == 2 ) + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &r); + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &g); + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &b); + len -= seq_len; + seq += seq_len; + FIXME("24bit rgb is not supported yet for foreground r[%d]g[%d]b[%d]\n", r, g, b); + info_params.info.attr = ( BLACK < 8 ) | ( WHITE ); /* Fixme set correct color */ + } + if ( *arg == 5 ) + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, arg); + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0xff00 ) | colors[*arg]; + } + break; + } + case 39: + { + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0xf0 ) | ( WHITE ); /* default white fg */ + break; + } + case 40 ... 47: + { + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0x0f ) | ( colors[*arg-40] << 4 ); + break; + } + case 48: + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, arg); + if ( *arg == 2 ) + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &r); + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &g); + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, &b); + len -= seq_len; + seq += seq_len; + FIXME("24bit rgb is not supported yet for background r[%d]g[%d]b[%d]\n", r, g, b); + info_params.info.attr = ( BLACK < 4 ) | ( WHITE ); /* Fixme set correct color */ + } + if ( *arg == 5 ) + { + len -= seq_len; + seq += seq_len; + seq_len = next_csi_sgr_argument(seq, len, arg); + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0xf0 ) | colors[*arg]; + } + break; + } + case 49: + { + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( info_params.info.attr & 0x000f ) | ( BLACK < 4 ); /* default black bg */ + break; + } + case 0: + { + len -= seq_len; + seq += seq_len; + info_params.info.attr = ( BLACK < 4 ) | ( WHITE ); //white on black + break; + } + default: + FIXME( "unhandled sgr sequence %s len[%d]\n", debugstr_wn( seq, len ), len); + info_params.info.attr = ( BLACK < 4 ) | ( WHITE ); //white on black + len = 0; + break; + + } + info_params.mask = SET_CONSOLE_OUTPUT_INFO_ATTR; + set_output_info( screen_buffer, &info_params, sizeof(info_params) ); + } + break; + default: + FIXME( "unhandled sequence %s switch char [%s] len[%d]\n", debugstr_wn( seq, len ), debugstr_wn( &seq[len], 1 ), len); + break; + } +} + static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR *buffer, size_t len ) { RECT update_rect; - size_t i, j; + unsigned int csi_len = 0, i, j; enum { ST_START, ST_PARAM, ST_INTER, ST_FINAL } state; + const WCHAR *csi_start; TRACE( "%s\n", debugstr_wn(buffer, len) ); @@ -2149,8 +2307,9 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR case '\e': if ( buffer[i+1] == '[') { - FIXME( "CSI sequences not supported fully yet, only skipping control sequences\n" ); state = ST_PARAM; + csi_start = &buffer[i+2]; + csi_len = 0; i+=2; } else @@ -2161,6 +2320,7 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR /* intermediate bytes 0x30 - 0x3f (0-?) */ for (; i<len && (state == ST_PARAM); i++) { + csi_len++; switch (buffer[i]) { case 0x30 ... 0x3b: @@ -2170,6 +2330,7 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR continue; default: state = ST_INTER; + csi_len--; break; } break; @@ -2177,12 +2338,14 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR /* intermediate bytes 0x20 - 0x2f*/ for (; i<len && (state == ST_INTER || state == ST_PARAM); i++) { + csi_len++; switch (buffer[i]) { case 0x20 ... 0x2f: continue; default: state = ST_FINAL; + csi_len--; break; } break; @@ -2200,9 +2363,10 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR ERR("This was no valid CSI sequence\n"); break; } - i++; - break; + csi_len++; + process_csi_sequence_console(screen_buffer, csi_start, csi_len); } + continue; } } if (screen_buffer->cursor_x == screen_buffer->width && !(screen_buffer->mode & ENABLE_WRAP_AT_EOL_OUTPUT)) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9973
From: Thomas Csovcsity <thc.fr13nd@gmail.com> add simple RGB24 to 3bit RGB --- programs/conhost/conhost.c | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 7993e466c1b..ed77541478b 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2125,7 +2125,7 @@ static unsigned int next_csi_sgr_argument(const WCHAR *seq, unsigned int len, un } value = 10 * value + seq[i]-'0'; } - return i; + return 0; } static void process_csi_sequence_console( struct screen_buffer *screen_buffer, const WCHAR *seq, unsigned int len ) @@ -2148,7 +2148,7 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c { len -= seq_len; seq += seq_len; - info_params.info.attr = ( info_params.info.attr & 0xff00 ) | colors[*arg-30]; + info_params.info.attr = ( info_params.info.attr & 0xf0 ) | colors[*arg-30]; break; } case 38: @@ -2158,6 +2158,7 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c seq_len = next_csi_sgr_argument(seq, len, arg); if ( *arg == 2 ) { + int color = BLACK; len -= seq_len; seq += seq_len; seq_len = next_csi_sgr_argument(seq, len, &r); @@ -2169,8 +2170,14 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c seq_len = next_csi_sgr_argument(seq, len, &b); len -= seq_len; seq += seq_len; - FIXME("24bit rgb is not supported yet for foreground r[%d]g[%d]b[%d]\n", r, g, b); - info_params.info.attr = ( BLACK < 8 ) | ( WHITE ); /* Fixme set correct color */ + /* a simple color mapping for basic support */ + if ( r > 127 ) + color |= RED; + if ( g > 127 ) + color |= GREEN; + if ( b > 127 ) + color |= BLUE; + info_params.info.attr = ( info_params.info.attr & 0xf0 ) | color; } if ( *arg == 5 ) { @@ -2179,7 +2186,7 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c seq_len = next_csi_sgr_argument(seq, len, arg); len -= seq_len; seq += seq_len; - info_params.info.attr = ( info_params.info.attr & 0xff00 ) | colors[*arg]; + info_params.info.attr = ( info_params.info.attr & 0xf0 ) | colors[*arg]; } break; } @@ -2204,6 +2211,7 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c seq_len = next_csi_sgr_argument(seq, len, arg); if ( *arg == 2 ) { + int color = BLACK; len -= seq_len; seq += seq_len; seq_len = next_csi_sgr_argument(seq, len, &r); @@ -2215,8 +2223,15 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c seq_len = next_csi_sgr_argument(seq, len, &b); len -= seq_len; seq += seq_len; - FIXME("24bit rgb is not supported yet for background r[%d]g[%d]b[%d]\n", r, g, b); - info_params.info.attr = ( BLACK < 4 ) | ( WHITE ); /* Fixme set correct color */ + /* a simple color mapping for basic support */ + if ( r > 127 ) + color |= RED; + if ( g > 127 ) + color |= GREEN; + if ( b > 127 ) + color |= BLUE; + info_params.info.attr = ( info_params.info.attr & 0x0f ) | ( color << 4 ); + } if ( *arg == 5 ) { @@ -2315,6 +2330,7 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR else { ERR("Invalid CSI start sequence\n"); + ERR( "%s\n", debugstr_wn(buffer, len) ); break; } /* intermediate bytes 0x30 - 0x3f (0-?) */ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9973
From: Thomas Csovcsity <thc.fr13nd@gmail.com> --- programs/conhost/conhost.c | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index ed77541478b..c7691cb4132 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2269,6 +2269,46 @@ static void process_csi_sequence_console( struct screen_buffer *screen_buffer, c set_output_info( screen_buffer, &info_params, sizeof(info_params) ); } break; + case 'H': + unsigned int x = 0, y = 0, value = 0, i; + TRACE("This is an CUP seq with %d length and [%s] as payload! seq_p %p\n",len, debugstr_wn( seq, len ), seq); + + for (i = 0; i < len; i++) + { + if ( seq[i] != ';' && seq[i] != 'H' ) + value = 10 * value + seq[i] - '0'; + else if ( seq[i] == ';' ) + { + if ( value != 0 ) + { + y = value; + value = 0; + } + else + y = 1; + } + else if ( seq[i] == 'H') + { + if ( y == 0 && value == 0) + { + x = 1; + y = 1; + } else if ( y == 0 && value != 0 ) + { + y = value; + x = 1; + } else /* ( x != 0 && value != 0 ) */ + x = value; + } + else + { + ERR("CSI CUP parsing failed, this should not happen!\n"); + break; + } + } + screen_buffer->cursor_x = x-1; + screen_buffer->cursor_y = y-1; + break; default: FIXME( "unhandled sequence %s switch char [%s] len[%d]\n", debugstr_wn( seq, len ), debugstr_wn( &seq[len], 1 ), len); break; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9973
thanks for taking care of this evolution just a cursory look (far from being a review): * style: please don't use C++ comments, nor case switch ranges (they are not portable enough) * style: try to stick to the current style of modified file * size_t shall be printed with %Iu * escape sequences should only be interpreted when console output mode has the vt seq bit set * some non regression tests could be useful (no precise idea on what they should cover) * reusing SET_CONSOLE_OUTPUT_INFO_ATTR is a tempting idea; did you check that how is done on native? (it's a matter of knowning if the "current" color from an ANSI seq is also the default screen buffer color; or when two programs are attached to the same conhost, they share the "current" ANSI color) * MR should be tested for both wineconsole and unix console -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9973#note_128016
On Mon Jan 26 23:57:18 2026 +0000, eric pouech wrote:
> thanks for taking care of this evolution
> just a cursory look (far from being a review):
> * style: please don't use C++ comments, nor case switch ranges (they are
> not portable enough)
> * style: try to stick to the current style of modified file
> * size_t shall be printed with %Iu
> * escape sequences should only be interpreted when console output mode
> has the vt seq bit set
> * some non regression tests could be useful (no precise idea on what
> they should cover)
> * reusing SET_CONSOLE_OUTPUT_INFO_ATTR is a tempting idea; did you check
> that how is done on native? (it's a matter of knowning if the "current"
> color from an ANSI seq is also the default screen buffer color; or when
> two programs are attached to the same conhost, they share the "current"
> ANSI color)
> * MR should be tested for both wineconsole and unix console
- style: please don't use C++ comments, nor case switch ranges (they are not portable enough)
- switch case ranges removed in local branch
- style: try to stick to the current style of modified file
- style and comments adjusted in local branch
- size_t shall be printed with %Iu
- i had a conflict for size_t on my local machine vs. build pipeline build32
```
%lu for size_t?!
on my 64bit system
size_t {aka long long unsigned int}
VS
pipeline build32
size_t {aka unsigned int}
```
- escape sequences should only be interpreted when console output mode has the vt seq bit set
- Is ENABLE_VIRTUAL_TERMINAL_PROCESSING the correct bit? It is not checked for other sequences like \t or \n
- some non regression tests could be useful (no precise idea on what they should cover)
- if you have some suggestions or hints how this should look like, i will try to implement some test
- reusing SET_CONSOLE_OUTPUT_INFO_ATTR is a tempting idea; did you check that how is done on native? (it's a matter of knowning if the "current" color from an ANSI seq is also the default screen buffer color;
- the basic SGR sequences use ANSI color, but the 38 and 48 commands can use RGB values too, therefore my simple approach to convert RGB to ANSI color. Midnight Commander uses this RGB values for setting color, but seems to stick to ANSI colors, but i am pretty sure that MC allows to modify its color scheme
- or when two programs are attached to the same conhost, they share the "current" ANSI color)
- what do you mean by share current ANSI color? each screenbuffer stores its color per character/data, this should not conflict between multiple consoles
- MR should be tested for both wineconsole and unix console
- how can i test unix console? do i have to read the code to figure it out or is there a wiki/howto?
**In general:**
Should i push additional commits to the branch or should i rebase and fix the already existing commits?
--
https://gitlab.winehq.org/wine/wine/-/merge_requests/9973#note_128075
size_t shall be printed with %Iu
* i had a conflict for size_t on my local machine vs. build pipeline build32
you should use %\<uppercase I\>u for size_t (that will work on both 32 & 64bit), and no need to cast integer
Is ENABLE_VIRTUAL_TERMINAL_PROCESSING the correct bit?
yes
It is not checked for other sequences like \\t or \\n
this should be handled by another bit in the output mode about auto folding lines larger than console IIRC reusing `SET_CONSOLE_OUTPUT_INFO_ATT`R what I mean here is there are basically different types of console APIs: * old ones (referring eg to screen buffer) * new ones, closer to unix pty:s and escape seq when you use `SET_CONSOLE_OUTPUT_INFO_ATTR` it's like setting the default screen buffer color in old console APIs what should be tested is something like: * write esc seq to change current color in red * don't emit esc seq to go back to default color * use old API to get screen buffer color and check the result (GetConsoleScreenBufferInfo)
`What do you mean by share current ANSI color? each screenbuffer stores its color per character/data, this should not conflict between multiple consoles`
you can have several programs attached to the same console (and screen buffer)
that's what happens eg when you start mc.exe from cmd.exe; they both will be attached to the same console (yes, cmd suspends itself when mc runs, but that's just cmd's behavior; both programs could read/write to the console at the same time)
how can i test unix console?
running ./wine foo.exe from a unix shell will run foo.exe into a unix console running ./wine wineconsole foo (from a unix shell) will run foo.exe into a (user32) console
Should i push additional commits to the branch or should i rebase and fix the already existing commits?
rebase -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9973#note_128116
size_t shall be printed with %Iu
* i had a conflict for size_t on my local machine vs. build pipeline build32
you should use %\<uppercase I\>u for size_t (that will work on both 32 & 64bit), and no need to cast integer
okay, done
Is ENABLE_VIRTUAL_TERMINAL_PROCESSING the correct bit?
yes
It is not checked for other sequences like \\t or \\n
this should be handled by another bit in the output mode about auto folding lines larger than console IIRC
this needs more research, but actual WIN10 enables it by default ans seams to ignore HKCU\Console\VirtualTerminalLevel
reusing `SET_CONSOLE_OUTPUT_INFO_ATTR`
what I mean here is there are basically different types of console APIs:
* old ones (referring eg to screen buffer) * new ones, closer to unix pty:s and escape seq
when you use `SET_CONSOLE_OUTPUT_INFO_ATTR` it's like setting the default screen buffer color in old console APIs
to use newer API additional rewrite is need as far as i see. SetBkColor for example is not supported from conhost.c but in window.c, but if that is the way to go i will do that
what should be tested is something like:
* write esc seq to change current color in red * don't emit esc seq to go back to default color * use old API to get screen buffer color and check the result (GetConsoleScreenBufferInfo)
okay, i will have a look
`What do you mean by share current ANSI color? each screenbuffer stores its color per character/data, this should not conflict between multiple consoles`
you can have several programs attached to the same console (and screen buffer)
that's what happens eg when you start mc.exe from cmd.exe; they both will be attached to the same console (yes, cmd suspends itself when mc runs, but that's just cmd's behavior; both programs could read/write to the console at the same time)
Got it. Do you have a real world scenario i can test
how can i test unix console?
running ./wine foo.exe from a unix shell will run foo.exe into a unix console
running ./wine wineconsole foo (from a unix shell) will run foo.exe into a (user32) console
Got it. unix shell already supports esc sequences, my code does not inter fer.
Should i push additional commits to the branch or should i rebase and fix the already existing commits?
rebase
okay, will do it -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9973#note_128320
participants (3)
-
eric pouech (@epo) -
Thomas Csovcsity -
Thomas Csovcsity (@thc13)