Spaces:
Build error
Build error
namespace console { | |
// | |
// Console state | |
// | |
static bool advanced_display = false; | |
static bool simple_io = true; | |
static display_t current_display = reset; | |
static FILE* out = stdout; | |
static void* hConsole; | |
static FILE* tty = nullptr; | |
static termios initial_state; | |
// | |
// Init and cleanup | |
// | |
void init(bool use_simple_io, bool use_advanced_display) { | |
advanced_display = use_advanced_display; | |
simple_io = use_simple_io; | |
// Windows-specific console initialization | |
DWORD dwMode = 0; | |
hConsole = GetStdHandle(STD_OUTPUT_HANDLE); | |
if (hConsole == INVALID_HANDLE_VALUE || !GetConsoleMode(hConsole, &dwMode)) { | |
hConsole = GetStdHandle(STD_ERROR_HANDLE); | |
if (hConsole != INVALID_HANDLE_VALUE && (!GetConsoleMode(hConsole, &dwMode))) { | |
hConsole = nullptr; | |
simple_io = true; | |
} | |
} | |
if (hConsole) { | |
// Enable ANSI colors on Windows 10+ | |
if (advanced_display && !(dwMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { | |
SetConsoleMode(hConsole, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); | |
} | |
// Set console output codepage to UTF8 | |
SetConsoleOutputCP(CP_UTF8); | |
} | |
HANDLE hConIn = GetStdHandle(STD_INPUT_HANDLE); | |
if (hConIn != INVALID_HANDLE_VALUE && GetConsoleMode(hConIn, &dwMode)) { | |
// Set console input codepage to UTF16 | |
_setmode(_fileno(stdin), _O_WTEXT); | |
// Set ICANON (ENABLE_LINE_INPUT) and ECHO (ENABLE_ECHO_INPUT) | |
if (simple_io) { | |
dwMode |= ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT; | |
} else { | |
dwMode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); | |
} | |
if (!SetConsoleMode(hConIn, dwMode)) { | |
simple_io = true; | |
} | |
} | |
// POSIX-specific console initialization | |
if (!simple_io) { | |
struct termios new_termios; | |
tcgetattr(STDIN_FILENO, &initial_state); | |
new_termios = initial_state; | |
new_termios.c_lflag &= ~(ICANON | ECHO); | |
new_termios.c_cc[VMIN] = 1; | |
new_termios.c_cc[VTIME] = 0; | |
tcsetattr(STDIN_FILENO, TCSANOW, &new_termios); | |
tty = fopen("/dev/tty", "w+"); | |
if (tty != nullptr) { | |
out = tty; | |
} | |
} | |
setlocale(LC_ALL, ""); | |
} | |
void cleanup() { | |
// Reset console display | |
set_display(reset); | |
// Restore settings on POSIX systems | |
if (!simple_io) { | |
if (tty != nullptr) { | |
out = stdout; | |
fclose(tty); | |
tty = nullptr; | |
} | |
tcsetattr(STDIN_FILENO, TCSANOW, &initial_state); | |
} | |
} | |
// | |
// Display and IO | |
// | |
// Keep track of current display and only emit ANSI code if it changes | |
void set_display(display_t display) { | |
if (advanced_display && current_display != display) { | |
fflush(stdout); | |
switch(display) { | |
case reset: | |
fprintf(out, ANSI_COLOR_RESET); | |
break; | |
case prompt: | |
fprintf(out, ANSI_COLOR_YELLOW); | |
break; | |
case user_input: | |
fprintf(out, ANSI_BOLD ANSI_COLOR_GREEN); | |
break; | |
case error: | |
fprintf(out, ANSI_BOLD ANSI_COLOR_RED); | |
} | |
current_display = display; | |
fflush(out); | |
} | |
} | |
char32_t getchar32() { | |
HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); | |
wchar_t high_surrogate = 0; | |
while (true) { | |
INPUT_RECORD record; | |
DWORD count; | |
if (!ReadConsoleInputW(hConsole, &record, 1, &count) || count == 0) { | |
return WEOF; | |
} | |
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) { | |
wchar_t wc = record.Event.KeyEvent.uChar.UnicodeChar; | |
if (wc == 0) { | |
continue; | |
} | |
if ((wc >= 0xD800) && (wc <= 0xDBFF)) { // Check if wc is a high surrogate | |
high_surrogate = wc; | |
continue; | |
} | |
if ((wc >= 0xDC00) && (wc <= 0xDFFF)) { // Check if wc is a low surrogate | |
if (high_surrogate != 0) { // Check if we have a high surrogate | |
return ((high_surrogate - 0xD800) << 10) + (wc - 0xDC00) + 0x10000; | |
} | |
} | |
high_surrogate = 0; // Reset the high surrogate | |
return static_cast<char32_t>(wc); | |
} | |
} | |
wchar_t wc = getwchar(); | |
if (static_cast<wint_t>(wc) == WEOF) { | |
return WEOF; | |
} | |
if ((wc >= 0xD800) && (wc <= 0xDBFF)) { // Check if wc is a high surrogate | |
wchar_t low_surrogate = getwchar(); | |
if ((low_surrogate >= 0xDC00) && (low_surrogate <= 0xDFFF)) { // Check if the next wchar is a low surrogate | |
return (static_cast<char32_t>(wc & 0x03FF) << 10) + (low_surrogate & 0x03FF) + 0x10000; | |
} | |
} | |
if ((wc >= 0xD800) && (wc <= 0xDFFF)) { // Invalid surrogate pair | |
return 0xFFFD; // Return the replacement character U+FFFD | |
} | |
return static_cast<char32_t>(wc); | |
} | |
void pop_cursor() { | |
if (hConsole != NULL) { | |
CONSOLE_SCREEN_BUFFER_INFO bufferInfo; | |
GetConsoleScreenBufferInfo(hConsole, &bufferInfo); | |
COORD newCursorPosition = bufferInfo.dwCursorPosition; | |
if (newCursorPosition.X == 0) { | |
newCursorPosition.X = bufferInfo.dwSize.X - 1; | |
newCursorPosition.Y -= 1; | |
} else { | |
newCursorPosition.X -= 1; | |
} | |
SetConsoleCursorPosition(hConsole, newCursorPosition); | |
return; | |
} | |
putc('\b', out); | |
} | |
int estimateWidth(char32_t codepoint) { | |
return 1; | |
return wcwidth(codepoint); | |
} | |
int put_codepoint(const char* utf8_codepoint, size_t length, int expectedWidth) { | |
CONSOLE_SCREEN_BUFFER_INFO bufferInfo; | |
if (!GetConsoleScreenBufferInfo(hConsole, &bufferInfo)) { | |
// go with the default | |
return expectedWidth; | |
} | |
COORD initialPosition = bufferInfo.dwCursorPosition; | |
DWORD nNumberOfChars = length; | |
WriteConsole(hConsole, utf8_codepoint, nNumberOfChars, &nNumberOfChars, NULL); | |
CONSOLE_SCREEN_BUFFER_INFO newBufferInfo; | |
GetConsoleScreenBufferInfo(hConsole, &newBufferInfo); | |
// Figure out our real position if we're in the last column | |
if (utf8_codepoint[0] != 0x09 && initialPosition.X == newBufferInfo.dwSize.X - 1) { | |
DWORD nNumberOfChars; | |
WriteConsole(hConsole, &" \b", 2, &nNumberOfChars, NULL); | |
GetConsoleScreenBufferInfo(hConsole, &newBufferInfo); | |
} | |
int width = newBufferInfo.dwCursorPosition.X - initialPosition.X; | |
if (width < 0) { | |
width += newBufferInfo.dwSize.X; | |
} | |
return width; | |
// We can trust expectedWidth if we've got one | |
if (expectedWidth >= 0 || tty == nullptr) { | |
fwrite(utf8_codepoint, length, 1, out); | |
return expectedWidth; | |
} | |
fputs("\033[6n", tty); // Query cursor position | |
int x1; | |
int y1; | |
int x2; | |
int y2; | |
int results = 0; | |
results = fscanf(tty, "\033[%d;%dR", &y1, &x1); | |
fwrite(utf8_codepoint, length, 1, tty); | |
fputs("\033[6n", tty); // Query cursor position | |
results += fscanf(tty, "\033[%d;%dR", &y2, &x2); | |
if (results != 4) { | |
return expectedWidth; | |
} | |
int width = x2 - x1; | |
if (width < 0) { | |
// Calculate the width considering text wrapping | |
struct winsize w; | |
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); | |
width += w.ws_col; | |
} | |
return width; | |
} | |
void replace_last(char ch) { | |
pop_cursor(); | |
put_codepoint(&ch, 1, 1); | |
fprintf(out, "\b%c", ch); | |
} | |
void append_utf8(char32_t ch, std::string & out) { | |
if (ch <= 0x7F) { | |
out.push_back(static_cast<unsigned char>(ch)); | |
} else if (ch <= 0x7FF) { | |
out.push_back(static_cast<unsigned char>(0xC0 | ((ch >> 6) & 0x1F))); | |
out.push_back(static_cast<unsigned char>(0x80 | (ch & 0x3F))); | |
} else if (ch <= 0xFFFF) { | |
out.push_back(static_cast<unsigned char>(0xE0 | ((ch >> 12) & 0x0F))); | |
out.push_back(static_cast<unsigned char>(0x80 | ((ch >> 6) & 0x3F))); | |
out.push_back(static_cast<unsigned char>(0x80 | (ch & 0x3F))); | |
} else if (ch <= 0x10FFFF) { | |
out.push_back(static_cast<unsigned char>(0xF0 | ((ch >> 18) & 0x07))); | |
out.push_back(static_cast<unsigned char>(0x80 | ((ch >> 12) & 0x3F))); | |
out.push_back(static_cast<unsigned char>(0x80 | ((ch >> 6) & 0x3F))); | |
out.push_back(static_cast<unsigned char>(0x80 | (ch & 0x3F))); | |
} else { | |
// Invalid Unicode code point | |
} | |
} | |
// Helper function to remove the last UTF-8 character from a string | |
void pop_back_utf8_char(std::string & line) { | |
if (line.empty()) { | |
return; | |
} | |
size_t pos = line.length() - 1; | |
// 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 | |
} | |
} | |
line.erase(pos); | |
} | |
bool readline_advanced(std::string & line, bool multiline_input) { | |
if (out != stdout) { | |
fflush(stdout); | |
} | |
line.clear(); | |
std::vector<int> widths; | |
bool is_special_char = false; | |
bool end_of_stream = false; | |
char32_t input_char; | |
while (true) { | |
fflush(out); // Ensure all output is displayed before waiting for input | |
input_char = getchar32(); | |
if (input_char == '\r' || input_char == '\n') { | |
break; | |
} | |
if (input_char == (char32_t) WEOF || input_char == 0x04 /* Ctrl+D*/) { | |
end_of_stream = true; | |
break; | |
} | |
if (is_special_char) { | |
set_display(user_input); | |
replace_last(line.back()); | |
is_special_char = false; | |
} | |
if (input_char == '\033') { // Escape sequence | |
char32_t code = getchar32(); | |
if (code == '[' || 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 == '~') { | |
break; | |
} | |
} | |
} | |
} 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()); | |
} | |
} 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; | |
} | |
widths.push_back(width); | |
} | |
if (!line.empty() && (line.back() == '\\' || line.back() == '/')) { | |
set_display(prompt); | |
replace_last(line.back()); | |
is_special_char = true; | |
} | |
} | |
bool has_more = multiline_input; | |
if (is_special_char) { | |
replace_last(' '); | |
pop_cursor(); | |
char last = line.back(); | |
line.pop_back(); | |
if (last == '\\') { | |
line += '\n'; | |
fputc('\n', out); | |
has_more = !has_more; | |
} else { | |
// llama will just eat the single space, it won't act as a space | |
if (line.length() == 1 && line.back() == ' ') { | |
line.clear(); | |
pop_cursor(); | |
} | |
has_more = false; | |
} | |
} else { | |
if (end_of_stream) { | |
has_more = false; | |
} else { | |
line += '\n'; | |
fputc('\n', out); | |
} | |
} | |
fflush(out); | |
return has_more; | |
} | |
bool readline_simple(std::string & line, bool multiline_input) { | |
std::wstring wline; | |
if (!std::getline(std::wcin, wline)) { | |
// Input stream is bad or EOF received | |
line.clear(); | |
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); | |
return false; | |
} | |
int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wline[0], (int)wline.size(), NULL, 0, NULL, NULL); | |
line.resize(size_needed); | |
WideCharToMultiByte(CP_UTF8, 0, &wline[0], (int)wline.size(), &line[0], size_needed, NULL, NULL); | |
if (!std::getline(std::cin, line)) { | |
// Input stream is bad or EOF received | |
line.clear(); | |
return false; | |
} | |
if (!line.empty()) { | |
char last = line.back(); | |
if (last == '/') { // Always return control on '/' symbol | |
line.pop_back(); | |
return false; | |
} | |
if (last == '\\') { // '\\' changes the default action | |
line.pop_back(); | |
multiline_input = !multiline_input; | |
} | |
} | |
line += '\n'; | |
// By default, continue input if multiline_input is set | |
return multiline_input; | |
} | |
bool readline(std::string & line, bool multiline_input) { | |
set_display(user_input); | |
if (simple_io) { | |
return readline_simple(line, multiline_input); | |
} | |
return readline_advanced(line, multiline_input); | |
} | |
} | |