Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 202 additions & 1 deletion common/console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <vector>
#include <iostream>
#include <cassert>
#include <cstddef>

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
Expand Down Expand Up @@ -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
//
Expand All @@ -44,6 +57,8 @@ namespace console {
static bool simple_io = true;
static display_t current_display = reset;

static std::vector<std::string> history;

static FILE* out = stdout;

#if defined (_WIN32)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<unsigned char>(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<unsigned char>(input[pos + 1]) & 0x3Fu);
}
if ((c & 0xF0u) == 0xE0u && pos + 2 < input.size()) {
advance = 3;
return ((c & 0x0Fu) << 12) |
((static_cast<unsigned char>(input[pos + 1]) & 0x3Fu) << 6) |
(static_cast<unsigned char>(input[pos + 2]) & 0x3Fu);
}
if ((c & 0xF8u) == 0xF0u && pos + 3 < input.size()) {
advance = 4;
return ((c & 0x07u) << 18) |
((static_cast<unsigned char>(input[pos + 1]) & 0x3Fu) << 12) |
((static_cast<unsigned char>(input[pos + 2]) & 0x3Fu) << 6) |
(static_cast<unsigned char>(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<unsigned char>(ch));
Expand Down Expand Up @@ -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<int> & widths);
static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line);

static void clear_current_line(const std::vector<int> & 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<int> & 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<int> & 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<int> & 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)
Expand Down Expand Up @@ -397,6 +512,7 @@ namespace console {
std::vector<int> 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)
Expand Down Expand Up @@ -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<char>(code));
while (true) {
code = getchar32();
if (code >= '0' && code <= '9') {
digits.push_back(static_cast<char>(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) {
Expand All @@ -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];
Expand Down Expand Up @@ -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;
}
Expand Down