[PATCH 0/2] MR11582: Draft: conhost: Add basic VT control sequence processing.
Based on work by Thomas Csovcsity in !9973 Adds basic CSI sequence processing in write_console: \- SGR: ANSI 16-color foreground/background, bright variants, reset, 256-color, 24-bit RGB mapped to 16-color palette \- CUP: cursor positioning via `\e[row;colH` Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=49780 -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11582
From: Lokesh Poovaragan <lokesh.poovaragan@gmail.com> Based on work by Thomas Csovcsity. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=49780 --- dlls/kernel32/tests/console.c | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/dlls/kernel32/tests/console.c b/dlls/kernel32/tests/console.c index 3c3538041f4..98b4e89c64b 100644 --- a/dlls/kernel32/tests/console.c +++ b/dlls/kernel32/tests/console.c @@ -5717,6 +5717,93 @@ static void test_FreeConsoleStd(void) } } +static void test_ANSI_escape_sequences(void) +{ + CONSOLE_SCREEN_BUFFER_INFO sb_info; + HANDLE hConOut; + BOOL ret; + DWORD mode, dw; + COORD c = {}; + + FreeConsole(); + AllocConsole(); + hConOut = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0); + + ret = SetConsoleMode(hConOut, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + ok(ret, "SetConsoleMode failed: %lu\n", GetLastError()); + + ret = GetConsoleMode(hConOut, &mode); + ok(ret, "GetConsoleMode failed: %lu\n", GetLastError()); + ok(mode == (ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING), + "Unexpected mode: %lx\n", mode); + + /* Test SGR foreground color */ + ret = SetConsoleCursorPosition(hConOut, c); + ok(ret, "SetConsoleCursorPosition failed: %lu\n", GetLastError()); + ret = SetConsoleTextAttribute(hConOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY); + ok(ret, "SetConsoleTextAttribute failed: %lu\n", GetLastError()); + + ret = WriteConsoleW(hConOut, L"GREEN\x1b[91mBRIGHT RED", 5 + 5 + 10, &dw, NULL); + ok(dw == 5 + 5 + 10, "Wrong count: %lu\n", dw); + ok(ret, "WriteConsole failed: %lu\n", GetLastError()); + + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + todo_wine ok(sb_info.dwCursorPosition.X == 5 + 10, + "Incorrect X cursor position: got %d, expected %d\n", + sb_info.dwCursorPosition.X, 5 + 10); + todo_wine ok(sb_info.wAttributes == (FOREGROUND_RED | FOREGROUND_INTENSITY), + "Unexpected attributes: got %x, expected %x\n", + sb_info.wAttributes, FOREGROUND_RED | FOREGROUND_INTENSITY); + + /* Test SGR reset to default */ + ret = SetConsoleTextAttribute(hConOut, FOREGROUND_BLUE | FOREGROUND_INTENSITY); + ok(ret, "SetConsoleTextAttribute failed: %lu\n", GetLastError()); + + ret = WriteConsoleW(hConOut, L"BLUE\x1b[m", 4 + 3, &dw, NULL); + ok(dw == 4 + 3, "Wrong count: %lu\n", dw); + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + todo_wine ok(sb_info.wAttributes == (FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN), + "Unexpected attributes: got %x, expected %x\n", + sb_info.wAttributes, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN); + + /* Test SGR foreground and background */ + ret = WriteConsoleW(hConOut, L"\n\x1b[31mRED", 1 + 5 + 3, &dw, NULL); + ok(dw == 1 + 5 + 3, "Wrong count: %lu\n", dw); + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + todo_wine ok(sb_info.dwCursorPosition.X == 3, + "Incorrect X cursor position: got %d, expected %d\n", + sb_info.dwCursorPosition.X, 3); + todo_wine ok(sb_info.wAttributes == FOREGROUND_RED, + "Unexpected attributes: got %x, expected %x\n", + sb_info.wAttributes, FOREGROUND_RED); + + ret = WriteConsoleW(hConOut, L"\x1b[31;44mRED on BLUE", 5 + 14, &dw, NULL); + ok(dw == 5 + 14, "Wrong count: %lu\n", dw); + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + todo_wine ok(sb_info.wAttributes == (FOREGROUND_RED | BACKGROUND_BLUE), + "Unexpected attributes: got %x, expected %x\n", + sb_info.wAttributes, FOREGROUND_RED | BACKGROUND_BLUE); + + /* Test CUP cursor positioning */ + ret = WriteConsoleW(hConOut, L"\x1b[1;1H", 6, &dw, NULL); + ok(dw == 6, "Wrong count: %lu\n", dw); + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + todo_wine ok(sb_info.dwCursorPosition.X == 0, + "Incorrect X cursor position: got %d, expected %d\n", + sb_info.dwCursorPosition.X, 0); + todo_wine ok(sb_info.dwCursorPosition.Y == 0, + "Incorrect Y cursor position: got %d, expected %d\n", + sb_info.dwCursorPosition.Y, 0); + + CloseHandle(hConOut); + FreeConsole(); +} + START_TEST(console) { HANDLE hConIn, hConOut, revert_output = NULL, unbound_output; @@ -6018,6 +6105,7 @@ START_TEST(console) test_AttachConsole(hConOut); test_AllocConsole(); test_FreeConsole(hConIn, hConOut); + test_ANSI_escape_sequences(); test_condrv_server_as_root_directory(); test_CreateProcessCUI(); test_CtrlHandlerSubsystem(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11582
From: Lokesh Poovaragan <lokesh.poovaragan@gmail.com> Implement CSI sequence parsing in write_console with support for: - SGR (Select Graphic Rendition) for ANSI 16-color foreground and background, including bright variants and reset (0/39/49). - CUP (Cursor Position) for moving the cursor to absolute row/col. - 256-color and 24-bit RGB color sequences mapped to the 16-color palette. The parser handles incomplete sequences split across multiple WriteConsole calls by stashing partial sequences in the screen buffer struct and resuming when new data arrives. Based on work by Thomas Csovcsity. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=49780 --- dlls/kernel32/tests/console.c | 27 ++-- programs/conhost/conhost.c | 253 ++++++++++++++++++++++++++++++++++ programs/conhost/conhost.h | 2 + 3 files changed, 274 insertions(+), 8 deletions(-) diff --git a/dlls/kernel32/tests/console.c b/dlls/kernel32/tests/console.c index 98b4e89c64b..71616917954 100644 --- a/dlls/kernel32/tests/console.c +++ b/dlls/kernel32/tests/console.c @@ -5749,12 +5749,23 @@ static void test_ANSI_escape_sequences(void) ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); - todo_wine ok(sb_info.dwCursorPosition.X == 5 + 10, + ok(sb_info.dwCursorPosition.X == 5 + 10, "Incorrect X cursor position: got %d, expected %d\n", sb_info.dwCursorPosition.X, 5 + 10); - todo_wine ok(sb_info.wAttributes == (FOREGROUND_RED | FOREGROUND_INTENSITY), + ok(sb_info.wAttributes == (FOREGROUND_RED | FOREGROUND_INTENSITY), "Unexpected attributes: got %x, expected %x\n", sb_info.wAttributes, FOREGROUND_RED | FOREGROUND_INTENSITY); + { + /* verify color via ReadConsoleOutput */ + CHAR_INFO ci; + COORD buf_size = {1, 1}, buf_coord = {0, 0}; + SMALL_RECT region = {5, 0, 5, 0}; + ret = ReadConsoleOutputW( hConOut, &ci, buf_size, buf_coord, ®ion ); + ok( ret, "ReadConsoleOutputW failed: %lu\n", GetLastError() ); + ok( ci.Char.UnicodeChar == 'B' && ci.Attributes == (FOREGROUND_RED | FOREGROUND_INTENSITY), + "ReadConsoleOutput: got char %c attr %x, expected B attr %x\n", + ci.Char.UnicodeChar, ci.Attributes, FOREGROUND_RED | FOREGROUND_INTENSITY ); + } /* Test SGR reset to default */ ret = SetConsoleTextAttribute(hConOut, FOREGROUND_BLUE | FOREGROUND_INTENSITY); @@ -5764,7 +5775,7 @@ static void test_ANSI_escape_sequences(void) ok(dw == 4 + 3, "Wrong count: %lu\n", dw); ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); - todo_wine ok(sb_info.wAttributes == (FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN), + ok(sb_info.wAttributes == (FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN), "Unexpected attributes: got %x, expected %x\n", sb_info.wAttributes, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN); @@ -5773,10 +5784,10 @@ static void test_ANSI_escape_sequences(void) ok(dw == 1 + 5 + 3, "Wrong count: %lu\n", dw); ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); - todo_wine ok(sb_info.dwCursorPosition.X == 3, + ok(sb_info.dwCursorPosition.X == 3, "Incorrect X cursor position: got %d, expected %d\n", sb_info.dwCursorPosition.X, 3); - todo_wine ok(sb_info.wAttributes == FOREGROUND_RED, + ok(sb_info.wAttributes == FOREGROUND_RED, "Unexpected attributes: got %x, expected %x\n", sb_info.wAttributes, FOREGROUND_RED); @@ -5784,7 +5795,7 @@ static void test_ANSI_escape_sequences(void) ok(dw == 5 + 14, "Wrong count: %lu\n", dw); ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); - todo_wine ok(sb_info.wAttributes == (FOREGROUND_RED | BACKGROUND_BLUE), + ok(sb_info.wAttributes == (FOREGROUND_RED | BACKGROUND_BLUE), "Unexpected attributes: got %x, expected %x\n", sb_info.wAttributes, FOREGROUND_RED | BACKGROUND_BLUE); @@ -5793,10 +5804,10 @@ static void test_ANSI_escape_sequences(void) ok(dw == 6, "Wrong count: %lu\n", dw); ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); - todo_wine ok(sb_info.dwCursorPosition.X == 0, + ok(sb_info.dwCursorPosition.X == 0, "Incorrect X cursor position: got %d, expected %d\n", sb_info.dwCursorPosition.X, 0); - todo_wine ok(sb_info.dwCursorPosition.Y == 0, + ok(sb_info.dwCursorPosition.Y == 0, "Incorrect Y cursor position: got %d, expected %d\n", sb_info.dwCursorPosition.Y, 0); diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 9c9edc999d1..7f4be34d686 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2104,6 +2104,159 @@ static NTSTATUS set_output_info( struct screen_buffer *screen_buffer, return STATUS_SUCCESS; } +static const unsigned int ansi_colors[] = {0, FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_GREEN | FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_BLUE | FOREGROUND_RED, + FOREGROUND_BLUE | FOREGROUND_GREEN, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED}; + +/* parse the next integer argument from a CSI sequence; returns the number of + * characters consumed, or 0 if no valid argument was found */ +static size_t next_csi_arg( const WCHAR *seq, size_t len, unsigned int *value ) +{ + size_t i; + + *value = 0; + for (i = 0; i < len; i++) + { + if (seq[i] == ';' || seq[i] == 'm' || seq[i] == 'H') return i + 1; + if (seq[i] < '0' || seq[i] > '9') return 0; + *value = *value * 10 + seq[i] - '0'; + } + return 0; +} + +/* process a complete CSI control sequence */ +static void process_csi( struct screen_buffer *screen_buffer, const WCHAR *seq, size_t len ) +{ + struct condrv_output_info_params info_params; + unsigned int arg, r, g, b; + BOOL is_bright; + size_t pos; + int color; + + if (!len) return; + + switch (seq[len - 1]) + { + case 'm': /* SGR - Select Graphic Rendition */ + info_params.mask = SET_CONSOLE_OUTPUT_INFO_ATTR; + info_params.info.attr = screen_buffer->attr; + + pos = 0; + while (pos < len) + { + size_t consumed = next_csi_arg( seq + pos, len - pos, &arg ); + if (!consumed) break; + pos += consumed; + + is_bright = FALSE; + if (arg >= 90 && arg <= 97) + { + is_bright = TRUE; + arg -= 60; + } + else if (arg >= 100 && arg <= 107) + { + is_bright = TRUE; + arg -= 60; + } + + if (arg >= 30 && arg <= 37) + { + info_params.info.attr = (info_params.info.attr & 0xf0) | ansi_colors[arg - 30]; + if (is_bright) info_params.info.attr |= FOREGROUND_INTENSITY; + } + else if (arg >= 40 && arg <= 47) + { + info_params.info.attr = (info_params.info.attr & 0x0f) | (ansi_colors[arg - 40] << 4); + if (is_bright) info_params.info.attr |= BACKGROUND_INTENSITY; + } + else if (arg == 38 || arg == 48) + { + BOOL is_background = (arg == 48); + + /* 256-color or 24-bit color */ + consumed = next_csi_arg( seq + pos, len - pos, &arg ); + if (!consumed) break; + pos += consumed; + + if (arg == 5) /* 256-color */ + { + consumed = next_csi_arg( seq + pos, len - pos, &arg ); + if (!consumed) break; + pos += consumed; + color = ansi_colors[arg % 8]; + if (arg >= 8) color |= FOREGROUND_INTENSITY; + } + else if (arg == 2) /* 24-bit RGB */ + { + consumed = next_csi_arg( seq + pos, len - pos, &r ); + if (!consumed) break; + pos += consumed; + consumed = next_csi_arg( seq + pos, len - pos, &g ); + if (!consumed) break; + pos += consumed; + consumed = next_csi_arg( seq + pos, len - pos, &b ); + if (!consumed) break; + pos += consumed; + color = 0; + if (r > 127) color |= FOREGROUND_RED; + if (g > 127) color |= FOREGROUND_GREEN; + if (b > 127) color |= FOREGROUND_BLUE; + } + else break; + + if (is_background) + info_params.info.attr = (info_params.info.attr & 0x0f) | (color << 4); + else + info_params.info.attr = (info_params.info.attr & 0xf0) | color; + } + else if (arg == 39) + { + info_params.info.attr = (info_params.info.attr & 0xf0) | 7; /* default foreground */ + } + else if (arg == 49) + { + info_params.info.attr = (info_params.info.attr & 0x0f); /* default background */ + } + else if (arg == 0) + { + info_params.info.attr = 7; /* white on black */ + } + } + set_output_info( screen_buffer, &info_params, sizeof(info_params) ); + break; + + case 'H': /* CUP - Cursor Position */ + { + unsigned int x = 1, y = 1; + + pos = 0; + if (pos < len && seq[pos] != ';' && seq[pos] != 'H') + { + next_csi_arg( seq + pos, len - pos, &y ); + while (pos < len && seq[pos] != ';' && seq[pos] != 'H') pos++; + } + if (pos < len && seq[pos] == ';') + { + pos++; + if (pos < len && seq[pos] != 'H') + { + next_csi_arg( seq + pos, len - pos, &x ); + } + } + if (y > 0) y--; + if (x > 0) x--; + screen_buffer->cursor_x = min( x, screen_buffer->width - 1 ); + screen_buffer->cursor_y = min( y, screen_buffer->height - 1 ); + } + break; + + default: + WARN( "unhandled CSI sequence final byte %c (0x%02x)\n", seq[len - 1], seq[len - 1] ); + break; + } +} + static void play_console_beep( struct console *console ) { if (console->is_unix) @@ -2127,6 +2280,47 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR empty_update_rect( screen_buffer, &update_rect ); + /* resume an incomplete CSI sequence from a previous write */ + if (screen_buffer->csi_buf_len > 0) + { + size_t pos, new_len; + + for (pos = 0; pos < len; pos++) + if (!((buffer[pos] >= 0x30 && buffer[pos] <= 0x3f) || + (buffer[pos] >= 0x20 && buffer[pos] <= 0x2f))) + break; + + if (pos < len && buffer[pos] >= 0x40 && buffer[pos] <= 0x7e) + { + /* complete the sequence */ + WCHAR full_seq[128]; + new_len = screen_buffer->csi_buf_len + pos + 1; + if (new_len <= ARRAY_SIZE(full_seq)) + { + memcpy( full_seq, screen_buffer->csi_buf, + screen_buffer->csi_buf_len * sizeof(WCHAR) ); + memcpy( full_seq + screen_buffer->csi_buf_len, buffer, + (pos + 1) * sizeof(WCHAR) ); + screen_buffer->csi_buf_len = 0; + process_csi( screen_buffer, full_seq + 2, new_len - 2 ); + buffer += pos + 1; + len -= pos + 1; + } + } + else + { + /* still incomplete or invalid - stash more data */ + new_len = screen_buffer->csi_buf_len + min( len, pos ); + if (new_len < ARRAY_SIZE(screen_buffer->csi_buf)) + { + memcpy( screen_buffer->csi_buf + screen_buffer->csi_buf_len, + buffer, min( len, pos ) * sizeof(WCHAR) ); + screen_buffer->csi_buf_len = new_len; + } + return STATUS_SUCCESS; + } + } + for (i = 0; i < len; i++) { if (screen_buffer->mode & ENABLE_PROCESSED_OUTPUT) @@ -2158,6 +2352,65 @@ static NTSTATUS write_console( struct screen_buffer *screen_buffer, const WCHAR case '\r': screen_buffer->cursor_x = 0; continue; + case '\e': + if ((screen_buffer->mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) && i + 1 < len + && buffer[i + 1] == '[') + { + size_t start = i + 2; + i += 2; + /* consume CSI sequence: params 0x30-0x3f, intermediates 0x20-0x2f, + * final byte 0x40-0x7e */ + while (i < len && + ((buffer[i] >= 0x30 && buffer[i] <= 0x3f) || + (buffer[i] >= 0x20 && buffer[i] <= 0x2f))) + i++; + if (i < len && buffer[i] >= 0x40 && buffer[i] <= 0x7e) + { + process_csi( screen_buffer, buffer + start, i - start + 1 ); + continue; + } + /* incomplete sequence at end of buffer - stash for next call */ + if (i == len) + { + size_t seq_len = len - (start - 2); + if (seq_len < ARRAY_SIZE(screen_buffer->csi_buf)) + { + memcpy( screen_buffer->csi_buf, buffer + start - 2, + seq_len * sizeof(WCHAR) ); + screen_buffer->csi_buf_len = seq_len; + } + return STATUS_SUCCESS; + } + /* unrecognized sequence - write literally */ + } + break; + } + } + else if ((screen_buffer->mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) && buffer[i] == '\e' + && i + 1 < len && buffer[i + 1] == '[') + { + /* CSI when ENABLE_PROCESSED_OUTPUT is not set */ + size_t start = i + 2; + i += 2; + while (i < len && + ((buffer[i] >= 0x30 && buffer[i] <= 0x3f) || + (buffer[i] >= 0x20 && buffer[i] <= 0x2f))) + i++; + if (i < len && buffer[i] >= 0x40 && buffer[i] <= 0x7e) + { + process_csi( screen_buffer, buffer + start, i - start + 1 ); + continue; + } + if (i == len) + { + size_t seq_len = len - (start - 2); + if (seq_len < ARRAY_SIZE(screen_buffer->csi_buf)) + { + memcpy( screen_buffer->csi_buf, buffer + start - 2, + seq_len * sizeof(WCHAR) ); + screen_buffer->csi_buf_len = seq_len; + } + return STATUS_SUCCESS; } } if (screen_buffer->cursor_x == screen_buffer->width && !(screen_buffer->mode & ENABLE_WRAP_AT_EOL_OUTPUT)) diff --git a/programs/conhost/conhost.h b/programs/conhost/conhost.h index be17cf5eaae..8d61c0e3668 100644 --- a/programs/conhost/conhost.h +++ b/programs/conhost/conhost.h @@ -132,6 +132,8 @@ struct screen_buffer RECT win; /* current visible window on the screen buffer */ struct font_info font; /* console font information */ struct wine_rb_entry entry; /* map entry */ + WCHAR csi_buf[64]; /* buffer for incomplete CSI sequence */ + size_t csi_buf_len; /* length of buffered partial sequence */ }; /* conhost.c */ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11582
participants (2)
-
Lokesh Poovaragan -
Lokesh Poovaragan (@lokeshpoovaragan)