From 77ca25902643240b5f162d6c6fce1a79d12668b1 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 7 Dec 2025 01:11:05 +0100 Subject: [PATCH 1/5] 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 078a8d678d..a7c9c1644c 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 2/5] 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 a7c9c1644c..03a0d9bfa8 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 3/5] 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 03a0d9bfa8..78bc2d4675 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 4/5] 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 78bc2d4675..84b7ff29ae 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 5/5] fix build --- common/console.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 84b7ff29ae..46f67e059a 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;