From b4f1826464a3faabd053eed8fba9f9cc7f90f087 Mon Sep 17 00:00:00 2001 From: Andrew Kloet Date: Tue, 8 Jul 2025 15:36:21 -0400 Subject: [PATCH] replace ncurses with termios --- Makefile | 2 +- src/Aquarium.cpp | 187 +++++++++++++++++++++++++++++++++++++---------- src/Aquarium.h | 11 +-- src/main.cpp | 60 ++++++++------- 4 files changed, 190 insertions(+), 70 deletions(-) diff --git a/Makefile b/Makefile index 2d4ffdf..28bc548 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Compiler and flags CXX = g++ CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -LDFLAGS = -lncurses -ltinfo +LDFLAGS = -static -lncurses -ltinfo # Directories SRC_DIR = src diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index 65010e9..ea67c77 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -8,19 +8,86 @@ #include "Waterline.h" #include "Whale.h" #include +#include +#include #include +#include +#include +#include +#include int g_maxCells = 0; +// ANSI color codes +namespace ANSI { +const char *RESET = "\033[0m"; +const char *BOLD = "\033[1m"; +const char *CLEAR_SCREEN = "\033[2J"; +const char *CURSOR_HOME = "\033[H"; +const char *HIDE_CURSOR = "\033[?25l"; +const char *SHOW_CURSOR = "\033[?25h"; + +// Colors (foreground) +const char *BLACK = "\033[30m"; +const char *RED = "\033[31m"; +const char *GREEN = "\033[32m"; +const char *YELLOW = "\033[33m"; +const char *BLUE = "\033[34m"; +const char *MAGENTA = "\033[35m"; +const char *CYAN = "\033[36m"; +const char *WHITE = "\033[37m"; + +// Move cursor to position +std::string moveTo(int row, int col) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1); + return std::string(buffer); +} +} // namespace ANSI + +// Global terminal state +static struct termios original_termios; +static bool termios_saved = false; + +// Signal handler for cleanup +void cleanup_terminal(int sig) { + if (termios_saved) { + tcsetattr(STDIN_FILENO, TCSANOW, &original_termios); + } + printf("%s%s", ANSI::SHOW_CURSOR, ANSI::RESET); + fflush(stdout); + if (sig != 0) { + exit(sig); + } +} + Aquarium::Aquarium() { - initscr(); - noecho(); - cbreak(); - nodelay(stdscr, TRUE); - curs_set(0); - initColors(); - timeout(100); - getmaxyx(stdscr, height, width); + // Save original terminal settings + if (tcgetattr(STDIN_FILENO, &original_termios) == 0) { + termios_saved = true; + } + + // Set up signal handlers for cleanup + signal(SIGINT, cleanup_terminal); + signal(SIGTERM, cleanup_terminal); + signal(SIGQUIT, cleanup_terminal); + + // Set terminal to raw mode + struct termios raw = original_termios; + raw.c_lflag &= ~(ECHO | ICANON | ISIG); + raw.c_iflag &= ~(IXON | ICRNL); + raw.c_oflag &= ~(OPOST); + raw.c_cc[VMIN] = 0; // Non-blocking read + raw.c_cc[VTIME] = 1; // 100ms timeout + + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + + // Initialize display + printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR); + fflush(stdout); + + // Get terminal size + getTerminalSize(); currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); @@ -31,6 +98,18 @@ Aquarium::Aquarium() { } } +void Aquarium::getTerminalSize() { + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + height = ws.ws_row; + width = ws.ws_col; + } else { + // Fallback to reasonable defaults + height = 24; + width = 80; + } +} + void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), @@ -109,11 +188,13 @@ void Aquarium::redraw() { } void Aquarium::resize() { - clear(); - getmaxyx(stdscr, height, width); + printf("%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME); + fflush(stdout); + + getTerminalSize(); if (g_maxCells && height * width > g_maxCells) { - endwin(); + cleanup_terminal(0); std::cerr << "Error: Terminal too large. Maximum allowed area is " << g_maxCells << " cells, but current size is " << (height * width) << ".\n"; @@ -191,18 +272,28 @@ void Aquarium::drawToFrame(int y, int x, const std::string &line, void Aquarium::initColorLookup() { for (int i = 0; i < 256; ++i) - colorLookup[i] = 8; // Default black - colorLookup['r'] = 1; - colorLookup['g'] = 2; - colorLookup['y'] = 3; - colorLookup['b'] = 4; - colorLookup['m'] = 5; - colorLookup['c'] = 6; - colorLookup['w'] = 7; - colorLookup['k'] = 8; + colorLookup[i] = ANSI::BLACK; // Default black + + colorLookup['r'] = ANSI::RED; + colorLookup['g'] = ANSI::GREEN; + colorLookup['y'] = ANSI::YELLOW; + colorLookup['b'] = ANSI::BLUE; + colorLookup['m'] = ANSI::MAGENTA; + colorLookup['c'] = ANSI::CYAN; + colorLookup['w'] = ANSI::WHITE; + colorLookup['k'] = ANSI::BLACK; +} + +void Aquarium::initColors() { + // This function is kept for compatibility but does nothing + // since we're using ANSI colors directly } void Aquarium::renderToScreen() { + static std::string output; + output.clear(); + output.reserve(height * width * 20); // Reserve space for efficiency + for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { const Cell &newCell = currentFrame[y][x]; @@ -210,30 +301,48 @@ void Aquarium::renderToScreen() { if (newCell != oldCell) { oldCell = newCell; - move(y, x); - int colorPair = - colorLookup[static_cast(newCell.colorChar)]; - attrset(COLOR_PAIR(colorPair) | (newCell.bold ? A_BOLD : A_NORMAL)); - addch(newCell.ch); + // Move cursor to position + output += ANSI::moveTo(y, x); + + // Set color and attributes + output += ANSI::RESET; // Reset first + if (newCell.bold) { + output += ANSI::BOLD; + } + output += colorLookup[static_cast(newCell.colorChar)]; + + // Add the character + output += newCell.ch; } } } - refresh(); -} -void Aquarium::initColors() { - if (has_colors()) { - start_color(); - init_pair(1, COLOR_RED, COLOR_BLACK); - init_pair(2, COLOR_GREEN, COLOR_BLACK); - init_pair(3, COLOR_YELLOW, COLOR_BLACK); - init_pair(4, COLOR_BLUE, COLOR_BLACK); - init_pair(5, COLOR_MAGENTA, COLOR_BLACK); - init_pair(6, COLOR_CYAN, COLOR_BLACK); - init_pair(7, COLOR_WHITE, COLOR_BLACK); - init_pair(8, COLOR_BLACK, COLOR_BLACK); + // Output everything at once for better performance + if (!output.empty()) { + printf("%s", output.c_str()); + fflush(stdout); } } -Aquarium::~Aquarium() { endwin(); } +// Check for input (non-blocking) +int Aquarium::checkInput() { + char c; + if (read(STDIN_FILENO, &c, 1) == 1) { + return c; + } + return -1; // No input available +} + +// Check if terminal was resized (you'll need to call this periodically) +bool Aquarium::checkResize() { + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + if (ws.ws_row != height || ws.ws_col != width) { + return true; + } + } + return false; +} + +Aquarium::~Aquarium() { cleanup_terminal(0); } diff --git a/src/Aquarium.h b/src/Aquarium.h index 80e00a8..5464b87 100644 --- a/src/Aquarium.h +++ b/src/Aquarium.h @@ -1,7 +1,6 @@ #pragma once #include "Entity.h" #include -#include #include extern int g_maxCells; @@ -15,7 +14,6 @@ private: char ch = ' '; char colorChar = 'k'; bool bold = false; - bool operator==(const Cell &other) const { return ch == other.ch && colorChar == other.colorChar && bold == other.bold; @@ -28,9 +26,8 @@ private: std::vector> entities; size_t big_entity_index = 0; void ensureBigEntityExists(); - bool entities_need_sorting = true; - static inline short colorLookup[256] = {0}; + static inline const char *colorLookup[256] = {nullptr}; static inline bool colorLookupInitialized = false; public: @@ -53,17 +50,21 @@ public: void addShip(); void addSeaMonster(); void addWhale(); - void redraw(); void initColors(); void resize(); void drawToFrame(int y, int x, const std::string &line, const std::string &colorLine); + // New termios-specific methods + int checkInput(); // Returns character code or -1 if no input + bool checkResize(); // Returns true if terminal was resized + private: void clearCurrentFrame(); void renderToScreen(); void ensureEntitiesSorted(); + void getTerminalSize(); static void initColorLookup(); template void addEntityImpl(Args &&...args) { diff --git a/src/main.cpp b/src/main.cpp index 6a3a919..b749ffe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,35 +1,45 @@ +// main.cpp #include "Aquarium.h" -#include -#include +#ifdef __OpenBSD__ #include +#endif +#include +#include +#include -int main(int argc, char *argv[]) { - int opt; - while ((opt = getopt(argc, argv, "r:")) != -1) { - switch (opt) { - case 'r': - g_maxCells = std::atoi(optarg); - break; - default: - fprintf(stderr, "Usage: %s [-r max_cells]\n", argv[0]); - return 1; - } - } - +int main() { + // Get the singleton instance Aquarium &aquarium = Aquarium::getInstance(); - aquarium.resize(); +#ifdef __OpenBSD__ + // Most restrictive pledge - no file access needed! + if (pledge("stdio tty", NULL) == -1) { + perror("pledge"); + return 1; + } +#endif + + // Initialize the aquarium display + aquarium.resize(); // Setup initial entities + + // Main game loop while (true) { - aquarium.redraw(); - int ch = getch(); - if (ch != ERR) { - if (ch == 'q') - break; - if (ch == 'r') - aquarium.resize(); - flushinp(); - usleep(100000); + // Check for user input + int input = aquarium.checkInput(); + if (input == 'q' || input == 'Q' || input == 27) { // ESC key + break; } + + // Check if terminal was resized + if (aquarium.checkResize()) { + aquarium.resize(); + } + + // Redraw the aquarium + aquarium.redraw(); + + // Control frame rate (~20 FPS) + // std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return 0;