Download
211 lines 7.2 KiBCModified 2025-02-28 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
// A console conway's game of life program by Bruce Hill
// Released under the MIT license, see LICENSE for details.
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/random.h>
#include <termios.h>
#include <unistd.h>

#define CELL(cells, x, y) cells[(((y)+H) % H)*W + (((x)+W) % W)]

static char rules[2][9] = {{0,0,0,1,0,0,0,0,0}, {0,0,1,1,0,0,0,0,0}};
static unsigned int W = 0, H = 0;
static char alive_color[64] = "\033[47m", dead_color[64] = "\033[0m";
int ttyin_fd = -1;

static void update(FILE *out, const char *cells, char *future_cells)
{
    const char *color = NULL;
    for (int y = 0; y < H; y++) {
        fprintf(out, "\033[%d;1H", y+1);
        for (int x = 0; x < W; x++) {
            int neighbors =
                CELL(cells,x-1,y-1) + CELL(cells,x,y-1) + CELL(cells,x+1,y-1)
                + CELL(cells,x-1,y) + CELL(cells,x+1,y)
                + CELL(cells,x-1,y+1) + CELL(cells,x,y+1) + CELL(cells,x+1,y+1);
            CELL(future_cells, x, y) = rules[CELL(cells, x, y)][neighbors];
            if (CELL(future_cells, x, y) && color != alive_color) {
                fputs(alive_color, out);
                color = alive_color;
            } else if (!CELL(future_cells, x, y) && color != dead_color) {
                fputs(dead_color, out);
                color = dead_color;
            }
            fputs("  ", out);
        }
    }
    fputs("\033[0m", out);
    fflush(out);
}

static void update_term_size(int sig)
{
    (void)sig;
    struct winsize sz = {0};
    ioctl(ttyin_fd, TIOCGWINSZ, &sz);
    W = sz.ws_col / 2;
    H = sz.ws_row;
}

static int gethexrgb(const char *str, int *r, int *g, int *b)
{
    if (str[0] == '#') ++str;
    if (strlen(str) == 3) {
        if (sscanf(str, "%01X%01X%01X", r, g, b) != 3)
            return 0;
        *r *= 16; *g *= 16; *b *= 16;
        return 3;
    } else {
        return sscanf(str, "%02X%02X%02X", r, g, b);
    }
}

int main(int argc, char **argv) {
    int ret = 0;
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
          usage:
            fprintf(stderr, "Conway's Game of Life Viewer\n"
                   "Usage: conway [-S <survival rules>] [-B <birth rules>] [-A <alive RGB hex>] [-D <dead RGB hex>]\n");
            return ret;
        } else if (strcmp(argv[i], "-B") == 0) {
            if (i + 1 >= argc) goto usage;
            memset(rules[0], 0, sizeof(rules[0]));
            for (char *p = argv[++i]; *p; p++)
                if ('0' <= *p && *p <= '8')
                    rules[0][*p - '0'] = 1;
        } else if (strcmp(argv[i], "-S") == 0) {
            if (i + 1 >= argc) goto usage;
            memset(rules[1], 0, sizeof(rules[1]));
            for (char *p = argv[++i]; *p; p++)
                if ('0' <= *p && *p <= '8')
                    rules[1][*p - '0'] = 1;
        } else if (strcmp(argv[i], "-A") == 0) {
            if (i + 1 >= argc) goto usage;
            int r,g,b;
            if (gethexrgb(argv[++i], &r, &g, &b) != 3) goto usage;
            sprintf(alive_color, "\033[48;2;%d;%d;%dm", r,g,b);
        } else if (strcmp(argv[i], "-D") == 0) {
            int r,g,b;
            if (gethexrgb(argv[++i], &r, &g, &b) != 3) goto usage;
            sprintf(dead_color, "\033[48;2;%d;%d;%dm", r,g,b);
        } else {
            goto usage;
        }
    }

    ttyin_fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
    if (ttyin_fd < 0) {
        fprintf(stderr, "Couldn't open terminal input.\n");
        return 1;
    }
    FILE *tty_out = fopen("/dev/tty", "w");
    if (!tty_out) {
        fprintf(stderr, "Couldn't open terminal output.\n");
        return 1;
    }

    struct termios orig_termios, termios;
    tcgetattr(fileno(tty_out), &orig_termios);
    memcpy(&termios, &orig_termios, sizeof(termios));
    cfmakeraw(&termios);
    termios.c_cc[VMIN] = 10;
    termios.c_cc[VTIME] = 1;
    if (tcsetattr(fileno(tty_out), TCSAFLUSH, &termios) == -1)
        return 1;

    struct winsize wsize = {0};
    fputs("\033[?25l" // Hide cursor
          "\033[?1049;" // Use alternate screen
          "1000;" // Enable mouse x/y on press
          "1002;" // Enable mouse dragging
          "1006"  // Use mouse SGR mode
          "h", tty_out);

    update_term_size(0);
    signal(SIGWINCH, update_term_size);

    char *buffers[2];
    int flipflop = 0, prev_W = W, prev_H = H;
  resize:
    // TODO: handle resize in SIGWINCH handler
    // Cells are 2 characters wide so they look square
    buffers[0] = calloc(W*H, sizeof(char));
    buffers[1] = calloc(W*H, sizeof(char));
    for (int x = 0; x < W; x++)
        for (int y = 0; y < H; y++)
            CELL(buffers[flipflop], x, y) = random() % 2;

    int draw_mode = 1, paused = 0;
    fputs("\033[2J", tty_out);
    while (1) {
        if (prev_W != W || prev_H != H) {
            prev_W = W, prev_H = H;
            free(buffers[0]);
            free(buffers[1]);
            buffers[0] = calloc(W*H, sizeof(char));
            buffers[1] = calloc(W*H, sizeof(char));
            goto resize;
        }
        if (!paused) {
            update(tty_out, buffers[flipflop], buffers[!flipflop]);
            flipflop = !flipflop;
        }
      present:
        usleep(60000);
        int drew = 0;
        char key;
        while (read(ttyin_fd, &key, 1) == 1) {
            switch (key) {
            case '\3': ret = 1; goto done; // CTRL-C
            case 'q': goto done;
            case 'p': paused ^= 1; break;
            case 'c': memset(buffers[flipflop], 0, W*H); break;
            case 'r':
                getrandom(buffers[flipflop], W*H, 0);
                for (int i = 0; i < W*H; i++)
                    buffers[flipflop][i] &= 0x1;
                break;
            case ' ': draw_mode ^= 1; break;
            case '<': { // Mouse dragging/clicks
                int buttons = 0, x = 0, y = 0;
                char buf;
                while (read(ttyin_fd, &buf, 1) == 1 && '0' <= buf && buf <= '9')
                    buttons = buttons * 10 + (buf - '0');
                if (buf != ';') return -1;
                while (read(ttyin_fd, &buf, 1) == 1 && '0' <= buf && buf <= '9')
                    x = x * 10 + (buf - '0');
                if (buf != ';') return -1;
                while (read(ttyin_fd, &buf, 1) == 1 && '0' <= buf && buf <= '9')
                    y = y * 10 + (buf - '0');
                if (buf != 'm' && buf != 'M') return -1;
                if ((buttons & (~32)) == 0 && ((x-1)/2) < W) {
                    CELL(buffers[flipflop], (x-1)/2, y-1) = draw_mode;
                    fprintf(tty_out, "\033[%d;%dH%s  \033[0m",
                            y, 2*((x-1)/2)+1,
                            draw_mode ? alive_color : dead_color);
                    fflush(tty_out);
                    drew = 1;
                }
                break;
            }
            }
        }
        if (drew) goto present;
    }
  done:
    tcsetattr(fileno(tty_out), TCSAFLUSH, &orig_termios);
    fputs("\033[?25h" // Show cursor
          "\033[?1049;" // Back to normal screen
          "1000;" // Disable mouse x/y reporting
          "1002;" // Disable mouse dragging
          "1006" // Disable mouse SGR mode
          "l", tty_out);
    fclose(tty_out);
    close(ttyin_fd);
    return ret;
}