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