fissh

termios terminal aquarium. demo at ssh://fish@kloet.net
git clone git://kloet.net/fissh
Download | Log | Files | Refs

Aquarium.cpp (8923B)


      1 #include "Aquarium.h"
      2 #include "Bubble.h"
      3 #include "Castle.h"
      4 #include "Fish.h"
      5 #include "SeaMonster.h"
      6 #include "Seaweed.h"
      7 #include "Ship.h"
      8 #include "Waterline.h"
      9 #include "Whale.h"
     10 #include <algorithm>
     11 #include <cstdio>
     12 #include <cstring>
     13 #include <iostream>
     14 #include <signal.h>
     15 #include <sys/ioctl.h>
     16 #include <termios.h>
     17 #include <unistd.h>
     18 
     19 // ANSI color codes
     20 namespace ANSI {
     21 const char *RESET = "\033[0m";
     22 const char *BOLD = "\033[1m";
     23 const char *CLEAR_SCREEN = "\033[2J";
     24 const char *CURSOR_HOME = "\033[H";
     25 const char *HIDE_CURSOR = "\033[?25l";
     26 const char *SHOW_CURSOR = "\033[?25h";
     27 
     28 // Colors (foreground)
     29 const char *BLACK = "\033[90m";
     30 const char *RED = "\033[31m";
     31 const char *GREEN = "\033[32m";
     32 const char *YELLOW = "\033[33m";
     33 const char *BLUE = "\033[34m";
     34 const char *MAGENTA = "\033[35m";
     35 const char *CYAN = "\033[36m";
     36 const char *WHITE = "\033[37m";
     37 // Colors (background)
     38 const char *BG_BLACK = "\033[40m";
     39 const char *RESET_BLACK_BG = "\033[0;40m";
     40 
     41 // Move cursor to position
     42 std::string moveTo(int row, int col) {
     43   char buffer[32];
     44   snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1);
     45   return std::string(buffer);
     46 }
     47 } // namespace ANSI
     48 
     49 // Global terminal state
     50 static struct termios original_termios;
     51 static bool termios_saved = false;
     52 
     53 // Signal handler for cleanup
     54 void cleanup_terminal(int sig) {
     55   if (termios_saved) {
     56     tcsetattr(STDIN_FILENO, TCSANOW, &original_termios);
     57   }
     58   printf("\033[999;1H%s%s", ANSI::SHOW_CURSOR, ANSI::RESET);
     59   fflush(stdout);
     60   if (sig != 0) {
     61     exit(sig);
     62   }
     63 }
     64 
     65 Aquarium::Aquarium() {
     66   // Save original terminal settings
     67   if (tcgetattr(STDIN_FILENO, &original_termios) == 0) {
     68     termios_saved = true;
     69   }
     70 
     71   // Set up signal handlers for cleanup
     72   signal(SIGINT, cleanup_terminal);
     73   signal(SIGTERM, cleanup_terminal);
     74   signal(SIGQUIT, cleanup_terminal);
     75 
     76   // Set terminal to raw mode
     77   struct termios raw = original_termios;
     78   raw.c_lflag &= ~(ECHO | ICANON);
     79   raw.c_iflag &= ~(IXON | ICRNL);
     80   raw.c_oflag &= ~(OPOST);
     81   raw.c_cc[VMIN] = 0;  // Non-blocking read
     82   raw.c_cc[VTIME] = 1; // 100ms timeout
     83 
     84   tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
     85 
     86   // Initialize display
     87   printf("%s%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR,
     88          ANSI::BG_BLACK);
     89   fflush(stdout);
     90 
     91   // Get terminal size
     92   getTerminalSize();
     93 
     94   currentFrame.assign(height, std::vector<Cell>(width));
     95   previousFrame.assign(height, std::vector<Cell>(width));
     96 
     97   if (!colorLookupInitialized) {
     98     initColorLookup();
     99     colorLookupInitialized = true;
    100   }
    101 }
    102 
    103 void Aquarium::getTerminalSize() {
    104   struct winsize ws;
    105   if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
    106     height = ws.ws_row;
    107     width = ws.ws_col;
    108   } else {
    109     cleanup_terminal(0);
    110     std::cerr << "Error: Unable to determine terminal size.\n";
    111     std::exit(1);
    112   }
    113 }
    114 
    115 void Aquarium::ensureEntitiesSorted() {
    116   if (entities_need_sorting) {
    117     std::sort(entities.begin(), entities.end(),
    118               [](const auto &a, const auto &b) {
    119                 int layerA = a->getPreferredLayer();
    120                 int layerB = b->getPreferredLayer();
    121                 if (layerA != layerB)
    122                   return layerA < layerB;
    123                 return a->getId() < b->getId();
    124               });
    125     entities_need_sorting = false;
    126   }
    127 }
    128 
    129 void Aquarium::redraw() {
    130   clearCurrentFrame();
    131   ensureBigEntityExists();
    132 
    133   static std::vector<std::unique_ptr<Entity>> newEntities;
    134   static std::vector<size_t> entitiesToRemove;
    135 
    136   newEntities.clear();
    137   entitiesToRemove.clear();
    138 
    139   // Update all entities and collect changes
    140   for (size_t i = 0; i < entities.size(); ++i) {
    141     auto &entity = entities[i];
    142     entity->update();
    143 
    144     // Handle fish bubble spawning
    145     if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
    146       if (fish->shouldSpawnBubble()) {
    147         newEntities.emplace_back(
    148             std::make_unique<Bubble>(fish->getX(), fish->getY()));
    149       }
    150     }
    151 
    152     if (entity->shouldBeRemoved()) {
    153       auto replacement = entity->createReplacement();
    154       if (replacement) {
    155         entity = std::move(replacement); // Replace in-place
    156         entities_need_sorting = true;
    157       } else {
    158         entitiesToRemove.push_back(i); // Mark for removal
    159       }
    160     }
    161   }
    162 
    163   // Remove entities in reverse order to maintain indices
    164   for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend();
    165        ++it) {
    166     entities.erase(entities.begin() + *it);
    167     entities_need_sorting = true;
    168   }
    169 
    170   // Add new entities if we have them
    171   if (!newEntities.empty()) {
    172     // Reserve space to minimize reallocations
    173     entities.reserve(entities.size() + newEntities.size());
    174 
    175     for (auto &newEntity : newEntities) {
    176       entities.emplace_back(std::move(newEntity));
    177     }
    178     entities_need_sorting = true;
    179   }
    180 
    181   ensureEntitiesSorted();
    182 
    183   // Draw all entities
    184   for (const auto &entity : entities) {
    185     entity->draw();
    186   }
    187 
    188   renderToScreen();
    189 }
    190 
    191 void Aquarium::resize() {
    192   printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK);
    193   fflush(stdout);
    194 
    195   getTerminalSize();
    196 
    197   currentFrame.assign(height, std::vector<Cell>(width));
    198   previousFrame.assign(height, std::vector<Cell>(width));
    199 
    200   entities.clear();
    201   entities_need_sorting = true;
    202 
    203   addWaterline();
    204   addCastle();
    205   for (int i = 0; i < width / 15; i++)
    206     addSeaweed();
    207   for (int i = 0; i < width * (height - 9) / 350; i++)
    208     addFish();
    209 }
    210 
    211 void Aquarium::addFish() { addEntityImpl<Fish>(); }
    212 void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); }
    213 void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); }
    214 void Aquarium::addWaterline() { addEntityImpl<Waterline>(); }
    215 void Aquarium::addCastle() { addEntityImpl<Castle>(); }
    216 void Aquarium::addShip() { addEntityImpl<Ship>(); }
    217 void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); }
    218 void Aquarium::addWhale() { addEntityImpl<Whale>(); }
    219 
    220 void Aquarium::ensureBigEntityExists() {
    221   // Check if any big entities exist on screen
    222   for (const auto &entity : entities) {
    223     if (dynamic_cast<Ship *>(entity.get()) ||
    224         dynamic_cast<SeaMonster *>(entity.get()) ||
    225         dynamic_cast<Whale *>(entity.get())) {
    226       return; // Big entity found, do nothing
    227     }
    228   }
    229 
    230   // No big entity found, spawn next in cycle
    231   int entity_type = big_entity_index % 3;
    232   if (entity_type == 0) {
    233     addEntityImpl<Ship>();
    234   } else if (entity_type == 1) {
    235     addEntityImpl<SeaMonster>();
    236   } else {
    237     addEntityImpl<Whale>();
    238   }
    239   ++big_entity_index;
    240 }
    241 
    242 void Aquarium::clearCurrentFrame() {
    243   for (auto &row : currentFrame) {
    244     std::fill(row.begin(), row.end(), Cell());
    245   }
    246 }
    247 
    248 void Aquarium::drawToFrame(int y, int x, const std::string &line,
    249                            const std::string &colorLine) {
    250   const size_t len = std::min(line.size(), colorLine.size());
    251 
    252   for (size_t j = 0; j < len; ++j) {
    253     int cx = x + static_cast<int>(j);
    254     if (cx < 0 || cx >= width)
    255       continue;
    256 
    257     const char ch = line[j];
    258     const char colorChar = colorLine[j];
    259     const bool isBold = (colorChar >= 'A' && colorChar <= 'Z');
    260 
    261     currentFrame[y][cx] = {
    262         ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
    263   }
    264 }
    265 
    266 void Aquarium::initColorLookup() {
    267   for (int i = 0; i < 256; ++i)
    268     colorLookup[i] = ANSI::BLACK; // Default black
    269 
    270   colorLookup['r'] = ANSI::RED;
    271   colorLookup['g'] = ANSI::GREEN;
    272   colorLookup['y'] = ANSI::YELLOW;
    273   colorLookup['b'] = ANSI::BLUE;
    274   colorLookup['m'] = ANSI::MAGENTA;
    275   colorLookup['c'] = ANSI::CYAN;
    276   colorLookup['w'] = ANSI::WHITE;
    277   colorLookup['k'] = ANSI::BLACK;
    278 }
    279 
    280 void Aquarium::renderToScreen() {
    281   static std::string output;
    282   output.clear();
    283   output.reserve(height * width * 20);
    284 
    285   int cursor_y = -1, cursor_x = -1;
    286 
    287   for (int y = 0; y < height; ++y) {
    288     for (int x = 0; x < width; ++x) {
    289       const Cell &newCell = currentFrame[y][x];
    290       Cell &oldCell = previousFrame[y][x];
    291 
    292       if (newCell == oldCell)
    293         continue;
    294 
    295       oldCell = newCell;
    296 
    297       // Move cursor only when needed
    298       if (cursor_y != y || cursor_x != x) {
    299         output += ANSI::moveTo(y, x);
    300         cursor_y = y;
    301         cursor_x = x;
    302       }
    303 
    304       // Apply cell formatting and character
    305       output += ANSI::RESET_BLACK_BG;
    306       if (newCell.bold)
    307         output += ANSI::BOLD;
    308       output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
    309       output += newCell.ch;
    310 
    311       ++cursor_x;
    312     }
    313   }
    314 
    315   if (!output.empty()) {
    316     std::cout << output << std::flush;
    317   }
    318 }
    319 
    320 // Check for input (non-blocking)
    321 int Aquarium::checkInput() {
    322   char c;
    323   if (read(STDIN_FILENO, &c, 1) == 1) {
    324     return c;
    325   }
    326   return -1; // No input available
    327 }
    328 
    329 // Check if terminal was resized
    330 bool Aquarium::checkResize() {
    331   struct winsize ws;
    332   if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
    333     if (ws.ws_row != height || ws.ws_col != width) {
    334       return true;
    335     }
    336   }
    337   return false;
    338 }
    339 
    340 Aquarium::~Aquarium() { cleanup_terminal(0); }