// A console conway's game of life program by Bruce Hill // Released under the MIT license, see LICENSE for details. #include #include #include #include #include #include #include #include #include #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 ] [-B ] [-A ] [-D ]\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; }