commit 9ac3bd6e02752e28712352f908f36909712b8fca
parent 729efd457b15b460208011abe362a73b0b41cf71
Author: amrfti <andrew@kloet.net>
Date: Tue, 8 Jul 2025 15:36:21 -0400
replace ncurses with termios
Diffstat:
4 files changed, 188 insertions(+), 68 deletions(-)
diff --git 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
@@ -8,19 +8,86 @@
#include "Waterline.h"
#include "Whale.h"
#include <algorithm>
+#include <cstdio>
+#include <cstring>
#include <iostream>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
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<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(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<unsigned char>(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<unsigned char>(newCell.colorChar)];
+
+ // Add the character
+ output += newCell.ch;
}
}
}
- refresh();
+
+ // Output everything at once for better performance
+ if (!output.empty()) {
+ printf("%s", output.c_str());
+ fflush(stdout);
+ }
}
-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);
+// 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() { endwin(); }
+Aquarium::~Aquarium() { cleanup_terminal(0); }
diff --git a/src/Aquarium.h b/src/Aquarium.h
@@ -1,7 +1,6 @@
#pragma once
#include "Entity.h"
#include <memory>
-#include <ncurses.h>
#include <vector>
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<std::unique_ptr<Entity>> 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 <typename T, typename... Args> void addEntityImpl(Args &&...args) {
diff --git a/src/main.cpp b/src/main.cpp
@@ -1,35 +1,45 @@
+// main.cpp
#include "Aquarium.h"
-#include <cstdio>
-#include <cstdlib>
+#ifdef __OpenBSD__
#include <unistd.h>
+#endif
+#include <chrono>
+#include <iostream>
+#include <thread>
-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();
+
+#ifdef __OpenBSD__
+ // Most restrictive pledge - no file access needed!
+ if (pledge("stdio tty", NULL) == -1) {
+ perror("pledge");
+ return 1;
}
+#endif
- Aquarium &aquarium = Aquarium::getInstance();
- aquarium.resize();
+ // 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;