commit 226adeaf72ae944f3f912c175c26f613a3232145 Author: Shylie Date: Mon Dec 25 11:26:13 2023 -0500 Initial commit. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b7686a3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.18) + +project(terml CXX) + +add_library(terml STATIC + include/terml.h + + source/terml_private.h + source/terml_windows.h + source/terml_linux.h + source/terml_windows.cpp + source/terml_linux.cpp + source/terml.cpp +) + +set_target_properties(terml + PROPERTIES + CXX_STANDARD 11 +) + +target_include_directories(terml + PUBLIC + include + PRIVATE + source +) diff --git a/include/terml.h b/include/terml.h new file mode 100644 index 0000000..7e15d79 --- /dev/null +++ b/include/terml.h @@ -0,0 +1,36 @@ +#ifndef TERML_TERML_H +#define TERML_TERML_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef void (*terml_main_callback)(); +typedef int (*terml_quit_callback)(); +typedef void (*terml_key_callback) (char code); + +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); +void terml_flush(); + +void terml_set_main_callback(terml_main_callback); +void terml_set_quit_callback(terml_quit_callback); +void terml_set_key_callback(terml_key_callback); + +void terml_start(); +void terml_stop(); + +const char* terml_get_error(); + +#ifdef __cplusplus +} +#endif + +#endif//TERML_TERML_H \ No newline at end of file diff --git a/source/terml.cpp b/source/terml.cpp new file mode 100644 index 0000000..44e8e20 --- /dev/null +++ b/source/terml.cpp @@ -0,0 +1,347 @@ +#include "terml_private.h" + +#ifdef _WIN32 +#include "terml_windows.h" +#else +#include "terml_linux.h" +#endif + +#include +#include + +terml::terml() : + main(nullptr), + quit(nullptr), + key(nullptr), + buffer(nullptr), + width(0), + height(0) +{ + printf(ALT_BUF() HIDE_CURSOR()); + fflush(stdout); +} + +terml::~terml() +{ + printf(REG_BUF() SHOW_CURSOR()); + fflush(stdout); + + delete[] buffer; +} + +char terml::get(unsigned int x, unsigned int y, int* fg, int* bg) 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 + 26] - '0') + + (buffer[offset * CELL_SIZE + 27] - '0') * 10 + + (buffer[offset * CELL_SIZE + 28] - '0') * 100; + + const unsigned char g = + (buffer[offset * CELL_SIZE + 30] - '0') + + (buffer[offset * CELL_SIZE + 31] - '0') * 10 + + (buffer[offset * CELL_SIZE + 32] - '0') * 100; + + const unsigned char b = + (buffer[offset * CELL_SIZE + 34] - '0') + + (buffer[offset * CELL_SIZE + 35] - '0') * 10 + + (buffer[offset * CELL_SIZE + 36] - '0') * 100; + + *bg = (r << 16) | (g << 8) | b; + } + + return buffer[offset * CELL_SIZE + CELL_SIZE - 1]; +} + +void terml::set(unsigned int x, unsigned int y, char c, int fg, int bg) +{ + 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; +} + +void terml::flush() const +{ + printf(CUP(1, 1)); + fflush(stdout); + + printf("%s", buffer); + fflush(stdout); +} + +void terml::set_main_callback(terml_main_callback callback) +{ + main = callback; +} + +void terml::set_quit_callback(terml_quit_callback callback) +{ + quit = callback; +} + +void terml::set_key_callback(terml_key_callback callback) +{ + key = callback; +} + +void terml::key_event(char code) +{ + if (key) + { + key(code); + } +} + +void terml::mainloop() +{ + should_quit = false; + really_should_quit = false; + + const unsigned long long wait_time = timer_frequency() / 1000; + unsigned long long last_time = timer(); + + while (!really_should_quit) + { + unsigned long long current_time = timer(); + + while (current_time >= last_time + wait_time) + { + main(); + + process_events(); + + last_time += wait_time; + } + + if (should_quit) + { + if (!quit || quit()) + { + really_should_quit = true; + } + else + { + should_quit = false; + } + } + } +} + +void terml::stop() +{ + should_quit = true; +} + +unsigned int terml::get_width() const +{ + return width; +} + +unsigned int terml::get_height() const +{ + return height; +} + +void terml::setup_buffer() +{ + printf(CUP(999, 999) REPORT_CUSROR_POSITION()); + fflush(stdout); + + constexpr const unsigned int STDIN_BUFFER_SIZE = 16; + char stdin_buffer[STDIN_BUFFER_SIZE + 1]; + read_stdin(stdin_buffer, STDIN_BUFFER_SIZE); + + stdin_buffer[STDIN_BUFFER_SIZE] = '\0'; + + unsigned int new_width = width; + unsigned int new_height = height; + const int scanned = sscanf(stdin_buffer, CURSOR_POSITION_FORMAT(), &new_height, &new_width); + if (scanned != 2) + { + throw "Failed to determine screen size."; + } + + if (width != new_width || height != new_height) + { + 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'; + } +} + +static terml* TERML_G; +static const char* LAST_ERROR = nullptr; + +extern "C" +{ + int terml_init() + { + try + { + if (!TERML_G) + { +#ifdef _WIN32 + TERML_G = new terml_windows; +#else + TERML_G = new terml_linux; +#endif + } + + TERML_G->set_console_settings(); + TERML_G->setup_buffer(); + + return 1; + } + catch (const char* c) + { + LAST_ERROR = c; + return 0; + } + } + + int terml_deinit() + { + try + { + if (TERML_G) + { + TERML_G->reset_console_settings(); + delete TERML_G; + } + + return 1; + } + catch (const char* c) + { + LAST_ERROR = c; + return 0; + } + } + + unsigned int terml_get_width() + { + return TERML_G->get_width(); + } + + unsigned int terml_get_height() + { + return TERML_G->get_height(); + } + + char terml_get(unsigned int x, unsigned int y, int* fg, int* bg) + { + return TERML_G->get(x, y, fg, bg); + } + + void terml_set(unsigned int x, unsigned int y, char c, int fg, int bg) + { + TERML_G->set(x, y, c, fg, bg); + } + + void terml_flush() + { + TERML_G->flush(); + } + + void terml_set_main_callback(terml_main_callback callback) + { + TERML_G->set_main_callback(callback); + } + + void terml_set_quit_callback(terml_quit_callback callback) + { + TERML_G->set_quit_callback(callback); + } + + void terml_set_key_callback(terml_key_callback callback) + { + TERML_G->set_key_callback(callback); + } + + void terml_start() + { + try + { + TERML_G->mainloop(); + } + catch (const char* c) + { + LAST_ERROR = c; + } + } + + void terml_stop() + { + TERML_G->stop(); + } + + const char* terml_get_error() + { + const char* err = LAST_ERROR; + LAST_ERROR = nullptr; + return err; + } +} \ No newline at end of file diff --git a/source/terml_linux.cpp b/source/terml_linux.cpp new file mode 100644 index 0000000..6b3511f --- /dev/null +++ b/source/terml_linux.cpp @@ -0,0 +1,85 @@ +#include "terml_linux.h" + +#ifndef _WIN32 + +#include +#include +#include +#include + +void terml_linux::set_console_settings() +{ + tcgetattr(STDIN_FILENO, &old_input_settings); + tcgetattr(STDOUT_FILENO, &old_output_settings); + + termios t = old_input_settings; + t.c_lflag &= ~(ICANON | ECHO); + + tcsetattr(STDIN_FILENO, TCSANOW, &t); +} + +void terml_linux::reset_console_settings() +{ + tcsetattr(STDIN_FILENO, TCSANOW, &old_input_settings); + tcsetattr(STDOUT_FILENO, TCSANOW, &old_output_settings); +} + +void terml_linux::read_stdin(char* buffer, unsigned int buffer_size) +{ + for (int i = 0; i < buffer_size;) + { + char ch = getchar(); + if (i == 0 && ch != '\x1B') + { + key_event(ch); + } + else + { + buffer[i++] = ch; + } + + if (ch == 'R') { return; } + } +} + +unsigned long long terml_linux::timer() +{ + timespec spec; + clock_gettime(CLOCK_REALTIME, &spec); + + return timer_frequency() * spec.tv_sec + spec.tv_nsec; +} + +unsigned long long terml_linux::timer_frequency() +{ + return 1000000000; // nanoseconds / second +} + +void terml_linux::process_events() +{ + constexpr int BUF_SIZE = 128; + char buf[BUF_SIZE]; + int num_read; + + pollfd pfd; + pfd.fd = STDIN_FILENO; + pfd.events = POLLIN; + pfd.revents = 0; + + poll(&pfd, 1, 0); + + while (pfd.revents & POLLIN) + { + num_read = read(STDIN_FILENO, buf, BUF_SIZE); + for (int i = 0; i < num_read; i++) + { + key_event(buf[i]); + } + + poll(&pfd, 1, 0); + } + + setup_buffer(); +} + +#endif \ No newline at end of file diff --git a/source/terml_linux.h b/source/terml_linux.h new file mode 100644 index 0000000..13c4915 --- /dev/null +++ b/source/terml_linux.h @@ -0,0 +1,28 @@ +#ifndef TERML_TERML_LINUX_H +#define TERML_TERML_LINUX_H + +#include "terml_private.h" + +#ifndef _WIN32 + +#include +#include + +class terml_linux : public terml +{ +protected: + virtual void set_console_settings() override; + virtual void reset_console_settings() 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; + virtual void process_events() override; + +private: + termios old_input_settings; + termios old_output_settings; +}; + +#endif//_WIN32 + +#endif//TERML_TERML_LINUX_H \ No newline at end of file diff --git a/source/terml_private.h b/source/terml_private.h new file mode 100644 index 0000000..18ba3da --- /dev/null +++ b/source/terml_private.h @@ -0,0 +1,80 @@ +#ifndef TERML_TERML_PRIVATE_H +#define TERML_TERML_PRIVATE_H + +#include "terml.h" + +#define STRINGIFY2(x) #x +#define STRINGIFY(x) STRINGIFY2(x) + +#define ESC "\x1B" +#define CSI "\x1B[" + +#define ALT_BUF() CSI "?1049h" +#define REG_BUF() CSI "?1049l" + +#define CLEAR_SCREEN() CSI "2J" + +#define HIDE_CURSOR() CSI "?25l" +#define SHOW_CURSOR() CSI "?25h" + +#define REPORT_CUSROR_POSITION() CSI "6n" +#define CURSOR_POSITION_FORMAT() "%*1s[%u;%u" + +#define CUP(x, y) CSI STRINGIFY(y) ";" STRINGIFY(x) "H" + +#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" + +class terml +{ +public: + terml(); + virtual ~terml(); + + terml(const terml&) = delete; + terml(terml&&) = delete; + + 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); + void flush() const; + + void set_main_callback(terml_main_callback); + void set_quit_callback(terml_quit_callback); + void set_key_callback(terml_key_callback); + + void mainloop(); + void stop(); + + unsigned int get_width() const; + unsigned int get_height() const; + + void setup_buffer(); + + virtual void set_console_settings() = 0; + virtual void reset_console_settings() = 0; + +protected: + virtual void read_stdin(char* buffer, unsigned int buffer_size) = 0; + virtual unsigned long long timer() = 0; + virtual unsigned long long timer_frequency() = 0; + virtual void process_events() = 0; + + void key_event(char code); + +private: + char* buffer; + unsigned int width; + unsigned int height; + terml_main_callback main; + terml_quit_callback quit; + terml_key_callback key; + 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 new file mode 100644 index 0000000..c2d4874 --- /dev/null +++ b/source/terml_windows.cpp @@ -0,0 +1,100 @@ +#include "terml_windows.h" + +#ifdef _WIN32 + +void terml_windows::set_console_settings() +{ + handle_stdin = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(handle_stdin, &previous_input_mode); + DWORD new_mode = previous_input_mode; + new_mode &= ~ENABLE_ECHO_INPUT; + new_mode &= ~ENABLE_LINE_INPUT; + new_mode |= ENABLE_WINDOW_INPUT; + new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + if (!SetConsoleMode(handle_stdin, new_mode)) + { + throw "Failed to set stdin mode"; + } + + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleMode(handle, &previous_output_mode); + new_mode = previous_output_mode; + new_mode |= ENABLE_PROCESSED_OUTPUT; + new_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (!SetConsoleMode(handle, new_mode)) + { + throw "Failed to set stdout mode"; + } +} + +void terml_windows::reset_console_settings() +{ + if (!SetConsoleMode(handle_stdin, previous_input_mode)) + { + throw "Failed to reset stdin mode"; + } + if (!SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), previous_output_mode)) + { + throw "Failed to reset stdout mode"; + } +} + +void terml_windows::read_stdin(char* buffer, unsigned int buffer_size) +{ + unsigned long unused; + ReadConsole(handle_stdin, buffer, buffer_size, &unused, nullptr); +} + +unsigned long long terml_windows::timer() +{ + LARGE_INTEGER time; + QueryPerformanceCounter(&time); + return time.QuadPart; +} + +unsigned long long terml_windows::timer_frequency() +{ + LARGE_INTEGER frequency; + QueryPerformanceFrequency(&frequency); + return frequency.QuadPart; +} + +void terml_windows::process_events() +{ + DWORD num_events_available = 0; + GetNumberOfConsoleInputEvents(handle_stdin, &num_events_available); + + DWORD num_events_read = 0; + INPUT_RECORD input_record_buffer[128]; + while (num_events_available > 0) + { + ReadConsoleInput(handle_stdin, input_record_buffer, sizeof(input_record_buffer) / sizeof(*input_record_buffer), &num_events_read); + + for (int i = 0; i < num_events_read; i++) + { + switch (input_record_buffer[i].EventType) + { + case KEY_EVENT: + { + KEY_EVENT_RECORD* record = &input_record_buffer[i].Event.KeyEvent; + if (record->bKeyDown) + { + for (int repeat = 0; repeat < record->wRepeatCount; repeat++) + { + key_event(record->uChar.AsciiChar); + } + } + break; + } + + case WINDOW_BUFFER_SIZE_EVENT: + setup_buffer(); + break; + } + } + + num_events_available -= num_events_read; + } +} + +#endif \ No newline at end of file diff --git a/source/terml_windows.h b/source/terml_windows.h new file mode 100644 index 0000000..5ecc37e --- /dev/null +++ b/source/terml_windows.h @@ -0,0 +1,29 @@ +#ifndef TERML_TERML_WINDOWS_H +#define TERML_TERML_WINDOWS_H + +#include "terml_private.h" + +#ifdef _WIN32 + +#define WIN32_LEAN_AND_MEAN +#include + +class terml_windows : public terml +{ +protected: + virtual void set_console_settings() override; + virtual void reset_console_settings() 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; + virtual void process_events() override; + +private: + DWORD previous_input_mode; + DWORD previous_output_mode; + HANDLE handle_stdin; +}; + +#endif//_WIN32 + +#endif//TERML_TERML_WINDOWS_H \ No newline at end of file