From 2210bf7d69f0438c84bb5dccdca0f24c50161f24 Mon Sep 17 00:00:00 2001 From: Shylie Date: Fri, 8 Mar 2024 15:44:27 -0500 Subject: [PATCH] Add single codepoint unicode character support --- include/terml.h | 11 +- source/terml.cpp | 248 ++++++++++++++++++++++----------------- source/terml_linux.cpp | 4 +- source/terml_linux.h | 4 +- source/terml_private.h | 17 +-- source/terml_windows.cpp | 20 +++- source/terml_windows.h | 5 +- 7 files changed, 181 insertions(+), 128 deletions(-) diff --git a/include/terml.h b/include/terml.h index 2480b28..973fdd6 100644 --- a/include/terml.h +++ b/include/terml.h @@ -14,14 +14,21 @@ typedef void (*terml_resize_callback)( unsigned int previous_height ); +struct tcell +{ + int codepoint; + int foreground; + int background; +}; + int terml_init(); int terml_deinit(); unsigned int terml_get_width(); unsigned int terml_get_height(); -char terml_get(unsigned int x, unsigned int y, int* foreground_color, int* background_color); -void terml_set(unsigned int x, unsigned int y, char c, int foreground_color, int background_color); +int terml_get(unsigned int x, unsigned int y, const tcell** cell); +int terml_set(unsigned int x, unsigned int y, tcell cell); void terml_flush(); void terml_set_main_callback(terml_main_callback); diff --git a/source/terml.cpp b/source/terml.cpp index 8761ea3..af4b808 100644 --- a/source/terml.cpp +++ b/source/terml.cpp @@ -9,108 +9,102 @@ #include #include +namespace +{ + constexpr int BYTE_ONE_MASK = 0b0000'0000'0000'0000'0011'1111; + constexpr int BYTE_TWO_MASK = 0b0000'0000'0000'1111'1100'0000; + constexpr int BYTE_THREE_MASK = 0b0000'0011'1111'0000'0000'0000; + constexpr int BYTE_FOUR_MASK = 0b0001'1100'0000'0000'0000'0000; + + constexpr int BYTE_ONE_MASK_OFFSET = 0; + constexpr int BYTE_TWO_MASK_OFFSET = BYTE_ONE_MASK_OFFSET + 2; + constexpr int BYTE_THREE_MASK_OFFSET = BYTE_TWO_MASK_OFFSET + 2; + constexpr int BYTE_FOUR_MASK_OFFSET = BYTE_THREE_MASK_OFFSET + 2; + + constexpr int ONE_BYTE_FILL = 0b10000000; + constexpr int TWO_BYTE_FILL = 0b11000000'10000000; + constexpr int THREE_BYTE_FILL = 0b11100000'10000000'10000000; + constexpr int FOUR_BYTE_FILL = 0b11110000'10000000'10000000'10000000; + + void print_cell(tcell cell, const tcell* last) + { + if (!last || cell.foreground != last->foreground) + { + printf(FGP, (cell.foreground & 0xFF0000) >> 16, (cell.foreground & 0xFF00) >> 8, cell.foreground & 0xFF); + } + if (!last || cell.background != last->background) + { + printf(BGP, (cell.background & 0xFF0000) >> 16, (cell.background & 0xFF00) >> 8, cell.background & 0xFF); + } + + // one-byte codepoints + if (cell.codepoint < 0x80) + { + const char str[] = { cell.codepoint, '\0' }; + printf("%s", str); + } + // two-byte codepoints + else if (cell.codepoint < 0x800) + { + const int byte_one = (cell.codepoint & BYTE_ONE_MASK) << BYTE_ONE_MASK_OFFSET; + const int byte_two = (cell.codepoint & BYTE_TWO_MASK) << BYTE_TWO_MASK_OFFSET; + const int utf8 = TWO_BYTE_FILL | byte_one | byte_two; + + const char str[] = { (utf8 & 0xFF00) >> 8, utf8 & 0xFF, '\0' }; + printf("%s", str); + } + // three-byte codepoints + else if (cell.codepoint < 0x10000) + { + const int byte_one = (cell.codepoint & BYTE_ONE_MASK) << BYTE_ONE_MASK_OFFSET; + const int byte_two = (cell.codepoint & BYTE_TWO_MASK) << BYTE_TWO_MASK_OFFSET; + const int byte_three = (cell.codepoint & BYTE_THREE_MASK) << BYTE_THREE_MASK_OFFSET; + const int utf8 = THREE_BYTE_FILL | byte_one | byte_two | byte_three; + + const char str[] = { (utf8 & 0xFF0000) >> 16, (utf8 & 0xFF00) >> 8, utf8 & 0xFF, '\0' }; + printf("%s", str); + } + // four-byte codepoints + else if (cell.codepoint < 0x110000) + { + const int byte_one = (cell.codepoint & BYTE_ONE_MASK) << BYTE_ONE_MASK_OFFSET; + const int byte_two = (cell.codepoint & BYTE_TWO_MASK) << BYTE_TWO_MASK_OFFSET; + const int byte_three = (cell.codepoint & BYTE_THREE_MASK) << BYTE_THREE_MASK_OFFSET; + const int byte_four = (cell.codepoint & BYTE_FOUR_MASK) << BYTE_FOUR_MASK_OFFSET; + const int utf8 = FOUR_BYTE_FILL | byte_one | byte_two | byte_three | byte_four; + + const char str[] = { (utf8 & 0xFF000000) >> 24, (utf8 & 0xFF0000) >> 16, (utf8 & 0xFF00) >> 8, utf8 & 0xFF, '\0' }; + printf("%s", str); + } + } +} + terml::terml() : + cells(nullptr), + width(0), + height(0), main(nullptr), quit(nullptr), key(nullptr), resize(nullptr), - buffer(nullptr), - width(0), - height(0) + should_quit(false), + really_should_quit(false) { - printf(ALT_BUF() HIDE_CURSOR()); - fflush(stdout); } terml::~terml() { - printf(REG_BUF() SHOW_CURSOR()); - fflush(stdout); - - delete[] buffer; + if (cells) { delete[] cells; } } -char terml::get(unsigned int x, unsigned int y, int* fg, int* bg) const +const tcell& terml::get(unsigned int x, unsigned int y) const { - const unsigned int offset = x + y * width; - - if (fg) - { - const unsigned char r = - (buffer[offset * CELL_SIZE + 9] - '0') + - (buffer[offset * CELL_SIZE + 8] - '0') * 10 + - (buffer[offset * CELL_SIZE + 7] - '0') * 100; - - const unsigned char g = - (buffer[offset * CELL_SIZE + 13] - '0') + - (buffer[offset * CELL_SIZE + 12] - '0') * 10 + - (buffer[offset * CELL_SIZE + 11] - '0') * 100; - - const unsigned char b = - (buffer[offset * CELL_SIZE + 17] - '0') + - (buffer[offset * CELL_SIZE + 16] - '0') * 10 + - (buffer[offset * CELL_SIZE + 15] - '0') * 100; - - *fg = (r << 16) | (g << 8) | b; - } - - if (bg) - { - const unsigned char r = - (buffer[offset * CELL_SIZE + 28] - '0') + - (buffer[offset * CELL_SIZE + 27] - '0') * 10 + - (buffer[offset * CELL_SIZE + 26] - '0') * 100; - - const unsigned char g = - (buffer[offset * CELL_SIZE + 32] - '0') + - (buffer[offset * CELL_SIZE + 31] - '0') * 10 + - (buffer[offset * CELL_SIZE + 30] - '0') * 100; - - const unsigned char b = - (buffer[offset * CELL_SIZE + 36] - '0') + - (buffer[offset * CELL_SIZE + 35] - '0') * 10 + - (buffer[offset * CELL_SIZE + 34] - '0') * 100; - - *bg = (r << 16) | (g << 8) | b; - } - - return buffer[offset * CELL_SIZE + CELL_SIZE - 1]; + return cells[x + y * width]; } -void terml::set(unsigned int x, unsigned int y, char c, int fg, int bg) +void terml::set(unsigned int x, unsigned int y, tcell cell) { - const unsigned int offset = x + y * width; - - unsigned char fgr = (fg & 0xFF0000) >> 16; - unsigned char fgg = (fg & 0x00FF00) >> 8; - unsigned char fgb = (fg & 0x0000FF); - - unsigned char bgr = (bg & 0xFF0000) >> 16; - unsigned char bgg = (bg & 0x00FF00) >> 8; - unsigned char bgb = (bg & 0x0000FF); - - for (int i = 0; i < 3; i++) - { - buffer[offset * CELL_SIZE + (9 - i)] = (fgr % 10) + '0'; - fgr /= 10; - - buffer[offset * CELL_SIZE + (13 - i)] = (fgg % 10) + '0'; - fgg /= 10; - - buffer[offset * CELL_SIZE + (17 - i)] = (fgb % 10) + '0'; - fgb /= 10; - - buffer[offset * CELL_SIZE + (28 - i)] = (bgr % 10) + '0'; - bgr /= 10; - - buffer[offset * CELL_SIZE + (32 - i)] = (bgg % 10) + '0'; - bgg /= 10; - - buffer[offset * CELL_SIZE + (36 - i)] = (bgb % 10) + '0'; - bgb /= 10; - } - - buffer[offset * CELL_SIZE + CELL_SIZE - 1] = c; + cells[x + y * width] = cell; } void terml::flush() const @@ -118,7 +112,11 @@ void terml::flush() const printf(CUP(1, 1)); fflush(stdout); - printf("%s", buffer); + print_cell(cells[0], nullptr); + for (int i = 1; i < width * height; i++) + { + print_cell(cells[i], &cells[i - 1]); + } fflush(stdout); } @@ -155,7 +153,7 @@ void terml::mainloop() should_quit = false; really_should_quit = false; - const unsigned long long wait_time = timer_frequency() / 1000; + const unsigned long long wait_time = timer_frequency() / 60; unsigned long long last_time = timer(); while (!really_should_quit) @@ -230,17 +228,9 @@ void terml::setup_buffer() width = new_width; height = new_height; - if (buffer) { delete[] buffer; } - buffer = new char[CELL_SIZE * width * height + 1]; - - constexpr char BLANK_CELL[] = FG(255, 255, 255) CSI "48;2;000;000;000m" " "; - - for (int i = 0; i < width * height; i++) - { - memcpy(buffer + i * CELL_SIZE, BLANK_CELL, CELL_SIZE); - } - - buffer[CELL_SIZE * width * height] = '\0'; + if (cells) { delete[] cells; } + cells = new tcell[width * height]; + memset(cells, 0, sizeof(tcell) * width * height); if (resize) { @@ -249,6 +239,23 @@ void terml::setup_buffer() } } +void terml::set_console_settings() +{ + setvbuf(stdout, nullptr, _IOFBF, BUFSIZ * BUFSIZ); + printf(ALT_BUF() HIDE_CURSOR()); + fflush(stdout); + set_console_settings_impl(); +} + +void terml::reset_console_settings() +{ + setvbuf(stdout, nullptr, _IOLBF, BUFSIZ); + printf(REG_BUF() SHOW_CURSOR()); + fflush(stdout); + + reset_console_settings_impl(); +} + static terml* TERML_G; static const char* LAST_ERROR = nullptr; @@ -266,16 +273,20 @@ extern "C" TERML_G = new terml_linux; #endif } + else + { + throw "terml already initialized."; + } TERML_G->set_console_settings(); TERML_G->setup_buffer(); - return 1; + return 0; } catch (const char* c) { LAST_ERROR = c; - return 0; + return 1; } } @@ -289,12 +300,12 @@ extern "C" delete TERML_G; } - return 1; + return 0; } catch (const char* c) { LAST_ERROR = c; - return 0; + return 1; } } @@ -308,14 +319,37 @@ extern "C" return TERML_G->get_height(); } - char terml_get(unsigned int x, unsigned int y, int* fg, int* bg) + int terml_get(unsigned int x, unsigned int y, const tcell** cell) { - return TERML_G->get(x, y, fg, bg); + if (x >= TERML_G->get_width() || y >= TERML_G->get_height()) + { + LAST_ERROR = "Coordinates out of bounds."; + return 1; + } + else if (!cell) + { + LAST_ERROR = "Null output pointer."; + return 1; + } + else + { + *cell = &TERML_G->get(x, y); + return 0; + } } - void terml_set(unsigned int x, unsigned int y, char c, int fg, int bg) + int terml_set(unsigned int x, unsigned int y, tcell cell) { - TERML_G->set(x, y, c, fg, bg); + if (x >= TERML_G->get_width() || y >= TERML_G->get_height()) + { + LAST_ERROR = "Coordinates out of bounds."; + return 1; + } + else + { + TERML_G->set(x, y, cell); + return 0; + } } void terml_flush() diff --git a/source/terml_linux.cpp b/source/terml_linux.cpp index 6b3511f..4776392 100644 --- a/source/terml_linux.cpp +++ b/source/terml_linux.cpp @@ -7,7 +7,7 @@ #include #include -void terml_linux::set_console_settings() +void terml_linux::set_console_settings_impl() { tcgetattr(STDIN_FILENO, &old_input_settings); tcgetattr(STDOUT_FILENO, &old_output_settings); @@ -18,7 +18,7 @@ void terml_linux::set_console_settings() tcsetattr(STDIN_FILENO, TCSANOW, &t); } -void terml_linux::reset_console_settings() +void terml_linux::reset_console_settings_impl() { tcsetattr(STDIN_FILENO, TCSANOW, &old_input_settings); tcsetattr(STDOUT_FILENO, TCSANOW, &old_output_settings); diff --git a/source/terml_linux.h b/source/terml_linux.h index 13c4915..2f95ec2 100644 --- a/source/terml_linux.h +++ b/source/terml_linux.h @@ -11,8 +11,8 @@ class terml_linux : public terml { protected: - virtual void set_console_settings() override; - virtual void reset_console_settings() override; + virtual void set_console_settings_impl() override; + virtual void reset_console_settings_impl() override; virtual void read_stdin(char* buffer, unsigned int buffer_size) override; virtual unsigned long long timer() override; virtual unsigned long long timer_frequency() override; diff --git a/source/terml_private.h b/source/terml_private.h index a3be10f..679ab0c 100644 --- a/source/terml_private.h +++ b/source/terml_private.h @@ -25,6 +25,9 @@ #define FG(r, g, b) CSI "38;2;" STRINGIFY(r) ";" STRINGIFY(g) ";" STRINGIFY(b) "m" #define BG(r, g, b) CSI "48;2;" STRINGIFY(r) ";" STRINGIFY(g) ";" STRINGIFY(b) "m" +#define FGP CSI "38;2;%d;%d;%dm" +#define BGP CSI "48;2;%d;%d;%dm" + class terml { public: @@ -37,8 +40,8 @@ public: terml& operator=(const terml&) = delete; terml& operator=(terml&&) = delete; - char get(unsigned int x, unsigned int y, int* fg, int* bg) const; - void set(unsigned int x, unsigned int y, char c, int fg, int bg); + const tcell& get(unsigned int x, unsigned int y) const; + void set(unsigned int x, unsigned int y, tcell cell); void flush() const; void set_main_callback(terml_main_callback); @@ -54,10 +57,12 @@ public: void setup_buffer(); - virtual void set_console_settings() = 0; - virtual void reset_console_settings() = 0; + void set_console_settings(); + void reset_console_settings(); protected: + virtual void set_console_settings_impl() = 0; + virtual void reset_console_settings_impl() = 0; virtual void read_stdin(char* buffer, unsigned int buffer_size) = 0; virtual unsigned long long timer() = 0; virtual unsigned long long timer_frequency() = 0; @@ -66,7 +71,7 @@ protected: void key_event(char code) const; private: - char* buffer; + tcell* cells; unsigned int width; unsigned int height; terml_main_callback main; @@ -75,8 +80,6 @@ private: terml_resize_callback resize; bool should_quit; bool really_should_quit; - - static const unsigned int CELL_SIZE = sizeof(FG(255, 255, 255) BG(255, 255, 255) " ") - 1; }; #endif//TERML_TERML_PRIVATE_H \ No newline at end of file diff --git a/source/terml_windows.cpp b/source/terml_windows.cpp index d5b1bff..51c49e0 100644 --- a/source/terml_windows.cpp +++ b/source/terml_windows.cpp @@ -2,7 +2,7 @@ #ifdef _WIN32 -void terml_windows::set_console_settings() +void terml_windows::set_console_settings_impl() { handle_stdin = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(handle_stdin, &previous_input_mode); @@ -13,7 +13,7 @@ void terml_windows::set_console_settings() new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; if (!SetConsoleMode(handle_stdin, new_mode)) { - throw "Failed to set stdin mode"; + throw "Failed to set stdin mode."; } HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); @@ -23,19 +23,27 @@ void terml_windows::set_console_settings() new_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(handle, new_mode)) { - throw "Failed to set stdout mode"; + throw "Failed to set stdout mode."; } + + previous_codepage = GetConsoleOutputCP(); + SetConsoleOutputCP(CP_UTF8); } -void terml_windows::reset_console_settings() +void terml_windows::reset_console_settings_impl() { if (!SetConsoleMode(handle_stdin, previous_input_mode)) { - throw "Failed to reset stdin mode"; + throw "Failed to reset stdin mode."; } if (!SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), previous_output_mode)) { - throw "Failed to reset stdout mode"; + throw "Failed to reset stdout mode."; + } + + if (!SetConsoleOutputCP(previous_codepage)) + { + throw "Failed to reset codepage."; } } diff --git a/source/terml_windows.h b/source/terml_windows.h index 5ecc37e..6b27387 100644 --- a/source/terml_windows.h +++ b/source/terml_windows.h @@ -11,8 +11,8 @@ class terml_windows : public terml { protected: - virtual void set_console_settings() override; - virtual void reset_console_settings() override; + virtual void set_console_settings_impl() override; + virtual void reset_console_settings_impl() override; virtual void read_stdin(char* buffer, unsigned int buffer_size) override; virtual unsigned long long timer() override; virtual unsigned long long timer_frequency() override; @@ -21,6 +21,7 @@ protected: private: DWORD previous_input_mode; DWORD previous_output_mode; + UINT previous_codepage; HANDLE handle_stdin; };