[PATCH v2 0/2] MR11582: 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 -- v2: conhost: Add basic VT control sequence processing. 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 | 16 +++ programs/conhost/conhost.c | 250 ++++++++++++++++++++++++++++++++++ programs/conhost/conhost.h | 2 + 3 files changed, 268 insertions(+) diff --git a/dlls/kernel32/tests/console.c b/dlls/kernel32/tests/console.c index 98b4e89c64b..daea0e31b1d 100644 --- a/dlls/kernel32/tests/console.c +++ b/dlls/kernel32/tests/console.c @@ -5800,6 +5800,22 @@ static void test_ANSI_escape_sequences(void) "Incorrect Y cursor position: got %d, expected %d\n", sb_info.dwCursorPosition.Y, 0); + /* Test split CSI sequence across multiple WriteConsole calls */ + ret = SetConsoleCursorPosition(hConOut, c); + ok(ret, "SetConsoleCursorPosition failed: %lu\n", GetLastError()); + ret = WriteConsoleW(hConOut, L"\x1b[31", 4, &dw, NULL); + ok(dw == 4, "Wrong count: %lu\n", dw); + ret = WriteConsoleW(hConOut, L"mSPLIT", 6, &dw, NULL); + ok(dw == 6, "Wrong count: %lu\n", dw); + ret = GetConsoleScreenBufferInfo(hConOut, &sb_info); + ok(ret, "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError()); + ok(sb_info.dwCursorPosition.X == 5, + "Incorrect X cursor position: got %d, expected %d\n", + sb_info.dwCursorPosition.X, 5); + ok(sb_info.wAttributes == FOREGROUND_RED, + "Unexpected attributes: got %x, expected %x\n", + sb_info.wAttributes, FOREGROUND_RED); + CloseHandle(hConOut); FreeConsole(); } diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 9c9edc999d1..3686cd3d477 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -2104,6 +2104,206 @@ 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; + } +} + +/* try to consume a CSI escape sequence at buffer[pos]. returns CSI_PROCESSED + * if the sequence was dispatched, CSI_STASHED if it was incomplete and saved + * for the next write_console call, or CSI_NOT_FOUND if it was not a valid CSI. */ +enum csi_result { CSI_NOT_FOUND, CSI_PROCESSED, CSI_STASHED }; + +static enum csi_result try_consume_csi( struct screen_buffer *screen_buffer, + const WCHAR *buffer, size_t len, size_t *pos ) +{ + size_t start, i; + + if (*pos + 1 >= len || buffer[*pos] != '\e' || buffer[*pos + 1] != '[') + return CSI_NOT_FOUND; + + start = *pos + 2; /* after \e[ */ + i = start; + + /* consume parameter bytes (0x30-0x3f) and intermediate bytes (0x20-0x2f) */ + while (i < len && + ((buffer[i] >= 0x30 && buffer[i] <= 0x3f) || + (buffer[i] >= 0x20 && buffer[i] <= 0x2f))) + i++; + + /* final byte (0x40-0x7e) */ + if (i < len && buffer[i] >= 0x40 && buffer[i] <= 0x7e) + { + process_csi( screen_buffer, buffer + start, i - start + 1 ); + *pos = i; + return CSI_PROCESSED; + } + + /* incomplete sequence at end of buffer - stash for next call */ + if (i == len) + { + size_t seq_len = len - *pos; + if (seq_len < ARRAY_SIZE( screen_buffer->csi_buf )) + { + memcpy( screen_buffer->csi_buf, buffer + *pos, seq_len * sizeof(WCHAR) ); + screen_buffer->csi_buf_len = seq_len; + } + *pos = len - 1; + return CSI_STASHED; + } + + /* not a valid CSI sequence */ + return CSI_NOT_FOUND; +} + static void play_console_beep( struct console *console ) { if (console->is_unix) @@ -2127,8 +2327,58 @@ 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++) { + /* try to consume a CSI sequence when VT processing is enabled */ + if ((screen_buffer->mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) && buffer[i] == '\e') + { + int csi = try_consume_csi( screen_buffer, buffer, len, &i ); + if (csi == CSI_PROCESSED) continue; + if (csi == CSI_STASHED) return STATUS_SUCCESS; + /* not a valid CSI sequence — write '\e' literally */ + } + if (screen_buffer->mode & ENABLE_PROCESSED_OUTPUT) { switch (buffer[i]) 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)