replace ncurses with termios

This commit is contained in:
2025-07-08 15:36:21 -04:00
parent 8302503333
commit b4f1826464
4 changed files with 190 additions and 70 deletions

View File

@@ -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

View File

@@ -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();
}
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); }

View File

@@ -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) {

View File

@@ -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();
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;