diff --git a/common/console.cpp b/common/console.cpp index a7c9c1644c3..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 @@ -36,6 +37,18 @@ 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; + static constexpr char32_t KEY_HOME = 0xE004; + static constexpr char32_t KEY_END = 0xE005; + } + // // Console state // @@ -44,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) @@ -176,6 +191,17 @@ 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; + case VK_HOME: return KEY_HOME; + case VK_END: return KEY_END; + default: continue; + } + } if (wc == 0) { continue; } @@ -316,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)); @@ -355,6 +409,67 @@ 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); + 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; + 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) @@ -397,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) @@ -442,9 +558,48 @@ 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 + 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)); + 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) { @@ -462,6 +617,44 @@ 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_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) { + 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) { int w = widths[char_pos - 1]; @@ -566,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; }