#include #include #include #include #include #include #include #include #include #include #include #include #include #include #define CSI "\x1b[" /* TODO * * - utf-8 input * - arrow keys * - KEY_RESIZE or draw callback * - maybe some custom colors */ typedef enum { FG_BLACK = 0, FG_RED = 1, FG_GREEN = 2, FG_YELLOW = 3, FG_BLUE = 4, FG_MAGENTA = 5, FG_CYAN = 6, FG_WHITE = 7, FG_BBLACK = FG_BLACK + 8, FG_BRED = FG_RED + 8, FG_BGREEN = FG_GREEN + 8, FG_BYELLOW = FG_YELLOW + 8, FG_BBLUE = FG_BLUE + 8, FG_BMAGENTA = FG_MAGENTA + 8, FG_BCYAN = FG_CYAN + 8, FG_BWHITE = FG_WHITE + 8, BG_BLACK = FG_BLACK << 4, BG_RED = FG_RED << 4, BG_GREEN = FG_GREEN << 4, BG_YELLOW = FG_YELLOW << 4, BG_BLUE = FG_BLUE << 4, BG_MAGENTA = FG_MAGENTA << 4, BG_CYAN = FG_CYAN << 4, BG_WHITE = FG_WHITE << 4, BG_BBLACK = FG_BBLACK << 4, BG_BRED = FG_BRED << 4, BG_BGREEN = FG_BGREEN << 4, BG_BYELLOW = FG_BYELLOW << 4, BG_BBLUE = FG_BBLUE << 4, BG_BMAGENTA = FG_BMAGENTA << 4, BG_BCYAN = FG_BCYAN << 4, BG_BWHITE = FG_BWHITE << 4, A_BOLD = 1 << 8, A_DIM = 1 << 9, A_ITALIC = 1 << 10, A_UNDERSCORE = 1 << 11, A_BLINK = 1 << 12, A_REVERSE = 1 << 13, } VuiAttr; #define ATTR_FG(a) ((a) & 0xf) #define ATTR_BG(a) (((a)>>4) & 0xf) #define ATTR_A(a) ((a) & ~0xff) #define ATTR_DEFAULT (FG_WHITE | BG_BLACK) typedef struct { unsigned width, height; char *chr; uint16_t *attr; } VuiBuffer; typedef struct { unsigned width, height; VuiBuffer buf1, buf2; VuiBuffer *front, *back; void (*redraw_fn)(void *ctx); void *redraw_ctx; int redraw_all; int scroll_x, scroll_y; } VuiWindow; static struct termios vui_init_stdin_tos, vui_init_stdout_tos, vui_raw_stdin_tos, vui_raw_stdout_tos; VuiWindow vui_win = { .front = &vui_win.buf1, .back = &vui_win.buf2, }; #define LINES (vui_win.height) #define COLS (vui_win.width) #define BCHR(b,x,y) ((b)->chr[(x) + (y) * (b)->width]) #define BATTR(b,x,y) ((b)->attr[(x) + (y) * (b)->width]) #define CHR(x,y) BCHR(vui_win.front, x, y) #define ATTR(x,y) BATTR(vui_win.front, x, y) void vui_getwinsz(unsigned *w, unsigned *h) { struct winsize wsz; ioctl(STDOUT_FILENO, TIOCGWINSZ, &wsz); *w = wsz.ws_col; *h = wsz.ws_row; } static void vui_clrspan(VuiBuffer *buf, unsigned x0, unsigned x1, unsigned y) { for (unsigned x = x0; x < x1; x++) { BCHR(buf, x, y) = ' '; } for (unsigned x = x0; x < x1; x++) { BATTR(buf, x, y) = ATTR_DEFAULT; } } static void vui_clrtoeol(VuiBuffer *buf, unsigned x0, unsigned y) { vui_clrspan(buf, x0, buf->width, y); } static void clear_buf(VuiBuffer *buf) { for (unsigned y = 0; y < buf->height; y++) { vui_clrtoeol(buf, 0, y); } } static void vui_clear(void) { clear_buf(vui_win.front); } static void resize_buf(VuiBuffer *buf, unsigned nw, unsigned nh) { char *nchr = calloc(nw * nh, sizeof(*buf->chr)); uint16_t *nattr = calloc(nw * nh, sizeof(*buf->attr)); assert(nchr); assert(nattr); unsigned oldw = buf->width; unsigned oldh = buf->height; unsigned minw = nw < oldw ? nw : oldw; unsigned minh = nh < oldh ? nh : oldh; for (unsigned y = 0; y < minh; y++) { memcpy(nchr + y*nw, buf->chr + y*oldw, minw * sizeof(*buf->chr)); memcpy(nattr + y*nw, buf->attr + y*oldw, minw * sizeof(*buf->attr)); } free(buf->chr); free(buf->attr); buf->chr = nchr; buf->attr = nattr; buf->width = nw; buf->height = nh; for (unsigned y = 0; y < minh; y++) vui_clrtoeol(buf, minw, y); for (unsigned y = minh; y < buf->height; y++) vui_clrtoeol(buf, 0, y); } static void vui_resize(unsigned nw, unsigned nh) { resize_buf(&vui_win.buf1, nw, nh); resize_buf(&vui_win.buf2, nw, nh); vui_win.width = nw; vui_win.height = nh; } static void vui_adjust(void) { unsigned w, h; vui_getwinsz(&w, &h); if (w != COLS || h != LINES) { vui_resize(w, h); vui_win.redraw_all = 1; } } void vui_curs_vis(int vis) { if (vis) { fputs(CSI "?25h", stdout); } else { fputs(CSI "?25l", stdout); } } void vui_on_sigwinch(int _) { vui_adjust(); if (vui_win.redraw_fn) { vui_win.redraw_fn(vui_win.redraw_ctx); } } void vui_init(void) { tcgetattr(STDIN_FILENO, &vui_init_stdin_tos); vui_raw_stdin_tos = vui_init_stdin_tos; vui_raw_stdin_tos.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN); tcsetattr(STDIN_FILENO, TCSANOW, &vui_raw_stdin_tos); tcgetattr(STDOUT_FILENO, &vui_init_stdout_tos); vui_raw_stdout_tos = vui_init_stdout_tos; vui_raw_stdout_tos.c_oflag &= ~OPOST; tcsetattr(STDOUT_FILENO, TCSANOW, &vui_raw_stdout_tos); vui_adjust(); signal(SIGWINCH, vui_on_sigwinch); } static void free_buf(VuiBuffer *buf) { free(buf->chr); free(buf->attr); buf->chr = NULL; buf->attr = NULL; buf->width = 0; buf->height = 0; } static inline int bchr_equiv(VuiBuffer *back, VuiBuffer *front, int x, int y) { return BCHR(back,x,y) == BCHR(front,x,y) && (BATTR(back,x,y) == BATTR(front,x,y) /* || BCHR(back,x,y) == ' ' */); } static char *vui_out = NULL; static size_t vui_outn = 0; static size_t vui_out_cap = 0; void vui_fini(void) { tcsetattr(STDIN_FILENO, TCSANOW, &vui_init_stdin_tos); tcsetattr(STDOUT_FILENO, TCSANOW, &vui_init_stdout_tos); vui_curs_vis(1); free_buf(&vui_win.buf1); free_buf(&vui_win.buf2); free(vui_out); vui_out = NULL; vui_out_cap = 0; printf(CSI "H" CSI "2J" CSI "0m"); } void vui_out_fit(size_t n) { size_t c = stdc_bit_ceil(n); if (c > vui_out_cap) { char *p = realloc(vui_out, c); if (!p) { vui_fini(); fprintf(stderr, "failed to reallocate vui output buffer\n"); exit(1); } vui_out = p; vui_out_cap = c; } } void vui_outf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); va_start(ap, fmt); vui_out_fit(vui_outn + n); vsprintf(&vui_out[vui_outn], fmt, ap); vui_outn += n; va_end(ap); } static inline void vui_outc(char c) { vui_out_fit(vui_outn + 1); vui_out[vui_outn++] = c; } static inline void vui_outsn(const char *s, unsigned n) { vui_out_fit(vui_outn + n); memcpy(&vui_out[vui_outn], s, n); vui_outn += n; } static inline void vui_outs(const char *s) { vui_outsn(s, strlen(s)); } static inline void vui_out_flush(void) { fwrite(vui_out, 1, vui_outn, stdout); fflush(stdout); #if 0 static unsigned out_frame = 0; FILE *f = fopen("out_log.txt", "a"); assert(f); fprintf(f, "\n\n:: OUTPUT FRAME %u ::\n", ++out_frame); fwrite(vui_out, 1, vui_outn, f); fclose(f); #endif vui_outn = 0; } static inline void curs_move(int src_x, int src_y, int dst_x, int dst_y) { if (src_x != dst_x && src_y != dst_y) { if (dst_x > 0) { vui_outf(CSI "%d;%dH", dst_y + 1, dst_x + 1); } else if (dst_y > 0) { vui_outf(CSI "%dH", dst_y + 1); } else { vui_outs(CSI "H"); } } else if (src_x != dst_x) { if (dst_x == 0) { vui_outc('\r'); } else if (dst_x == src_x - 1) { vui_outc('\b'); } else { vui_outf(CSI "%dG", dst_x + 1); } } else if (src_y != dst_y) { if (dst_y == src_y - 1) { vui_outs("\x1b" "M"); } else if (dst_y == src_y + 1) { vui_outs("\x1b" "D"); } if (dst_y > 0) { vui_outf(CSI "%dd", dst_y + 1); } else { vui_outs(CSI "d"); } } } static void attr_chg(VuiAttr *ptr, VuiAttr to) { VuiAttr from = *ptr; if (from == to) return; int attr_chg_count = stdc_count_ones(ATTR_A(from) ^ ATTR_A(to)); int chg_attr = !!attr_chg_count; if (!chg_attr) goto chg_colors; /* deduct color changes */ attr_chg_count -= (ATTR_FG(to) == ATTR_FG(from)) && (ATTR_FG(to) != ATTR_DEFAULT); attr_chg_count -= (ATTR_BG(to) == ATTR_BG(from)) && (ATTR_BG(to) != ATTR_DEFAULT); int should_rebuild = (attr_chg_count > 1) || to == ATTR_DEFAULT; if (should_rebuild) { vui_outs(CSI "0"); if (to & A_BOLD) vui_outs(";1"); if (to & A_DIM) vui_outs(";2"); if (to & A_ITALIC) vui_outs(";3"); if (to & A_UNDERSCORE) vui_outs(";4"); if (to & A_BLINK) vui_outs(";5"); if (to & A_REVERSE) vui_outs(";7"); from = ATTR_DEFAULT; assert(ATTR_FG(from) == FG_WHITE); assert(ATTR_BG(from) == BG_BLACK); } else { vui_outs(CSI); if ((to ^ from) & A_BOLD) { if (to & A_BOLD) vui_outs(";1"); else { vui_outs(";22"); from &= (~A_BOLD & ~A_DIM); } } if ((to ^ from) & A_DIM) { if (to & A_BOLD) vui_outs(";2"); else { vui_outs(";22"); from &= (~A_BOLD & ~A_DIM); } } if ((to ^ from) & A_ITALIC) vui_outs((to & A_ITALIC) ? ";3" : ";23"); if ((to ^ from) & A_UNDERSCORE) vui_outs((to & A_UNDERSCORE) ? ";4" : ";24"); if ((to ^ from) & A_BLINK) vui_outs((to & A_BLINK) ? ";5" : ";25"); if ((to ^ from) & A_REVERSE) vui_outs((to & A_REVERSE) ? ";7" : ";27"); } chg_colors: int f_fg = ATTR_FG(from); int f_bg = ATTR_BG(from); int t_fg = ATTR_FG(to); int t_bg = ATTR_BG(to); int chg_fg = (t_fg != f_fg); int chg_bg = (t_bg != f_bg); if (chg_fg || chg_bg) { if (chg_attr) vui_outc(';'); else vui_outs(CSI); if (chg_fg) { vui_outc(t_fg > 7 ? '9' : '3'); vui_outc((t_fg & 7) + '0'); } if (chg_bg) { if (chg_fg) vui_outc(';'); if (t_bg > 7) { vui_outs("10"); } else { vui_outc('4'); } vui_outc((t_bg & 7) + '0'); } } if (chg_fg || chg_bg || chg_attr) vui_outc('m'); *ptr = to; } unsigned vui_changes = 0; unsigned vui_max_change = 0; unsigned vui_out_chars = 0; int scrolled_x = 0, scrolled_y = 0; void vui_scroll_buf(VuiBuffer *b, int dx, int dy); void vui_blit(void) { VuiBuffer *front = vui_win.front; VuiBuffer *back = vui_win.back; vui_outf(CSI "H" CSI "0m"); VuiAttr attr_last = ATTR_DEFAULT; scrolled_x = vui_win.scroll_x; scrolled_y = vui_win.scroll_y; if (vui_win.redraw_all) { vui_changes = COLS * LINES; for (unsigned y = 0; y < LINES; y++) { for (unsigned x = 0; x < COLS; x++) { attr_chg(&attr_last, ATTR(x, y)); vui_outc(CHR(x, y)); } } vui_win.redraw_all = 0; vui_win.scroll_x = 0; vui_win.scroll_y = 0; goto copy_buf; } vui_changes = 0; vui_max_change = 0; unsigned cur_x = 0, cur_y = 0; /* TODO: * * Scroll without repainting whole screen: * * - \x1b[#L to insert blank lines * - \x1b[#@ to insert blank chars * - \x1b[#M to delete lines * - \x1b[#P to delete chars * */ if (vui_win.scroll_x || vui_win.scroll_y) { if (abs(vui_win.scroll_x) >= COLS || abs(vui_win.scroll_y) > LINES) { vui_outs(CSI "2J"); goto scrolled; } if (vui_win.scroll_x < 0) { for (unsigned y = 0; y < LINES; y++) { curs_move(cur_x, cur_y, 0, y); vui_outf(CSI "%dP", -vui_win.scroll_x); cur_y = y; } } else if (vui_win.scroll_x > 0) { for (unsigned y = 0; y < LINES; y++) { curs_move(cur_x, cur_y, 0, y); vui_outf(CSI "%d@", vui_win.scroll_x); cur_y = y; } } if (vui_win.scroll_y < 0) { if (vui_win.scroll_y == -1) vui_outs(CSI "S"); else vui_outf(CSI "%dS", -vui_win.scroll_y); } else if (vui_win.scroll_y > 0) { if (vui_win.scroll_y == 1) vui_outs(CSI "T"); else vui_outf(CSI "%dT", vui_win.scroll_y); } scrolled: vui_scroll_buf(back, vui_win.scroll_x, vui_win.scroll_y); vui_win.scroll_x = 0; vui_win.scroll_y = 0; } vui_win.scroll_x = 0; vui_win.scroll_y = 0; for (unsigned y = 0; y < LINES; y++) { unsigned x = 0; while (x < COLS) { while (x < COLS && bchr_equiv(back, front, x, y)) x++; if (x >= COLS) break; unsigned x0 = x; VuiAttr a = ATTR(x0, y); while (x < COLS && !bchr_equiv(back, front, x, y) && ATTR(x, y) == a) x++; if (x0 != x) { vui_changes++; curs_move(cur_x, cur_y, x0, y); attr_chg(&attr_last, a); vui_outsn(&CHR(x0, y), x - x0); cur_x = x; cur_y = y; if (x - x0 > vui_max_change) vui_max_change = x - x0; } } } copy_buf: vui_out_chars = vui_outn; vui_out_flush(); memcpy(back->chr, front->chr, sizeof(*back->chr) * (back->width * back->height)); memcpy(back->attr, front->attr, sizeof(*back->attr) * (back->width * back->height)); vui_win.front = back; vui_win.back = front; } void vui_chra(int x, int y, char c, VuiAttr a) { if (x >= 0 && x < (int)COLS && y >= 0 && y < (int)LINES) { CHR(x, y) = c; ATTR(x, y) = a; } } void vui_chr(int x, int y, char c) { vui_chra(x, y, c, ATTR_DEFAULT); } int vui_avprintf(int x, int y, VuiAttr a, const char *fmt, va_list ap) { va_list ap2; va_copy(ap2, ap); int n = vsnprintf(NULL, 0, fmt, ap); if (x < 0) x = COLS + x - (n - 1); if (y < 0) y = LINES + y; if (x >= COLS || y >= LINES) return n; if (n > 0) { char buf[n + 1]; vsnprintf(buf, n + 1, fmt, ap2); memcpy(&CHR(x,y), buf, n < COLS - x ? n : COLS - x); for (unsigned x1 = x; x1 < COLS && x1 < x + n; x1++) { ATTR(x1, y) = a; } } return n; } int vui_aprintf(int x, int y, VuiAttr a, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int r = vui_avprintf(x, y, a, fmt, ap); va_end(ap); return r; } int vui_printf(int x, int y, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int r = vui_avprintf(x, y, ATTR_DEFAULT, fmt, ap); va_end(ap); return r; } int wait_for_input(int fd, int ms) { return poll(&(struct pollfd) { .fd = fd, .events = POLLIN }, 1, ms); } unsigned isqrt(unsigned x) { if (x <= 1) return x; unsigned a = x/2, b = (a + x/a) / 2; while (b < a) a = b, b = (a + x/a) / 2; return a; } void vui_scroll_buf(VuiBuffer *b, int dx, int dy) { if (abs(dx) >= b->width || abs(dy) >= b->height) { clear_buf(b); return; } if (dy > 0) { memmove(b->chr + (b->width * dy), b->chr, sizeof(*b->chr) * b->width * (b->height - dy)); memmove(b->attr + (b->width * dy), b->attr, sizeof(*b->attr) * b->width * (b->height - dy)); for (int y = 0; y < dy; y++) vui_clrtoeol(b, 0, y); } else if (dy < 0) { memmove(b->chr, b->chr + (b->width * -dy), sizeof(*b->chr) * b->width * (b->height + dy)); memmove(b->attr, b->attr + (b->width * -dy), sizeof(*b->attr) * b->width * (b->height + dy)); for (int y = b->height - 1; y > b->height + dy - 1; y--) vui_clrtoeol(b, 0, y); } if (dx > 0) { for (unsigned i = 0; i < b->height; i++) { memmove(b->chr + (i * b->width) + dx, b->chr + (i * b->width), sizeof(*b->chr) * (b->width - dx)); memmove(b->attr + (i * b->width) + dx, b->attr + (i * b->width), sizeof(*b->attr) * (b->width - dx)); vui_clrspan(b, 0, dx, i); } } else if (dx < 0) { for (unsigned i = 0; i < b->height; i++) { memmove(b->chr + (i * b->width), b->chr + (i * b->width) - dx, sizeof(*b->chr) * (b->width + dx)); memmove(b->attr + (i * b->width), b->attr + (i * b->width) - dx, sizeof(*b->attr) * (b->width + dx)); vui_clrspan(b, b->width + dx - 1, b->width, i); } } } void vui_scroll(int dx, int dy) { vui_scroll_buf(vui_win.front, dx, dy); vui_win.scroll_x += dx; vui_win.scroll_y += dy; } int x = 0, y = 0; int dx = 0, dy = 0; int camx = 0, camy = 0; int C = '*'; unsigned frame = 0; int half_y = 0; void draw(void *ctx) { (void)ctx; vui_chra(x, y, C, ((x + y) & 0xf) | BG_BBLUE); vui_chra(x-1, y, C, ((x + y) & 0xf) | BG_BLUE | A_UNDERSCORE); vui_chra(x+1, y, C, ((x + y) & 0xf) | BG_BLUE | A_UNDERSCORE); vui_chra(x, y-1, C, ((x + y) & 0xf) | BG_BLUE | A_UNDERSCORE); vui_chra(x, y+1, C, ((x + y) & 0xf) | BG_BLUE | A_UNDERSCORE); vui_printf(-1, -1, "(%u, %u)", COLS, LINES); vui_printf(0, -1, "scrolled (%d, %d)", scrolled_x, scrolled_y); vui_printf(-1, 0, "longest change = %3u, changes = %5u, output = %5u, chars = %5u", vui_max_change, vui_changes, vui_out_chars, COLS * LINES); vui_blit(); } int main(int argc, const char **argv) { vui_init(); vui_curs_vis(0); for (;;) { frame++; x += dx; int ydy = dy; if (half_y) ydy = (frame & 1) ? (dy / 2) : dy - (dy / 2); y += ydy; int left = 32; int right = COLS - (left + 1); int top = left >> half_y; int bottom = LINES - (top + 1); int lcamx = camx, lcamy = camy; if (x < left) { camx += x - left; x = left; } if (x > right) { camx += x - right; x = right; } if (y < top) { camy += y - top; y = top; } if (y > bottom) { camy += y - bottom; y = bottom; } int dcx = camx - lcamx, dcy = camy - lcamy; vui_scroll(-dcx, -dcy); /* if (x < 0) x += win.width; if (x > win.width) x -= win.width; if (y < 0) y += win.height; if (y > win.height) y -= win.height; */ static int paused = 0; vui_win.redraw_fn = draw; #if 1 /* weird cellular automata */ if (!paused) { int vsteps = (COLS * LINES) / 2048; if (!vsteps) vsteps++; vsteps = 1; int y0 = LINES * (frame % vsteps) / vsteps; int y1 = y0 + LINES / vsteps; if (y1 > LINES) y1 = LINES; for (int y = y0; y < y1; y++) { for (int x = 0; x < COLS; x++) { if (frame & 1) { int txa = x, tya = y, txc = x, tyc = y; txa += (random() % 3) - 1; tya += (random() % 3) - 1; txc += (random() % 3) - 1; tyc += (random() % 3) - 1; if (txa >= 0 && txa < COLS && tya >= 0 && tya < LINES) { ATTR(txa, tya) = (BATTR(vui_win.back,x,y) & ~0xf) | (ATTR(txa,tya) & 0xf); if (!(random() & 127)) ATTR(x,y) = ATTR_DEFAULT; } if (txc >= 0 && txc < COLS && tyc >= 0 && tyc < LINES) { CHR(txc, tyc) = BCHR(vui_win.back, x, y); ATTR(txc, tyc) = (ATTR(txc,tyc) & ~0xf) | (BATTR(vui_win.back,x,y) & 0xf); if (!(random() & 127)) CHR(x,y) = ' '; } } } } for (unsigned i = 0; i < isqrt(COLS * LINES) / 10; i++) { int tx = random() % COLS; int ty = random() % LINES; VuiAttr a = ((random()&15) << 4) | (random()&15); vui_chra(tx, ty, ' ' + (random() % 0x5f), a); } } #else vui_clear(); for (unsigned y = 0; y < LINES; y++) { for (unsigned x = 0; x < COLS; x++) { int tx = x + camx, ty = y + camy; char ch = " ',."[(tx^ty)&3]; ch = ' '; VuiAttr a = (((tx>>2)^(ty>>2)) % 3 == 0) ? BG_WHITE : BG_RED; vui_chra(x, y, ch, a); } } #endif draw(NULL); clock_nanosleep(CLOCK_MONOTONIC, 0, &(struct timespec) { .tv_nsec = 15 * 1000000 }, NULL); if (!wait_for_input(STDIN_FILENO, 0)) continue; int c = getchar(); if (c == 'q') break; if (c > 0x20 && c < 0x7f) C = c; switch (c) { /* case 'j': dx=-1; dy=0; break; case 'i': dx=0; dy=-1; break; case 'k': dx=0; dy=1; break; case 'l': dx=1; dy=0; break; */ case 'j': dx--; break; case 'l': dx++; break; case 'i': dy--; break; case 'k': dy++; break; case ' ': paused = !paused; break; } } vui_fini(); printf(CSI "2J" CSI "H %u, %u\n", COLS, LINES); return 0; }