From 77ca25902643240b5f162d6c6fce1a79d12668b1 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 7 Dec 2025 01:11:05 +0100 Subject: [PATCH 01/16] console: allow using arrow left/right to edit the line (with UTF-8 support) --- common/console.cpp | 171 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 143 insertions(+), 28 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 078a8d678d9..a7c9c1644c3 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -1,6 +1,7 @@ #include "console.h" #include #include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -336,20 +337,55 @@ namespace console { } // Helper function to remove the last UTF-8 character from a string - static void pop_back_utf8_char(std::string & line) { - if (line.empty()) { - return; + static size_t prev_utf8_char_pos(const std::string & line, size_t pos) { + if (pos == 0) return 0; + pos--; + while (pos > 0 && (line[pos] & 0xC0) == 0x80) { + pos--; } + return pos; + } - size_t pos = line.length() - 1; + static size_t next_utf8_char_pos(const std::string & line, size_t pos) { + if (pos >= line.length()) return line.length(); + pos++; + while (pos < line.length() && (line[pos] & 0xC0) == 0x80) { + pos++; + } + return pos; + } - // Find the start of the last UTF-8 character (checking up to 4 bytes back) - for (size_t i = 0; i < 3 && pos > 0; ++i, --pos) { - if ((line[pos] & 0xC0) != 0x80) { - break; // Found the start of the character + static void move_cursor(int delta) { + if (delta == 0) return; +#if defined(_WIN32) + if (hConsole != NULL) { + CONSOLE_SCREEN_BUFFER_INFO bufferInfo; + GetConsoleScreenBufferInfo(hConsole, &bufferInfo); + COORD newCursorPosition = bufferInfo.dwCursorPosition; + int width = bufferInfo.dwSize.X; + int newX = newCursorPosition.X + delta; + int newY = newCursorPosition.Y; + + while (newX >= width) { + newX -= width; + newY++; } + while (newX < 0) { + newX += width; + newY--; + } + + newCursorPosition.X = newX; + newCursorPosition.Y = newY; + SetConsoleCursorPosition(hConsole, newCursorPosition); + } +#else + if (delta < 0) { + for (int i = 0; i < -delta; i++) fprintf(out, "\b"); + } else { + for (int i = 0; i < delta; i++) fprintf(out, "\033[C"); } - line.erase(pos); +#endif } static bool readline_advanced(std::string & line, bool multiline_input) { @@ -362,8 +398,14 @@ namespace console { bool is_special_char = false; bool end_of_stream = false; + size_t byte_pos = 0; // current byte index + size_t char_pos = 0; // current character index (one char can be multiple bytes) + char32_t input_char; while (true) { + assert(char_pos <= byte_pos); + assert(char_pos <= widths.size()); + fflush(out); // Ensure all output is displayed before waiting for input input_char = getchar32(); @@ -384,7 +426,35 @@ namespace console { if (input_char == '\033') { // Escape sequence char32_t code = getchar32(); - if (code == '[' || code == 0x1B) { + if (code == '[') { + code = getchar32(); + if (code == 'D') { // left + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + byte_pos = prev_utf8_char_pos(line, byte_pos); + } + } else if (code == 'C') { // right + if (char_pos < widths.size()) { + int w = widths[char_pos]; + move_cursor(w); + char_pos++; + byte_pos = next_utf8_char_pos(line, byte_pos); + } + } else if (code == 'A' || code == 'B') { + // up/down + // TODO: Implement history navigation + } else { + // Discard the rest of the escape sequence + while ((code = getchar32()) != (char32_t) WEOF) { + if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') { + break; + } + } + } + // TODO: Handle Ctrl+Arrow + } else if (code == 0x1B) { // Discard the rest of the escape sequence while ((code = getchar32()) != (char32_t) WEOF) { if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') { @@ -393,27 +463,72 @@ namespace console { } } } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace - if (!widths.empty()) { - int count; - do { - count = widths.back(); - widths.pop_back(); - // Move cursor back, print space, and move cursor back again - for (int i = 0; i < count; i++) { - replace_last(' '); - pop_cursor(); - } - pop_back_utf8_char(line); - } while (count == 0 && !widths.empty()); + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + size_t prev_pos = prev_utf8_char_pos(line, byte_pos); + size_t char_len = byte_pos - prev_pos; + byte_pos = prev_pos; + + // remove the character + line.erase(byte_pos, char_len); + widths.erase(widths.begin() + char_pos); + + // redraw tail + size_t p = byte_pos; + int tail_width = 0; + for (size_t i = char_pos; i < widths.size(); ++i) { + size_t next_p = next_utf8_char_pos(line, p); + put_codepoint(line.c_str() + p, next_p - p, widths[i]); + tail_width += widths[i]; + p = next_p; + } + + // clear display + for (int i = 0; i < w; ++i) { + fputc(' ', out); + } + move_cursor(-(tail_width + w)); } } else { - int offset = line.length(); - append_utf8(input_char, line); - int width = put_codepoint(line.c_str() + offset, line.length() - offset, estimateWidth(input_char)); - if (width < 0) { - width = 0; + // insert character + std::string new_char_str; + append_utf8(input_char, new_char_str); + int w = estimateWidth(input_char); + + if (char_pos == widths.size()) { + // insert at the end + line += new_char_str; + int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w); + if (real_w < 0) real_w = 0; + widths.push_back(real_w); + byte_pos += new_char_str.length(); + char_pos++; + } else { + // insert in middle + line.insert(byte_pos, new_char_str); + + int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w); + if (real_w < 0) real_w = 0; + + widths.insert(widths.begin() + char_pos, real_w); + + // print the tail + size_t p = byte_pos + new_char_str.length(); + int tail_width = 0; + for (size_t i = char_pos + 1; i < widths.size(); ++i) { + size_t next_p = next_utf8_char_pos(line, p); + put_codepoint(line.c_str() + p, next_p - p, widths[i]); + tail_width += widths[i]; + p = next_p; + } + + move_cursor(-tail_width); + + byte_pos += new_char_str.length(); + char_pos++; } - widths.push_back(width); } if (!line.empty() && (line.back() == '\\' || line.back() == '/')) { From 75a412e8abfb73c6a94453e5fa51f9b2eaf665ad Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 03:16:12 +0100 Subject: [PATCH 02/16] console: fix arrow keys on Windows using private-use Unicode --- common/console.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/common/console.cpp b/common/console.cpp index a7c9c1644c3..03a0d9bfa8b 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -36,6 +36,16 @@ namespace console { + namespace { + // Use private-use unicode values to represent special keys that are not reported + // as characters (e.g. arrows on Windows). These values should never clash with + // real input and let the rest of the code handle navigation uniformly. + static constexpr char32_t KEY_ARROW_LEFT = 0xE000; + static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; + static constexpr char32_t KEY_ARROW_UP = 0xE002; + static constexpr char32_t KEY_ARROW_DOWN = 0xE003; + } + // // Console state // @@ -176,6 +186,15 @@ namespace console { if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) { wchar_t wc = record.Event.KeyEvent.uChar.UnicodeChar; + if (wc == 0) { + switch (record.Event.KeyEvent.wVirtualKeyCode) { + case VK_LEFT: return KEY_ARROW_LEFT; + case VK_RIGHT: return KEY_ARROW_RIGHT; + case VK_UP: return KEY_ARROW_UP; + case VK_DOWN: return KEY_ARROW_DOWN; + default: continue; + } + } if (wc == 0) { continue; } @@ -462,6 +481,24 @@ namespace console { } } } +#if defined(_WIN32) + } else if (input_char == KEY_ARROW_LEFT) { + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + byte_pos = prev_utf8_char_pos(line, byte_pos); + } + } else if (input_char == KEY_ARROW_RIGHT) { + if (char_pos < widths.size()) { + int w = widths[char_pos]; + move_cursor(w); + char_pos++; + byte_pos = next_utf8_char_pos(line, byte_pos); + } + } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { + // TODO: Implement history navigation +#endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace if (char_pos > 0) { int w = widths[char_pos - 1]; From 5e19ce2355b32c138c652776a413a8588ae89d90 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 03:36:42 +0100 Subject: [PATCH 03/16] console: add Home/End key support for Windows and Linux --- common/console.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/common/console.cpp b/common/console.cpp index 03a0d9bfa8b..78bc2d46752 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -44,6 +44,8 @@ namespace console { static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; static constexpr char32_t KEY_ARROW_UP = 0xE002; static constexpr char32_t KEY_ARROW_DOWN = 0xE003; + static constexpr char32_t KEY_HOME = 0xE004; + static constexpr char32_t KEY_END = 0xE005; } // @@ -192,6 +194,8 @@ namespace console { case VK_RIGHT: return KEY_ARROW_RIGHT; case VK_UP: return KEY_ARROW_UP; case VK_DOWN: return KEY_ARROW_DOWN; + case VK_HOME: return KEY_HOME; + case VK_END: return KEY_END; default: continue; } } @@ -374,6 +378,28 @@ namespace console { return pos; } + static void move_cursor(int delta); + + static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths) { + int back_width = 0; + for (size_t i = 0; i < char_pos; ++i) { + back_width += widths[i]; + } + move_cursor(-back_width); + char_pos = 0; + byte_pos = 0; + } + + static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line) { + int forward_width = 0; + for (size_t i = char_pos; i < widths.size(); ++i) { + forward_width += widths[i]; + } + move_cursor(forward_width); + char_pos = widths.size(); + byte_pos = line.length(); + } + static void move_cursor(int delta) { if (delta == 0) return; #if defined(_WIN32) @@ -461,9 +487,32 @@ namespace console { char_pos++; byte_pos = next_utf8_char_pos(line, byte_pos); } + } else if (code == 'H') { // home + move_to_line_start(char_pos, byte_pos, widths); + } else if (code == 'F') { // end + move_to_line_end(char_pos, byte_pos, widths, line); } else if (code == 'A' || code == 'B') { // up/down // TODO: Implement history navigation + } else if (code >= '0' && code <= '9') { + std::string digits; + digits.push_back(static_cast(code)); + while (true) { + code = getchar32(); + if (code >= '0' && code <= '9') { + digits.push_back(static_cast(code)); + continue; + } + break; + } + + if (code == '~') { + if (digits == "1" || digits == "7") { + move_to_line_start(char_pos, byte_pos, widths); + } else if (digits == "4" || digits == "8") { + move_to_line_end(char_pos, byte_pos, widths, line); + } + } } else { // Discard the rest of the escape sequence while ((code = getchar32()) != (char32_t) WEOF) { @@ -496,6 +545,10 @@ namespace console { char_pos++; byte_pos = next_utf8_char_pos(line, byte_pos); } + } else if (input_char == KEY_HOME) { + move_to_line_start(char_pos, byte_pos, widths); + } else if (input_char == KEY_END) { + move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { // TODO: Implement history navigation #endif From 6e67dd3cc01865168fc597c1e6d4f743099f77aa Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 04:09:40 +0100 Subject: [PATCH 04/16] console: add basic Up/Down history navigation --- common/console.cpp | 115 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 78bc2d46752..84b7ff29aef 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -56,6 +57,8 @@ namespace console { static bool simple_io = true; static display_t current_display = reset; + static std::vector history; + static FILE* out = stdout; #if defined (_WIN32) @@ -339,6 +342,34 @@ namespace console { #endif } + static char32_t decode_utf8(const std::string & input, size_t pos, size_t & advance) { + unsigned char c = static_cast(input[pos]); + if ((c & 0x80u) == 0u) { + advance = 1; + return c; + } + if ((c & 0xE0u) == 0xC0u && pos + 1 < input.size()) { + advance = 2; + return ((c & 0x1Fu) << 6) | (static_cast(input[pos + 1]) & 0x3Fu); + } + if ((c & 0xF0u) == 0xE0u && pos + 2 < input.size()) { + advance = 3; + return ((c & 0x0Fu) << 12) | + ((static_cast(input[pos + 1]) & 0x3Fu) << 6) | + (static_cast(input[pos + 2]) & 0x3Fu); + } + if ((c & 0xF8u) == 0xF0u && pos + 3 < input.size()) { + advance = 4; + return ((c & 0x07u) << 18) | + ((static_cast(input[pos + 1]) & 0x3Fu) << 12) | + ((static_cast(input[pos + 2]) & 0x3Fu) << 6) | + (static_cast(input[pos + 3]) & 0x3Fu); + } + + advance = 1; + return 0xFFFD; // replacement character for invalid input + } + static void append_utf8(char32_t ch, std::string & out) { if (ch <= 0x7F) { out.push_back(static_cast(ch)); @@ -379,6 +410,45 @@ namespace console { } static void move_cursor(int delta); + static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths); + static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); + + static void clear_current_line(const std::vector & widths) { + int total_width = 0; + for (int w : widths) { + total_width += (w > 0 ? w : 1); + } + + if (total_width > 0) { + std::string spaces(total_width, ' '); + fwrite(spaces.c_str(), 1, total_width, out); + move_cursor(-total_width); + } + } + + static void set_line_contents(std::string new_line, std::string & line, std::vector & widths, size_t & char_pos, + size_t & byte_pos) { + move_to_line_start(char_pos, byte_pos, widths); + clear_current_line(widths); + + line = std::move(new_line); + widths.clear(); + byte_pos = 0; + char_pos = 0; + + size_t idx = 0; + while (idx < line.size()) { + size_t advance = 0; + char32_t cp = decode_utf8(line, idx, advance); + int expected_width = estimateWidth(cp); + int real_width = put_codepoint(line.c_str() + idx, advance, expected_width); + if (real_width < 0) real_width = 0; + widths.push_back(real_width); + idx += advance; + ++char_pos; + byte_pos = idx; + } + } static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths) { int back_width = 0; @@ -442,6 +512,7 @@ namespace console { std::vector widths; bool is_special_char = false; bool end_of_stream = false; + size_t history_index = history.size(); size_t byte_pos = 0; // current byte index size_t char_pos = 0; // current character index (one char can be multiple bytes) @@ -493,7 +564,23 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } else if (code == 'A' || code == 'B') { // up/down - // TODO: Implement history navigation + if (!history.empty()) { + if (code == 'A' && history_index > 0) { + history_index--; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (code == 'B') { + if (history_index + 1 < history.size()) { + history_index++; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents("", line, widths, char_pos, byte_pos); + is_special_char = false; + } + } + } } else if (code >= '0' && code <= '9') { std::string digits; digits.push_back(static_cast(code)); @@ -550,7 +637,23 @@ namespace console { } else if (input_char == KEY_END) { move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { - // TODO: Implement history navigation + if (!history.empty()) { + if (input_char == KEY_ARROW_UP && history_index > 0) { + history_index--; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (input_char == KEY_ARROW_DOWN) { + if (history_index + 1 < history.size()) { + history_index++; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents("", line, widths, char_pos, byte_pos); + is_special_char = false; + } + } + } #endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace if (char_pos > 0) { @@ -656,6 +759,14 @@ namespace console { } } + if (!end_of_stream && !line.empty()) { + std::string history_entry = line; + if (!history_entry.empty() && history_entry.back() == '\n') { + history_entry.pop_back(); + } + history.push_back(std::move(history_entry)); + } + fflush(out); return has_more; } From 440ac832b52b6aaeafe3c507afc392e37cfe8d2a Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 7 Dec 2025 13:31:03 +0100 Subject: [PATCH 05/16] fix build --- common/console.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 84b7ff29aef..46f67e059ac 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -37,6 +37,7 @@ namespace console { +#if defined(_WIN32) namespace { // Use private-use unicode values to represent special keys that are not reported // as characters (e.g. arrows on Windows). These values should never clash with @@ -48,6 +49,7 @@ namespace console { static constexpr char32_t KEY_HOME = 0xE004; static constexpr char32_t KEY_END = 0xE005; } +#endif // // Console state @@ -202,9 +204,6 @@ namespace console { default: continue; } } - if (wc == 0) { - continue; - } if ((wc >= 0xD800) && (wc <= 0xDBFF)) { // Check if wc is a high surrogate high_surrogate = wc; From 92ac8ed5f5543bb5df11f9fae37e4b3836d72f26 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 7 Dec 2025 01:11:05 +0100 Subject: [PATCH 06/16] console: allow using arrow left/right to edit the line (with UTF-8 support) --- common/console.cpp | 171 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 143 insertions(+), 28 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 078a8d678d9..a7c9c1644c3 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -1,6 +1,7 @@ #include "console.h" #include #include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -336,20 +337,55 @@ namespace console { } // Helper function to remove the last UTF-8 character from a string - static void pop_back_utf8_char(std::string & line) { - if (line.empty()) { - return; + static size_t prev_utf8_char_pos(const std::string & line, size_t pos) { + if (pos == 0) return 0; + pos--; + while (pos > 0 && (line[pos] & 0xC0) == 0x80) { + pos--; } + return pos; + } - size_t pos = line.length() - 1; + static size_t next_utf8_char_pos(const std::string & line, size_t pos) { + if (pos >= line.length()) return line.length(); + pos++; + while (pos < line.length() && (line[pos] & 0xC0) == 0x80) { + pos++; + } + return pos; + } - // Find the start of the last UTF-8 character (checking up to 4 bytes back) - for (size_t i = 0; i < 3 && pos > 0; ++i, --pos) { - if ((line[pos] & 0xC0) != 0x80) { - break; // Found the start of the character + static void move_cursor(int delta) { + if (delta == 0) return; +#if defined(_WIN32) + if (hConsole != NULL) { + CONSOLE_SCREEN_BUFFER_INFO bufferInfo; + GetConsoleScreenBufferInfo(hConsole, &bufferInfo); + COORD newCursorPosition = bufferInfo.dwCursorPosition; + int width = bufferInfo.dwSize.X; + int newX = newCursorPosition.X + delta; + int newY = newCursorPosition.Y; + + while (newX >= width) { + newX -= width; + newY++; } + while (newX < 0) { + newX += width; + newY--; + } + + newCursorPosition.X = newX; + newCursorPosition.Y = newY; + SetConsoleCursorPosition(hConsole, newCursorPosition); + } +#else + if (delta < 0) { + for (int i = 0; i < -delta; i++) fprintf(out, "\b"); + } else { + for (int i = 0; i < delta; i++) fprintf(out, "\033[C"); } - line.erase(pos); +#endif } static bool readline_advanced(std::string & line, bool multiline_input) { @@ -362,8 +398,14 @@ namespace console { bool is_special_char = false; bool end_of_stream = false; + size_t byte_pos = 0; // current byte index + size_t char_pos = 0; // current character index (one char can be multiple bytes) + char32_t input_char; while (true) { + assert(char_pos <= byte_pos); + assert(char_pos <= widths.size()); + fflush(out); // Ensure all output is displayed before waiting for input input_char = getchar32(); @@ -384,7 +426,35 @@ namespace console { if (input_char == '\033') { // Escape sequence char32_t code = getchar32(); - if (code == '[' || code == 0x1B) { + if (code == '[') { + code = getchar32(); + if (code == 'D') { // left + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + byte_pos = prev_utf8_char_pos(line, byte_pos); + } + } else if (code == 'C') { // right + if (char_pos < widths.size()) { + int w = widths[char_pos]; + move_cursor(w); + char_pos++; + byte_pos = next_utf8_char_pos(line, byte_pos); + } + } else if (code == 'A' || code == 'B') { + // up/down + // TODO: Implement history navigation + } else { + // Discard the rest of the escape sequence + while ((code = getchar32()) != (char32_t) WEOF) { + if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') { + break; + } + } + } + // TODO: Handle Ctrl+Arrow + } else if (code == 0x1B) { // Discard the rest of the escape sequence while ((code = getchar32()) != (char32_t) WEOF) { if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') { @@ -393,27 +463,72 @@ namespace console { } } } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace - if (!widths.empty()) { - int count; - do { - count = widths.back(); - widths.pop_back(); - // Move cursor back, print space, and move cursor back again - for (int i = 0; i < count; i++) { - replace_last(' '); - pop_cursor(); - } - pop_back_utf8_char(line); - } while (count == 0 && !widths.empty()); + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + size_t prev_pos = prev_utf8_char_pos(line, byte_pos); + size_t char_len = byte_pos - prev_pos; + byte_pos = prev_pos; + + // remove the character + line.erase(byte_pos, char_len); + widths.erase(widths.begin() + char_pos); + + // redraw tail + size_t p = byte_pos; + int tail_width = 0; + for (size_t i = char_pos; i < widths.size(); ++i) { + size_t next_p = next_utf8_char_pos(line, p); + put_codepoint(line.c_str() + p, next_p - p, widths[i]); + tail_width += widths[i]; + p = next_p; + } + + // clear display + for (int i = 0; i < w; ++i) { + fputc(' ', out); + } + move_cursor(-(tail_width + w)); } } else { - int offset = line.length(); - append_utf8(input_char, line); - int width = put_codepoint(line.c_str() + offset, line.length() - offset, estimateWidth(input_char)); - if (width < 0) { - width = 0; + // insert character + std::string new_char_str; + append_utf8(input_char, new_char_str); + int w = estimateWidth(input_char); + + if (char_pos == widths.size()) { + // insert at the end + line += new_char_str; + int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w); + if (real_w < 0) real_w = 0; + widths.push_back(real_w); + byte_pos += new_char_str.length(); + char_pos++; + } else { + // insert in middle + line.insert(byte_pos, new_char_str); + + int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w); + if (real_w < 0) real_w = 0; + + widths.insert(widths.begin() + char_pos, real_w); + + // print the tail + size_t p = byte_pos + new_char_str.length(); + int tail_width = 0; + for (size_t i = char_pos + 1; i < widths.size(); ++i) { + size_t next_p = next_utf8_char_pos(line, p); + put_codepoint(line.c_str() + p, next_p - p, widths[i]); + tail_width += widths[i]; + p = next_p; + } + + move_cursor(-tail_width); + + byte_pos += new_char_str.length(); + char_pos++; } - widths.push_back(width); } if (!line.empty() && (line.back() == '\\' || line.back() == '/')) { From d0963eee9da19e8d5d18d7b1cd5bed71fbb2c3f5 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 03:16:12 +0100 Subject: [PATCH 07/16] console: fix arrow keys on Windows using private-use Unicode --- common/console.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/common/console.cpp b/common/console.cpp index a7c9c1644c3..03a0d9bfa8b 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -36,6 +36,16 @@ namespace console { + namespace { + // Use private-use unicode values to represent special keys that are not reported + // as characters (e.g. arrows on Windows). These values should never clash with + // real input and let the rest of the code handle navigation uniformly. + static constexpr char32_t KEY_ARROW_LEFT = 0xE000; + static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; + static constexpr char32_t KEY_ARROW_UP = 0xE002; + static constexpr char32_t KEY_ARROW_DOWN = 0xE003; + } + // // Console state // @@ -176,6 +186,15 @@ namespace console { if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) { wchar_t wc = record.Event.KeyEvent.uChar.UnicodeChar; + if (wc == 0) { + switch (record.Event.KeyEvent.wVirtualKeyCode) { + case VK_LEFT: return KEY_ARROW_LEFT; + case VK_RIGHT: return KEY_ARROW_RIGHT; + case VK_UP: return KEY_ARROW_UP; + case VK_DOWN: return KEY_ARROW_DOWN; + default: continue; + } + } if (wc == 0) { continue; } @@ -462,6 +481,24 @@ namespace console { } } } +#if defined(_WIN32) + } else if (input_char == KEY_ARROW_LEFT) { + if (char_pos > 0) { + int w = widths[char_pos - 1]; + move_cursor(-w); + char_pos--; + byte_pos = prev_utf8_char_pos(line, byte_pos); + } + } else if (input_char == KEY_ARROW_RIGHT) { + if (char_pos < widths.size()) { + int w = widths[char_pos]; + move_cursor(w); + char_pos++; + byte_pos = next_utf8_char_pos(line, byte_pos); + } + } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { + // TODO: Implement history navigation +#endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace if (char_pos > 0) { int w = widths[char_pos - 1]; From bb594af7771b4715ccf940daf7e51dd69f93c937 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 03:36:42 +0100 Subject: [PATCH 08/16] console: add Home/End key support for Windows and Linux --- common/console.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/common/console.cpp b/common/console.cpp index 03a0d9bfa8b..78bc2d46752 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -44,6 +44,8 @@ namespace console { static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; static constexpr char32_t KEY_ARROW_UP = 0xE002; static constexpr char32_t KEY_ARROW_DOWN = 0xE003; + static constexpr char32_t KEY_HOME = 0xE004; + static constexpr char32_t KEY_END = 0xE005; } // @@ -192,6 +194,8 @@ namespace console { case VK_RIGHT: return KEY_ARROW_RIGHT; case VK_UP: return KEY_ARROW_UP; case VK_DOWN: return KEY_ARROW_DOWN; + case VK_HOME: return KEY_HOME; + case VK_END: return KEY_END; default: continue; } } @@ -374,6 +378,28 @@ namespace console { return pos; } + static void move_cursor(int delta); + + static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths) { + int back_width = 0; + for (size_t i = 0; i < char_pos; ++i) { + back_width += widths[i]; + } + move_cursor(-back_width); + char_pos = 0; + byte_pos = 0; + } + + static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line) { + int forward_width = 0; + for (size_t i = char_pos; i < widths.size(); ++i) { + forward_width += widths[i]; + } + move_cursor(forward_width); + char_pos = widths.size(); + byte_pos = line.length(); + } + static void move_cursor(int delta) { if (delta == 0) return; #if defined(_WIN32) @@ -461,9 +487,32 @@ namespace console { char_pos++; byte_pos = next_utf8_char_pos(line, byte_pos); } + } else if (code == 'H') { // home + move_to_line_start(char_pos, byte_pos, widths); + } else if (code == 'F') { // end + move_to_line_end(char_pos, byte_pos, widths, line); } else if (code == 'A' || code == 'B') { // up/down // TODO: Implement history navigation + } else if (code >= '0' && code <= '9') { + std::string digits; + digits.push_back(static_cast(code)); + while (true) { + code = getchar32(); + if (code >= '0' && code <= '9') { + digits.push_back(static_cast(code)); + continue; + } + break; + } + + if (code == '~') { + if (digits == "1" || digits == "7") { + move_to_line_start(char_pos, byte_pos, widths); + } else if (digits == "4" || digits == "8") { + move_to_line_end(char_pos, byte_pos, widths, line); + } + } } else { // Discard the rest of the escape sequence while ((code = getchar32()) != (char32_t) WEOF) { @@ -496,6 +545,10 @@ namespace console { char_pos++; byte_pos = next_utf8_char_pos(line, byte_pos); } + } else if (input_char == KEY_HOME) { + move_to_line_start(char_pos, byte_pos, widths); + } else if (input_char == KEY_END) { + move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { // TODO: Implement history navigation #endif From 934a842df22cb2ad1bb5632074a3fcc85aa1c5f2 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 04:09:40 +0100 Subject: [PATCH 09/16] console: add basic Up/Down history navigation --- common/console.cpp | 115 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 78bc2d46752..84b7ff29aef 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -56,6 +57,8 @@ namespace console { static bool simple_io = true; static display_t current_display = reset; + static std::vector history; + static FILE* out = stdout; #if defined (_WIN32) @@ -339,6 +342,34 @@ namespace console { #endif } + static char32_t decode_utf8(const std::string & input, size_t pos, size_t & advance) { + unsigned char c = static_cast(input[pos]); + if ((c & 0x80u) == 0u) { + advance = 1; + return c; + } + if ((c & 0xE0u) == 0xC0u && pos + 1 < input.size()) { + advance = 2; + return ((c & 0x1Fu) << 6) | (static_cast(input[pos + 1]) & 0x3Fu); + } + if ((c & 0xF0u) == 0xE0u && pos + 2 < input.size()) { + advance = 3; + return ((c & 0x0Fu) << 12) | + ((static_cast(input[pos + 1]) & 0x3Fu) << 6) | + (static_cast(input[pos + 2]) & 0x3Fu); + } + if ((c & 0xF8u) == 0xF0u && pos + 3 < input.size()) { + advance = 4; + return ((c & 0x07u) << 18) | + ((static_cast(input[pos + 1]) & 0x3Fu) << 12) | + ((static_cast(input[pos + 2]) & 0x3Fu) << 6) | + (static_cast(input[pos + 3]) & 0x3Fu); + } + + advance = 1; + return 0xFFFD; // replacement character for invalid input + } + static void append_utf8(char32_t ch, std::string & out) { if (ch <= 0x7F) { out.push_back(static_cast(ch)); @@ -379,6 +410,45 @@ namespace console { } static void move_cursor(int delta); + static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths); + static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); + + static void clear_current_line(const std::vector & widths) { + int total_width = 0; + for (int w : widths) { + total_width += (w > 0 ? w : 1); + } + + if (total_width > 0) { + std::string spaces(total_width, ' '); + fwrite(spaces.c_str(), 1, total_width, out); + move_cursor(-total_width); + } + } + + static void set_line_contents(std::string new_line, std::string & line, std::vector & widths, size_t & char_pos, + size_t & byte_pos) { + move_to_line_start(char_pos, byte_pos, widths); + clear_current_line(widths); + + line = std::move(new_line); + widths.clear(); + byte_pos = 0; + char_pos = 0; + + size_t idx = 0; + while (idx < line.size()) { + size_t advance = 0; + char32_t cp = decode_utf8(line, idx, advance); + int expected_width = estimateWidth(cp); + int real_width = put_codepoint(line.c_str() + idx, advance, expected_width); + if (real_width < 0) real_width = 0; + widths.push_back(real_width); + idx += advance; + ++char_pos; + byte_pos = idx; + } + } static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths) { int back_width = 0; @@ -442,6 +512,7 @@ namespace console { std::vector widths; bool is_special_char = false; bool end_of_stream = false; + size_t history_index = history.size(); size_t byte_pos = 0; // current byte index size_t char_pos = 0; // current character index (one char can be multiple bytes) @@ -493,7 +564,23 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } else if (code == 'A' || code == 'B') { // up/down - // TODO: Implement history navigation + if (!history.empty()) { + if (code == 'A' && history_index > 0) { + history_index--; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (code == 'B') { + if (history_index + 1 < history.size()) { + history_index++; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents("", line, widths, char_pos, byte_pos); + is_special_char = false; + } + } + } } else if (code >= '0' && code <= '9') { std::string digits; digits.push_back(static_cast(code)); @@ -550,7 +637,23 @@ namespace console { } else if (input_char == KEY_END) { move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { - // TODO: Implement history navigation + if (!history.empty()) { + if (input_char == KEY_ARROW_UP && history_index > 0) { + history_index--; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (input_char == KEY_ARROW_DOWN) { + if (history_index + 1 < history.size()) { + history_index++; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents("", line, widths, char_pos, byte_pos); + is_special_char = false; + } + } + } #endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace if (char_pos > 0) { @@ -656,6 +759,14 @@ namespace console { } } + if (!end_of_stream && !line.empty()) { + std::string history_entry = line; + if (!history_entry.empty() && history_entry.back() == '\n') { + history_entry.pop_back(); + } + history.push_back(std::move(history_entry)); + } + fflush(out); return has_more; } From b0bea5b6a1bb78afeba7a6b8699d83075d840bea Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 15:02:26 +0100 Subject: [PATCH 10/16] console: remove unreachable wc == 0 check after VK switch --- common/console.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 84b7ff29aef..af1c38fccb1 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -202,9 +202,6 @@ namespace console { default: continue; } } - if (wc == 0) { - continue; - } if ((wc >= 0xD800) && (wc <= 0xDBFF)) { // Check if wc is a high surrogate high_surrogate = wc; From 9fb58496b93a0696fc4b5dfb22ecc7907247c505 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 7 Dec 2025 17:35:16 +0100 Subject: [PATCH 11/16] console: add Ctrl+Left/Right word navigation - Add KEY_CTRL_ARROW_LEFT and KEY_CTRL_ARROW_RIGHT codes - Windows: detect CTRL modifier via dwControlKeyState - Linux: parse ANSI sequences with modifier (1;5D/C) - Implement move_word_left/right with space-skipping logic - Refactor escape sequence parsing to accumulate params --- common/console.cpp | 188 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 161 insertions(+), 27 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index af1c38fccb1..7a611d94c9b 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -41,12 +43,14 @@ namespace console { // Use private-use unicode values to represent special keys that are not reported // as characters (e.g. arrows on Windows). These values should never clash with // real input and let the rest of the code handle navigation uniformly. - static constexpr char32_t KEY_ARROW_LEFT = 0xE000; - static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; - static constexpr char32_t KEY_ARROW_UP = 0xE002; - static constexpr char32_t KEY_ARROW_DOWN = 0xE003; - static constexpr char32_t KEY_HOME = 0xE004; - static constexpr char32_t KEY_END = 0xE005; + static constexpr char32_t KEY_ARROW_LEFT = 0xE000; + static constexpr char32_t KEY_ARROW_RIGHT = 0xE001; + static constexpr char32_t KEY_ARROW_UP = 0xE002; + static constexpr char32_t KEY_ARROW_DOWN = 0xE003; + static constexpr char32_t KEY_HOME = 0xE004; + static constexpr char32_t KEY_END = 0xE005; + static constexpr char32_t KEY_CTRL_ARROW_LEFT = 0xE006; + static constexpr char32_t KEY_CTRL_ARROW_RIGHT = 0xE007; } // @@ -192,9 +196,11 @@ namespace console { if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) { wchar_t wc = record.Event.KeyEvent.uChar.UnicodeChar; if (wc == 0) { + const DWORD ctrl_mask = LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED; + const bool ctrl_pressed = (record.Event.KeyEvent.dwControlKeyState & ctrl_mask) != 0; switch (record.Event.KeyEvent.wVirtualKeyCode) { - case VK_LEFT: return KEY_ARROW_LEFT; - case VK_RIGHT: return KEY_ARROW_RIGHT; + case VK_LEFT: return ctrl_pressed ? KEY_CTRL_ARROW_LEFT : KEY_ARROW_LEFT; + case VK_RIGHT: return ctrl_pressed ? KEY_CTRL_ARROW_RIGHT : KEY_ARROW_RIGHT; case VK_UP: return KEY_ARROW_UP; case VK_DOWN: return KEY_ARROW_DOWN; case VK_HOME: return KEY_HOME; @@ -407,6 +413,8 @@ namespace console { } static void move_cursor(int delta); + static void move_word_left(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); + static void move_word_right(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths); static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); @@ -467,6 +475,123 @@ namespace console { byte_pos = line.length(); } + static bool has_ctrl_modifier(const std::string & params) { + size_t start = 0; + while (start < params.size()) { + size_t end = params.find(';', start); + size_t len = (end == std::string::npos) ? params.size() - start : end - start; + if (len > 0) { + int value = 0; + for (size_t i = 0; i < len; ++i) { + char ch = params[start + i]; + if (!std::isdigit(static_cast(ch))) { + value = -1; + break; + } + value = value * 10 + (ch - '0'); + } + if (value == 5) { + return true; + } + } + + if (end == std::string::npos) { + break; + } + start = end + 1; + } + return false; + } + + static bool is_space_codepoint(char32_t cp) { + return std::iswspace(static_cast(cp)) != 0; + } + + static void move_word_left(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line) { + if (char_pos == 0) { + return; + } + + size_t new_char_pos = char_pos; + size_t new_byte_pos = byte_pos; + int move_width = 0; + + while (new_char_pos > 0) { + size_t prev_byte = prev_utf8_char_pos(line, new_byte_pos); + size_t advance = 0; + char32_t cp = decode_utf8(line, prev_byte, advance); + if (!is_space_codepoint(cp)) { + break; + } + move_width += widths[new_char_pos - 1]; + new_char_pos--; + new_byte_pos = prev_byte; + } + + while (new_char_pos > 0) { + size_t prev_byte = prev_utf8_char_pos(line, new_byte_pos); + size_t advance = 0; + char32_t cp = decode_utf8(line, prev_byte, advance); + if (is_space_codepoint(cp)) { + break; + } + move_width += widths[new_char_pos - 1]; + new_char_pos--; + new_byte_pos = prev_byte; + } + + move_cursor(-move_width); + char_pos = new_char_pos; + byte_pos = new_byte_pos; + } + + static void move_word_right(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line) { + if (char_pos >= widths.size()) { + return; + } + + size_t new_char_pos = char_pos; + size_t new_byte_pos = byte_pos; + int move_width = 0; + + while (new_char_pos < widths.size()) { + size_t advance = 0; + char32_t cp = decode_utf8(line, new_byte_pos, advance); + if (!is_space_codepoint(cp)) { + break; + } + move_width += widths[new_char_pos]; + new_char_pos++; + new_byte_pos += advance; + } + + while (new_char_pos < widths.size()) { + size_t advance = 0; + char32_t cp = decode_utf8(line, new_byte_pos, advance); + if (is_space_codepoint(cp)) { + break; + } + move_width += widths[new_char_pos]; + new_char_pos++; + new_byte_pos += advance; + } + + while (new_char_pos < widths.size()) { + size_t advance = 0; + char32_t cp = decode_utf8(line, new_byte_pos, advance); + if (!is_space_codepoint(cp)) { + break; + } + move_width += widths[new_char_pos]; + new_char_pos++; + new_byte_pos += advance; + } + + move_cursor(move_width); + char_pos = new_char_pos; + byte_pos = new_byte_pos; + } + static void move_cursor(int delta) { if (delta == 0) return; #if defined(_WIN32) @@ -540,16 +665,30 @@ namespace console { if (input_char == '\033') { // Escape sequence char32_t code = getchar32(); if (code == '[') { - code = getchar32(); + std::string params; + while (true) { + code = getchar32(); + if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~' || code == (char32_t) WEOF) { + break; + } + params.push_back(static_cast(code)); + } + + const bool ctrl_modifier = has_ctrl_modifier(params); + if (code == 'D') { // left - if (char_pos > 0) { + if (ctrl_modifier) { + move_word_left(char_pos, byte_pos, widths, line); + } else if (char_pos > 0) { int w = widths[char_pos - 1]; move_cursor(-w); char_pos--; byte_pos = prev_utf8_char_pos(line, byte_pos); } } else if (code == 'C') { // right - if (char_pos < widths.size()) { + if (ctrl_modifier) { + move_word_right(char_pos, byte_pos, widths, line); + } else if (char_pos < widths.size()) { int w = widths[char_pos]; move_cursor(w); char_pos++; @@ -578,16 +717,15 @@ namespace console { } } } - } else if (code >= '0' && code <= '9') { + } else if ((code == '~' || (code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z')) && !params.empty()) { std::string digits; - digits.push_back(static_cast(code)); - while (true) { - code = getchar32(); - if (code >= '0' && code <= '9') { - digits.push_back(static_cast(code)); - continue; + for (char ch : params) { + if (ch == ';') { + break; + } + if (std::isdigit(static_cast(ch))) { + digits.push_back(ch); } - break; } if (code == '~') { @@ -597,15 +735,7 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } } - } else { - // Discard the rest of the escape sequence - while ((code = getchar32()) != (char32_t) WEOF) { - if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') { - break; - } - } } - // TODO: Handle Ctrl+Arrow } else if (code == 0x1B) { // Discard the rest of the escape sequence while ((code = getchar32()) != (char32_t) WEOF) { @@ -629,6 +759,10 @@ namespace console { char_pos++; byte_pos = next_utf8_char_pos(line, byte_pos); } + } else if (input_char == KEY_CTRL_ARROW_LEFT) { + move_word_left(char_pos, byte_pos, widths, line); + } else if (input_char == KEY_CTRL_ARROW_RIGHT) { + move_word_right(char_pos, byte_pos, widths, line); } else if (input_char == KEY_HOME) { move_to_line_start(char_pos, byte_pos, widths); } else if (input_char == KEY_END) { From 5364d16e77cae2acf8cd36dede5ef5c4ef017a68 Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 8 Dec 2025 16:12:41 +0100 Subject: [PATCH 12/16] console: add Delete key support - Windows: VK_DELETE detection - Linux: ESC[3~ sequence parsing - Forward character deletion with UTF-8 support --- common/console.cpp | 48 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 7a611d94c9b..50e126332d8 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -51,6 +51,7 @@ namespace console { static constexpr char32_t KEY_END = 0xE005; static constexpr char32_t KEY_CTRL_ARROW_LEFT = 0xE006; static constexpr char32_t KEY_CTRL_ARROW_RIGHT = 0xE007; + static constexpr char32_t KEY_DELETE = 0xE008; } // @@ -199,13 +200,14 @@ namespace console { const DWORD ctrl_mask = LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED; const bool ctrl_pressed = (record.Event.KeyEvent.dwControlKeyState & ctrl_mask) != 0; switch (record.Event.KeyEvent.wVirtualKeyCode) { - case VK_LEFT: return ctrl_pressed ? KEY_CTRL_ARROW_LEFT : KEY_ARROW_LEFT; - case VK_RIGHT: return ctrl_pressed ? KEY_CTRL_ARROW_RIGHT : KEY_ARROW_RIGHT; - case VK_UP: return KEY_ARROW_UP; - case VK_DOWN: return KEY_ARROW_DOWN; - case VK_HOME: return KEY_HOME; - case VK_END: return KEY_END; - default: continue; + case VK_LEFT: return ctrl_pressed ? KEY_CTRL_ARROW_LEFT : KEY_ARROW_LEFT; + case VK_RIGHT: return ctrl_pressed ? KEY_CTRL_ARROW_RIGHT : KEY_ARROW_RIGHT; + case VK_UP: return KEY_ARROW_UP; + case VK_DOWN: return KEY_ARROW_DOWN; + case VK_HOME: return KEY_HOME; + case VK_END: return KEY_END; + case VK_DELETE: return KEY_DELETE; + default: continue; } } @@ -418,6 +420,34 @@ namespace console { static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector & widths); static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector & widths, const std::string & line); + static void delete_at_cursor(std::string & line, std::vector & widths, size_t & char_pos, size_t & byte_pos) { + if (char_pos >= widths.size()) { + return; + } + + size_t next_pos = next_utf8_char_pos(line, byte_pos); + int w = widths[char_pos]; + size_t char_len = next_pos - byte_pos; + + line.erase(byte_pos, char_len); + widths.erase(widths.begin() + char_pos); + + size_t p = byte_pos; + int tail_width = 0; + for (size_t i = char_pos; i < widths.size(); ++i) { + size_t following = next_utf8_char_pos(line, p); + put_codepoint(line.c_str() + p, following - p, widths[i]); + tail_width += widths[i]; + p = following; + } + + for (int i = 0; i < w; ++i) { + fputc(' ', out); + } + + move_cursor(-(tail_width + w)); + } + static void clear_current_line(const std::vector & widths) { int total_width = 0; for (int w : widths) { @@ -733,6 +763,8 @@ namespace console { move_to_line_start(char_pos, byte_pos, widths); } else if (digits == "4" || digits == "8") { move_to_line_end(char_pos, byte_pos, widths, line); + } else if (digits == "3") { + delete_at_cursor(line, widths, char_pos, byte_pos); } } } @@ -767,6 +799,8 @@ namespace console { move_to_line_start(char_pos, byte_pos, widths); } else if (input_char == KEY_END) { move_to_line_end(char_pos, byte_pos, widths, line); + } else if (input_char == KEY_DELETE) { + delete_at_cursor(line, widths, char_pos, byte_pos); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { if (!history.empty()) { if (input_char == KEY_ARROW_UP && history_index > 0) { From 7e97693045abade704d3a8f62048a1d3429b5eee Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 8 Dec 2025 17:45:13 +0100 Subject: [PATCH 13/16] console: implement bash-style history editing - Edit any history line during UP/DOWN navigation, edits persist - Pressing Enter appends edited version as new history entry - Original line stay untouched in their positions --- common/console.cpp | 63 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 50e126332d8..e1b48c1a885 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -665,6 +665,9 @@ namespace console { bool is_special_char = false; bool end_of_stream = false; size_t history_index = history.size(); + std::string original_backup; + std::string prompt_backup; + size_t backup_index = SIZE_MAX; size_t byte_pos = 0; // current byte index size_t char_pos = 0; // current character index (one char can be multiple bytes) @@ -673,6 +676,11 @@ namespace console { while (true) { assert(char_pos <= byte_pos); assert(char_pos <= widths.size()); + auto sync_history_line = [&]() { + if (history_index < history.size()) { + history[history_index] = line; + } + }; fflush(out); // Ensure all output is displayed before waiting for input input_char = getchar32(); @@ -732,17 +740,27 @@ namespace console { // up/down if (!history.empty()) { if (code == 'A' && history_index > 0) { + sync_history_line(); + const bool from_end = history_index == history.size(); + if (from_end) { + prompt_backup = line; + } history_index--; + original_backup = history[history_index]; + backup_index = history_index; set_line_contents(history[history_index], line, widths, char_pos, byte_pos); is_special_char = false; } else if (code == 'B') { + sync_history_line(); if (history_index + 1 < history.size()) { history_index++; + original_backup = history[history_index]; + backup_index = history_index; set_line_contents(history[history_index], line, widths, char_pos, byte_pos); is_special_char = false; } else if (history_index < history.size()) { history_index = history.size(); - set_line_contents("", line, widths, char_pos, byte_pos); + set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); is_special_char = false; } } @@ -801,20 +819,31 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_DELETE) { delete_at_cursor(line, widths, char_pos, byte_pos); - } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { - if (!history.empty()) { - if (input_char == KEY_ARROW_UP && history_index > 0) { - history_index--; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (input_char == KEY_ARROW_DOWN) { - if (history_index + 1 < history.size()) { - history_index++; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (history_index < history.size()) { - history_index = history.size(); - set_line_contents("", line, widths, char_pos, byte_pos); + sync_history_line(); + } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { + if (!history.empty()) { + if (input_char == KEY_ARROW_UP && history_index > 0) { + sync_history_line(); + const bool from_end = history_index == history.size(); + if (from_end) { + prompt_backup = line; + } + history_index--; + original_backup = history[history_index]; + backup_index = history_index; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (input_char == KEY_ARROW_DOWN) { + sync_history_line(); + if (history_index + 1 < history.size()) { + history_index++; + original_backup = history[history_index]; + backup_index = history_index; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); is_special_char = false; } } @@ -848,6 +877,7 @@ namespace console { fputc(' ', out); } move_cursor(-(tail_width + w)); + sync_history_line(); } } else { // insert character @@ -925,6 +955,9 @@ namespace console { } if (!end_of_stream && !line.empty()) { + if (backup_index < history.size()) { + history[backup_index] = original_backup; + } std::string history_entry = line; if (!history_entry.empty() && history_entry.back() == '\n') { history_entry.pop_back(); From 758d85dfea35e87f3869d92184af4d0519da80e6 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Mon, 8 Dec 2025 23:01:19 +0100 Subject: [PATCH 14/16] clean up --- common/console.cpp | 101 ++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 9c1e4f8ef23..f26a3e2776f 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -683,6 +684,36 @@ namespace console { history[history_index] = line; } }; + auto history_prev = [&]() { + if (history.empty()) { + return; + } + if (history_index > 0) { + sync_history_line(); + history_index--; + original_backup = history[history_index]; + backup_index = history_index; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } + }; + auto history_next = [&]() { + if (history.empty()) { + return; + } + sync_history_line(); + if (history_index + 1 < history.size()) { + history_index++; + original_backup = history[history_index]; + backup_index = history_index; + set_line_contents(history[history_index], line, widths, char_pos, byte_pos); + is_special_char = false; + } else if (history_index < history.size()) { + history_index = history.size(); + set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); + is_special_char = false; + } + }; fflush(out); // Ensure all output is displayed before waiting for input input_char = getchar32(); @@ -691,7 +722,7 @@ namespace console { break; } - if (input_char == (char32_t) WEOF || input_char == 0x04 /* Ctrl+D*/) { + if (input_char == (char32_t) WEOF || input_char == 0x04 /* Ctrl+D */) { end_of_stream = true; break; } @@ -740,32 +771,10 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } else if (code == 'A' || code == 'B') { // up/down - if (!history.empty()) { - if (code == 'A' && history_index > 0) { - sync_history_line(); - const bool from_end = history_index == history.size(); - if (from_end) { - prompt_backup = line; - } - history_index--; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (code == 'B') { - sync_history_line(); - if (history_index + 1 < history.size()) { - history_index++; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (history_index < history.size()) { - history_index = history.size(); - set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); - is_special_char = false; - } - } + if (code == 'A') { + history_prev(); + } else if (code == 'B') { + history_next(); } } else if ((code == '~' || (code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z')) && !params.empty()) { std::string digits; @@ -779,11 +788,11 @@ namespace console { } if (code == '~') { - if (digits == "1" || digits == "7") { + if (digits == "1" || digits == "7") { // home move_to_line_start(char_pos, byte_pos, widths); - } else if (digits == "4" || digits == "8") { + } else if (digits == "4" || digits == "8") { // end move_to_line_end(char_pos, byte_pos, widths, line); - } else if (digits == "3") { + } else if (digits == "3") { // delete delete_at_cursor(line, widths, char_pos, byte_pos); } } @@ -822,33 +831,11 @@ namespace console { } else if (input_char == KEY_DELETE) { delete_at_cursor(line, widths, char_pos, byte_pos); sync_history_line(); - } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { - if (!history.empty()) { - if (input_char == KEY_ARROW_UP && history_index > 0) { - sync_history_line(); - const bool from_end = history_index == history.size(); - if (from_end) { - prompt_backup = line; - } - history_index--; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (input_char == KEY_ARROW_DOWN) { - sync_history_line(); - if (history_index + 1 < history.size()) { - history_index++; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (history_index < history.size()) { - history_index = history.size(); - set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); - is_special_char = false; - } - } + } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { + if (input_char == KEY_ARROW_UP) { + history_prev(); + } else if (input_char == KEY_ARROW_DOWN) { + history_next(); } #endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace From ae223fc0a6bd894c4c91ae181d652f1393fd5c7f Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Mon, 8 Dec 2025 23:38:20 +0100 Subject: [PATCH 15/16] better history impl --- common/console.cpp | 117 +++++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 42 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index f26a3e2776f..b7593f9587a 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -65,8 +65,6 @@ namespace console { static bool simple_io = true; static display_t current_display = reset; - static std::vector history; - static FILE* out = stdout; #if defined (_WIN32) @@ -658,6 +656,60 @@ namespace console { #endif } + struct history_t { + std::vector entries; + size_t viewing_idx = SIZE_MAX; + std::string backup_line; // current line before viewing history + void add(const std::string & line) { + if (line.empty()) { + return; + } + // avoid duplicates with the last entry + if (entries.empty() || entries.back() != line) { + entries.push_back(line); + } + // also clear viewing state + end_viewing(); + } + bool prev(std::string & cur_line) { + if (entries.empty()) { + return false; + } + if (viewing_idx == SIZE_MAX) { + return false; + } + if (viewing_idx > 0) { + viewing_idx--; + } + cur_line = entries[viewing_idx]; + return true; + } + bool next(std::string & cur_line) { + if (entries.empty() || viewing_idx == SIZE_MAX) { + return false; + } + viewing_idx++; + if (viewing_idx >= entries.size()) { + cur_line = backup_line; + end_viewing(); + } else { + cur_line = entries[viewing_idx]; + } + return true; + } + void begin_viewing(const std::string & line) { + backup_line = line; + viewing_idx = entries.size(); + } + void end_viewing() { + viewing_idx = SIZE_MAX; + backup_line.clear(); + } + bool is_viewing() const { + return viewing_idx != SIZE_MAX; + } + } history; + static bool readline_advanced(std::string & line, bool multiline_input) { if (out != stdout) { fflush(stdout); @@ -667,10 +719,6 @@ namespace console { std::vector widths; bool is_special_char = false; bool end_of_stream = false; - size_t history_index = history.size(); - std::string original_backup; - std::string prompt_backup; - size_t backup_index = SIZE_MAX; size_t byte_pos = 0; // current byte index size_t char_pos = 0; // current character index (one char can be multiple bytes) @@ -679,39 +727,23 @@ namespace console { while (true) { assert(char_pos <= byte_pos); assert(char_pos <= widths.size()); - auto sync_history_line = [&]() { - if (history_index < history.size()) { - history[history_index] = line; - } - }; auto history_prev = [&]() { - if (history.empty()) { - return; + if (!history.is_viewing()) { + history.begin_viewing(line); } - if (history_index > 0) { - sync_history_line(); - history_index--; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; + std::string new_line; + if (!history.prev(new_line)) { + return; } + set_line_contents(new_line, line, widths, char_pos, byte_pos); }; auto history_next = [&]() { - if (history.empty()) { - return; - } - sync_history_line(); - if (history_index + 1 < history.size()) { - history_index++; - original_backup = history[history_index]; - backup_index = history_index; - set_line_contents(history[history_index], line, widths, char_pos, byte_pos); - is_special_char = false; - } else if (history_index < history.size()) { - history_index = history.size(); - set_line_contents(prompt_backup, line, widths, char_pos, byte_pos); - is_special_char = false; + if (history.is_viewing()) { + std::string new_line; + if (!history.next(new_line)) { + return; + } + set_line_contents(new_line, line, widths, char_pos, byte_pos); } }; @@ -773,8 +805,10 @@ namespace console { // up/down if (code == 'A') { history_prev(); + is_special_char = false; } else if (code == 'B') { history_next(); + is_special_char = false; } } else if ((code == '~' || (code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z')) && !params.empty()) { std::string digits; @@ -834,8 +868,10 @@ namespace console { } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { if (input_char == KEY_ARROW_UP) { history_prev(); + is_special_char = false; } else if (input_char == KEY_ARROW_DOWN) { history_next(); + is_special_char = false; } #endif } else if (input_char == 0x08 || input_char == 0x7F) { // Backspace @@ -866,7 +902,6 @@ namespace console { fputc(' ', out); } move_cursor(-(tail_width + w)); - sync_history_line(); } } else { // insert character @@ -944,14 +979,12 @@ namespace console { } if (!end_of_stream && !line.empty()) { - if (backup_index < history.size()) { - history[backup_index] = original_backup; - } - std::string history_entry = line; - if (!history_entry.empty() && history_entry.back() == '\n') { - history_entry.pop_back(); + // remove the trailing newline for history storage + if (!line.empty() && line.back() == '\n') { + line.pop_back(); } - history.push_back(std::move(history_entry)); + // TODO: maybe support multiline history entries? + history.add(line); } fflush(out); From 9190faba7e0e508120427d98a128a1f8d9bfa5f3 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Mon, 8 Dec 2025 23:48:39 +0100 Subject: [PATCH 16/16] fix decode_utf8 --- common/console.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/common/console.cpp b/common/console.cpp index b7593f9587a..5e9901e4a2d 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -355,16 +355,34 @@ namespace console { return c; } if ((c & 0xE0u) == 0xC0u && pos + 1 < input.size()) { + unsigned char c1 = static_cast(input[pos + 1]); + if ((c1 & 0xC0u) != 0x80u) { + advance = 1; + return 0xFFFD; + } advance = 2; return ((c & 0x1Fu) << 6) | (static_cast(input[pos + 1]) & 0x3Fu); } if ((c & 0xF0u) == 0xE0u && pos + 2 < input.size()) { + unsigned char c1 = static_cast(input[pos + 1]); + unsigned char c2 = static_cast(input[pos + 2]); + if ((c1 & 0xC0u) != 0x80u || (c2 & 0xC0u) != 0x80u) { + advance = 1; + return 0xFFFD; + } advance = 3; return ((c & 0x0Fu) << 12) | ((static_cast(input[pos + 1]) & 0x3Fu) << 6) | (static_cast(input[pos + 2]) & 0x3Fu); } if ((c & 0xF8u) == 0xF0u && pos + 3 < input.size()) { + unsigned char c1 = static_cast(input[pos + 1]); + unsigned char c2 = static_cast(input[pos + 2]); + unsigned char c3 = static_cast(input[pos + 3]); + if ((c1 & 0xC0u) != 0x80u || (c2 & 0xC0u) != 0x80u || (c3 & 0xC0u) != 0x80u) { + advance = 1; + return 0xFFFD; + } advance = 4; return ((c & 0x07u) << 18) | ((static_cast(input[pos + 1]) & 0x3Fu) << 12) | @@ -864,7 +882,6 @@ namespace console { move_to_line_end(char_pos, byte_pos, widths, line); } else if (input_char == KEY_DELETE) { delete_at_cursor(line, widths, char_pos, byte_pos); - sync_history_line(); } else if (input_char == KEY_ARROW_UP || input_char == KEY_ARROW_DOWN) { if (input_char == KEY_ARROW_UP) { history_prev();