From 9f4310c24ca39284ad768a82d368e749b18fd76c Mon Sep 17 00:00:00 2001 From: WormHeamer Date: Sun, 28 Dec 2025 02:45:38 -0500 Subject: put more emphasis on TxtLoc --- main.c | 163 ++++++++++++- txt.c | 92 +++++++- txt.h | 20 +- utf8.h | 89 +++++++ vui.c | 836 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ vui.h | 144 ++++++++++++ 6 files changed, 1318 insertions(+), 26 deletions(-) create mode 100644 utf8.h create mode 100644 vui.c create mode 100644 vui.h diff --git a/main.c b/main.c index e367825..3862625 100644 --- a/main.c +++ b/main.c @@ -1,27 +1,174 @@ #include #include #include +#include +#include #include #include #include #define ARENA_IMPL +#define UTF8_IMPL #include "wrmr.h" #include "arena.h" #include "txt.h" +#include "vui.h" +#include "utf8.h" -int main(void) { - Arena a = arena_init(1L << 20); - Txt txt = { 0 }; - txt_open(&txt, "test.txt"); - txt_insert(&txt, txt.len, "wheeee", 6); - txt_delete(&txt, txt.len, 6); +Arena scratch = { 0 }; +Txt txt = { 0 }; +u32 cur = 0; + +int is_space(u32 c) { + return c <= 0x20 && c != 0; +} + +u32 move_word_back(Txt *t, u32 cur) { + TxtLoc l = txt_at(t, cur); + while (!txt_at_start(t,l)) { + TxtLoc n = txt_prev(t, l); + if (!is_space(txt_chr(t, n))) break; + l = n; + } + while (!txt_at_start(t,l)) { + TxtLoc n = txt_prev(t, l); + if (is_space(txt_chr(t, n))) break; + l = n; + } + return txt_ofs(t, l); +} + +u32 move_word_fwd(Txt *t, u32 cur) { + TxtLoc l = txt_at(t, cur); + while (!txt_at_end(t,l) && is_space(txt_chr(t, l))) l = txt_next(t, l); + while (!txt_at_end(t,l) && !is_space(txt_chr(t, l))) l = txt_next(t, l); + return txt_ofs(t, l); +} + +u32 move_char_back(Txt *t, u32 cur) { + while (cur > 0) { + cur--; + u8 c = txt_byte(t, txt_at(t, cur)); + if ((c & 0xc0) != 0x80) break; + } + return cur; +} + +u32 move_char_fwd(Txt *t, u32 cur) { + u8 b = txt_byte(t, txt_at(t, cur)); + u32 n = stdc_leading_ones(b); + if (cur + n >= t->len) return t->len; + return cur + n + !n; +} + +u32 del_between(Txt *t, u32 a, u32 b) { + if (b < a) { u32 t = a; a = b; b = t; } + //if (b - a > 10) TRAP(); + return txt_delete(t, b, b - a); +} + +/* main */ + +#define ODD_ATTR (FG_CYAN | BG_BLACK) +#define EVEN_ATTR (FG_WHITE | BG_BLACK) + +void draw(void *ctx) { + (void)ctx; +redraw:; + vui_clear(); + static int max_y = 0; + int x = 0, y = 0; + u32 ofs = 0; for (u32 i = 0; i < txt.ptbl.n; i++) { + Arena stk = scratch; TxtPiece *p = &txt.ptbl.v[i]; - printf("%u. %.*s\n", i, (int)p->n, txt.buf[p->buf].s + p->ofs); + const char *s = txt.buf[p->buf].s + p->ofs; + + u32 bufn = utf8_decode_len(s, p->n); + u32 *buf = new_arr(&scratch, u32, bufn); + utf8_decode(buf, s, bufn); + + VuiAttr a = i&1 ? ODD_ATTR : EVEN_ATTR; + vui_aprintf(-1, i, a, "%u, %u (%s)", p->ofs, p->n, p->buf == TXT_ADD ? "add" : "src"); + + for (u32 j = 0; j < bufn; j++) { + u32 c = buf[j]; + if (ofs == cur) vui_curs_pos(x, y - (max_y - LINES + 1)); + if (c != '\r' && c != '\n') { + vui_chra(x++, y - (max_y - LINES + 1), c, a); + } + if (c == '\n') { + x = 0; + y++; + } + ofs += UTF8_CP_LEN(c); + } + scratch = stk; + } + if (ofs == cur) vui_curs_pos(x, y - (max_y - LINES + 1)); + if (y > max_y) { + max_y = y; + goto redraw; + } + TxtLoc l = txt_at(&txt, cur); + u32 c = txt_chr(&txt, l); + vui_printf(-1, -1, "%u, %u - %02x (%c)", l.p, l.i, c, (c < 0x20 || c > 0x7e) ? ' ' : c); + u32 used = scratch.beg - scratch.start, max = scratch.end - scratch.start; + vui_printf(-1, -2, "scratch %.02f/%.02fk", used/1024.0, max/1024.0); +} + +int main(void) { + scratch = arena_init(1L << 20); + vui_init(); + vui_curs_vis(1); + vui_redraw_fn(draw); + if (txt_load(&txt, "test.txt")) err(1, "couldn't open file"); + cur = txt.len; + for (;;) { + scratch.beg = scratch.start; + draw(NULL); + vui_blit(); + u32 c = vui_key(); + switch (c) { + case KEY_ESC: + goto brk; + case KEY_BKSP: + cur = txt_delete_c(&txt, cur); + break; + case KEY_LEFT: + cur = move_char_back(&txt, cur); + break; + case KEY_LEFT | KEY_CTRL_BIT: + cur = move_word_back(&txt, cur); + break; + case KEY_RIGHT: + cur = move_char_fwd(&txt, cur); + break; + case KEY_RIGHT | KEY_CTRL_BIT: + cur = move_word_fwd(&txt, cur); + break; + case 0x17 /* ^W */: + cur = del_between(&txt, move_word_back(&txt, cur), cur); + break; + case 0x05 /* ^E */: + cur = del_between(&txt, cur, move_word_fwd(&txt, cur)); + break; + case 0x13 /* ^S */: + txt_save(&txt, "test.txt"); + txt_load(&txt, "test.txt"); + break; + case '\r': + cur = txt_insert_c(&txt, cur, '\n'); + break; + default: + if (c >= ' ' && c <= KEY_UTF8_MAX) cur = txt_insert_c(&txt, cur, c); + break; + } } - arena_free(&a); +brk: + vui_fini(); + arena_free(&scratch); return 0; } diff --git a/txt.c b/txt.c index 9f29f84..8fcafe9 100644 --- a/txt.c +++ b/txt.c @@ -9,6 +9,7 @@ #include "wrmr.h" #include "dynarr.h" #include "txt.h" +#include "utf8.h" void txt_replace_piece(Txt *b, u32 pi, TxtBufIdx buf, u32 ofs, u32 n) { b->ptbl.v[pi] = (TxtPiece) { buf, ofs, n }; @@ -39,16 +40,6 @@ u32 txt_split_piece(Txt *b, u32 pi, u32 i) { return pi + 1; } -TxtLoc txt_at(Txt *b, u32 cur) { - for (u32 i = 0; i < b->ptbl.n; i++) { - if (cur < b->ptbl.v[i].n) { - return (TxtLoc) { i, cur }; - } - cur -= b->ptbl.v[i].n; - } - return (TxtLoc) { b->ptbl.n - 1, b->ptbl.v[b->ptbl.n - 1].n }; -} - static void txt_buf_fit(Txt *b, TxtBufIdx bi, u32 sz) { TxtBuf *buf = &b->buf[bi]; if (sz > buf->c) { @@ -98,9 +89,11 @@ u32 txt_insert(Txt *b, u32 cur, const char *s, u32 n) { return cur + n; } -u32 txt_insert_c(Txt *b, u32 cur, char ch) { - /* TODO: utf-8 char */ - return txt_insert(b, cur, &ch, 1); +u32 txt_insert_c(Txt *b, u32 cur, u32 ch) { + char buf[6]; + u32 n = utf8_encode_len(&ch, 1); + utf8_encode(buf, &ch, 1); + return txt_insert(b, cur, buf, n); } static int txt_are_pieces_adjacent(Txt *t, u32 a, u32 b) { @@ -144,6 +137,13 @@ u32 txt_delete(Txt *b, u32 cur, u32 n) { return cur; } +u32 txt_delete_c(Txt *b, u32 cur) { + while (cur > 0 && (txt_byte(b, txt_at(b, cur-1)) & 0xc0) == 0x80) + cur = txt_delete(b, cur, 1); + cur = txt_delete(b, cur, 1); + return cur; +} + int txt_load(Txt *b, const char *path) { struct stat sb; int fd = open(path, O_RDONLY); @@ -192,3 +192,69 @@ void txt_free(Txt *t) { } free(t->ptbl.v); } + +/* reading individual chars */ + +u8 txt_byte(Txt *t, TxtLoc l) { + TxtPiece *p = &t->ptbl.v[l.p]; + TxtBuf *b = &t->buf[p->buf]; + if (l.p < t->ptbl.n && l.i == p->n) p++, l.i = 0; + if (p->ofs + l.i >= b->n) return 0; + return b->s[p->ofs + l.i]; +} + +u32 txt_chr(Txt *t, TxtLoc l) { + TxtPiece *p = &t->ptbl.v[l.p]; + TxtBuf *b = &t->buf[p->buf]; + if (l.p < t->ptbl.n && l.i == p->n) p++, l.i = 0; + return utf8_decode_at(b->s, p->ofs + l.i, b->n); +} + +/* cursor manipulation */ + +TxtLoc txt_at(Txt *b, u32 cur) { + for (u32 i = 0; i < b->ptbl.n; i++) { + if (cur < b->ptbl.v[i].n) { + return (TxtLoc) { i, cur }; + } + cur -= b->ptbl.v[i].n; + } + return (TxtLoc) { b->ptbl.n - 1, b->ptbl.v[b->ptbl.n - 1].n }; +} + +u32 txt_ofs(Txt *b, TxtLoc l) { + u32 r = 0; + for (u32 i = 0; i < l.p; i++) { + r += b->ptbl.v[i].n; + } + return r + l.i; +} + +TxtLoc txt_next(Txt *b, TxtLoc l) { + TxtPiece *p = &b->ptbl.v[l.p]; + if (l.i+1 < p->n) return (TxtLoc) { l.p, l.i + 1 }; + if (l.p+1 < b->ptbl.n) { + return (TxtLoc) { l.p + 1, l.i }; + } else { + return (TxtLoc) { l.p, p->n }; + } +} + +TxtLoc txt_prev(Txt *b, TxtLoc l) { + if (l.i > 0) { + return (TxtLoc) { l.p, l.i - 1 }; + } else if (l.p > 0) { + return (TxtLoc) { l.p - 1, b->ptbl.v[l.p - 1].n - 1 }; + } else { + return (TxtLoc) { 0, 0 }; + } +} + +int txt_at_start(Txt *b, TxtLoc l) { + (void)b; + return l.p == 0 && l.i == 0; +} + +int txt_at_end(Txt *b, TxtLoc l) { + return l.p + 1 == b->ptbl.n && l.i == b->ptbl.v[l.p].n; +} diff --git a/txt.h b/txt.h index 21712af..54b2146 100644 --- a/txt.h +++ b/txt.h @@ -28,16 +28,26 @@ typedef struct { u32 len; } Txt; -TxtLoc txt_at(Txt *b, u32 cur); - u32 txt_split_piece(Txt *b, u32 pi, u32 i); void txt_remove_piece(Txt *b, u32 pi); void txt_insert_piece(Txt *b, u32 pi, TxtBufIdx buf, u32 ofs, u32 n); -void txt_buf_append(Txt *b, TxtBufIdx bi, const char *s, u32 n); u32 txt_insert(Txt *b, u32 cur, const char *s, u32 n); -u32 txt_insert_c(Txt *b, u32 cur, char ch); u32 txt_delete(Txt *b, u32 cur, u32 n); -int txt_open(Txt *b, const char *path); +u32 txt_insert_c(Txt *b, u32 cur, u32 ch); +u32 txt_delete_c(Txt *b, u32 cur); +int txt_load(Txt *b, const char *path); +int txt_save(Txt *b, const char *path); +void txt_free(Txt *b); + +TxtLoc txt_at(Txt *b, u32 cur); +u32 txt_ofs(Txt *b, TxtLoc l); +TxtLoc txt_next(Txt *b, TxtLoc l); +TxtLoc txt_prev(Txt *b, TxtLoc l); +int txt_at_start(Txt *b, TxtLoc l); +int txt_at_end(Txt *b, TxtLoc l); + +u32 txt_chr(Txt *b, TxtLoc l); +u8 txt_byte(Txt *b, TxtLoc l); #endif diff --git a/utf8.h b/utf8.h new file mode 100644 index 0000000..01c3336 --- /dev/null +++ b/utf8.h @@ -0,0 +1,89 @@ +/* utf-8 encoding and decoding library */ + +#ifndef UTF8_H +#define UTF8_H + +#include "wrmr.h" + +#define UTF8_INVALID 0xFFFD /* replacement character */ + +u32 utf8_decode_len(const char *src, u32 ch_count); +u32 utf8_encode_len(const u32 *src, u32 cp_count); +void utf8_decode(u32 *dst, const char *src, u32 cp_count); +void utf8_encode(char *dst, const u32 *src, u32 cp_count); +u32 utf8_decode_at(const char *s, u32 i, u32 n); + +#ifdef UTF8_IMPL + +#include + +/* packed array of 2-bit lengths for codepoints 0..10FFFF */ +#define UTF8_CP_LEN_BITS ((uint64_t)0xFFEAA550000) +#define UTF8_CP_SHIFT(cp) ((32 - stdc_leading_zeros((uint32_t)(cp))) << 1) +#define UTF8_CP_LEN(cp) (1 + ((UTF8_CP_LEN_BITS >> UTF8_CP_SHIFT(cp)) & 3)) + +u32 utf8_encode_len(const u32 *src, u32 cp_count) { + u32 len = 0; + while (cp_count) len += UTF8_CP_LEN(src[--cp_count]); + return len; +} + +u32 utf8_decode_len(const char *src, u32 ch_count) { + u32 i = 0, len = 0; + while (i < ch_count) { + i += stdc_leading_ones((u8)src[i]) + ((~src[i] & 0x80) >> 7); + len++; + } + return len; +} + +void utf8_encode(char *dst, const u32 *src, u32 cp_count) { + while (cp_count--) { + u32 c = *src++; + ASSUME(c > 0 && c < 0x110000); + u32 len = UTF8_CP_LEN(c); + ASSUME(len > 0 && len < 5); + if (len > 1) { + for (u32 i = len; --i;) { + dst[i] = 0x80 | (c & 0x3f); + c >>= 6; + } + *dst = (0xf0 << (4 - len)) | c; + dst += len; + } else { + *dst++ = c; + } + } +} + +void utf8_decode(u32 *dst, const char *src, u32 cp_count) { + while (cp_count--) { + u8 c = *src++; + u32 bits = stdc_leading_ones(c); + ASSUME(bits < 5); + u32 cp = c & (0xff >> bits); + while (bits-- > 1) { + c = *src++; + cp = (cp << 6) | (c & 0x3F); + } + *dst++ = cp; + } +} + +u32 utf8_decode_at(const char *s, u32 i, u32 n) { + if (i >= n) return 0; + u32 cp = (u8)s[i++]; + u32 b = stdc_leading_ones((u8)cp); + if (!b) return cp; + u32 end = i + b - 1; + if (end >= n) return 0; + cp &= 0xff >> b; + while (i < end) { + u8 c = s[i++]; + cp = (cp << 6) | (c & 0x3f); + } + return cp; +} + +#endif +#endif diff --git a/vui.c b/vui.c new file mode 100644 index 0000000..cc8808c --- /dev/null +++ b/vui.c @@ -0,0 +1,836 @@ +#define _POSIX_C_SOURCE 20250918 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "wrmr.h" +#include "vui.h" +#include "utf8.h" + +#define CSI "\x1b[" + +#define VUI_LOG_OUTPUT 0 +#define VUI_LOG_OUTPUT_FILE "out_log.txt" + +/* terminal control */ + +static void vui_getwinsz(unsigned *w, unsigned *h); +static void vui_clrspan(VuiBuffer *buf, unsigned x0, unsigned x1, unsigned y); +static void vui_clrtoeol(VuiBuffer *buf, unsigned x0, unsigned y); + +static void attr_chg(VuiAttr *ptr, VuiAttr to); +static inline void curs_move(unsigned *restrict ptr_x, unsigned *restrict ptr_y, unsigned dst_x, unsigned dst_y); + +static void on_resized(int _); +static void adjust_win_size(void); +static void resize_win(unsigned nw, unsigned nh); + +/* buffer handling */ + +static void clear_buf(VuiBuffer *buf); +static void resize_buf(VuiBuffer *buf, unsigned nw, unsigned nh); +static void free_buf(VuiBuffer *buf); + +static inline int bchr_equiv(VuiBuffer *a, VuiBuffer *b, unsigned i); + +/* input */ + +static unsigned getk(void); +static unsigned getk_utf8(uint8_t start); +static VuiKey bad_key(void); +static VuiKey meta_key(u32 c); +static VuiKey esc_key(u32 c); + +/* output */ + +static void vui_out_fit(size_t n); +static void vui_outf(const char *fmt, ...); +static inline void vui_outc(char c); +static inline void vui_outvc(VuiChar c); +static inline void vui_outvcn(VuiChar *c, size_t n); +static inline void vui_outsn(const char *s, unsigned n); +static inline void vui_outs(const char *s); +static inline void vui_out_flush(void); + +/* globals */ + +static struct termios vui_init_stdin_tos, vui_init_stdout_tos, vui_raw_stdin_tos, vui_raw_stdout_tos; + +static char *vui_out = NULL; +static size_t vui_outn = 0; +static size_t vui_out_cap = 0; + +static VuiWindow win = { + .front = &win.buf1, + .back = &win.buf2, +}; + +/* TODO + * + * - clean up keyboard input + * - separate out library interface + * - maybe some custom colors + */ + +void vui_redraw_fn(void (*fn)(void *ctx)) { + win.redraw_fn = fn; +} + +void vui_redraw_ctx(void *ctx) { + win.redraw_ctx = ctx; +} + +VuiWindow *vui_win(void) { + return &win; +} + +static 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); + } +} + +void vui_clear(void) { + clear_buf(win.front); +} + +void vui_fill_rect(VuiChar c, VuiAttr a, int x0, int y0, int width, int height) { + if (x0 < 0) { width += x0; x0 = 0; } + if (y0 < 0) { height += y0; y0 = 0; } + if (x0 + width >= (int)COLS) width = (COLS - 1) - x0; + if (y0 + height >= (int)LINES) height = (LINES - 1) - y0; + if (width < 1 || height < 1) return; + for (int y = y0; y < y0 + height; y++) { + for (int x = x0; x < x0 + width; x++) { + CHR(x, y) = c; + ATTR(x, y) = a; + } + } +} + +void vui_fill(VuiChar c, VuiAttr a) { + vui_fill_rect(c, a, 0, 0, COLS, LINES); +} + +static void resize_buf(VuiBuffer *buf, unsigned nw, unsigned nh) { + VuiChar *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 resize_win(unsigned nw, unsigned nh) { + resize_buf(&win.buf1, nw, nh); + resize_buf(&win.buf2, nw, nh); + win.width = nw; + win.height = nh; +} + +static void adjust_win_size(void) { + unsigned w, h; + vui_getwinsz(&w, &h); + if (w != COLS || h != LINES) { + resize_win(w, h); + win.redraw_all = 1; + } +} + +void vui_curs_vis(int vis) { + win.curs_vis = vis; + if (vis) { + fputs(CSI "?25h", stdout); + fflush(stdout); + } else { + fputs(CSI "?25l", stdout); + fflush(stdout); + } +} + +void vui_curs_pos(int x, int y) { + win.curs_x = x; + win.curs_y = y; +} + +/* TODO: use something better than signal() */ +/* does sigaction allow for a context pointer? */ +static void on_resized(int signo) { + (void)signo; + adjust_win_size(); + if (win.redraw_fn) { + win.redraw_fn(win.redraw_ctx); + } + vui_blit(); +} + +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 | ISIG); + vui_raw_stdin_tos.c_lflag |= IGNBRK; + */ + + vui_raw_stdin_tos.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + //vui_raw_stdin_tos.c_oflag &= ~OPOST; + vui_raw_stdin_tos.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + vui_raw_stdin_tos.c_cflag &= ~(CSIZE | PARENB); + vui_raw_stdin_tos.c_cflag |= CS8; + + 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); + + /* set white:black to default */ + printf(CSI "40;37m" CSI "8]"); + vui_curs_vis(0); + win.redraw_all = 1; + + adjust_win_size(); + sigaction(SIGWINCH, &(struct sigaction) { .sa_handler = on_resized }, NULL); +} + + +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 *a, VuiBuffer *b, unsigned i) { + return a->chr[i] == b->chr[i] && a->attr[i] == b->attr[i]; +} + +void vui_fini(void) { + vui_curs_vis(1); + free_buf(&win.buf1); + free_buf(&win.buf2); + free(vui_out); + vui_out = NULL; + vui_out_cap = 0; + printf(CSI "0m" CSI "H" CSI "2J"); + tcsetattr(STDIN_FILENO, TCSANOW, &vui_init_stdin_tos); + tcsetattr(STDOUT_FILENO, TCSANOW, &vui_init_stdout_tos); +} + +static 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; + } +} + +static 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); +} + + +/* lookup table from utf-8 codepoint to byte length: + * { 6,6,6,6,6,6,5,5,5,5,5,4,4,4,4,4,3,3,3,3,3,2,2,2,2,1,1,1,1,1,1,1,1, } + * len(cp) = tbl[32 - clz(cp)]. + * + * how could we possibly reduce this? + * 6 sixes, 5 fives, 5 fours, 5 threes, four twos, and eight ones. + * no obvious pattern. + * + * no utf8 elements above 0x10FFFF, so never more than 4 bytes: + * 4444433333222211111111 + * + * subtract one from each, so they fit in 3 bytes: + * 3333322222111100000000 + * + * pack into a u64: + * 0b11111111111010101010010101010000000000000000 + * + * convert to hex, and now: + * len(cp) = 1 + (0xFFEAA550000 >> (2 * (32 - clz(cp)))) & 3 + */ + +static inline void vui_outc(char c) { + vui_out_fit(vui_outn + 1); + vui_out[vui_outn++] = c; +} + +/* don't pass a NUL to this */ +/* it doesn't make sense to do so, and assuming non-zero lets us dodge a branch + * in stdc_leading_zeros() */ +static inline void vui_outvc(VuiChar c) { + vui_outvcn(&c, 1); +} + +static inline void vui_outvcn(VuiChar *c, size_t n) { + u32 len = utf8_encode_len(c, n); + vui_out_fit(vui_outn + len); + utf8_encode(&vui_out[vui_outn], c, n); + vui_outn += len; +} + +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);*/ + + write(STDOUT_FILENO, vui_out, vui_outn); + +#if VUI_LOG_OUTPUT + static unsigned out_frame = 0; + FILE *f = fopen(VUI_LOG_OUTPUT_FILE, "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(unsigned *restrict ptr_x, unsigned *restrict ptr_y, unsigned dst_x, unsigned dst_y) { + unsigned src_x = *ptr_x; + unsigned src_y = *ptr_y; + if (src_x != dst_x && src_y != dst_y) { + if (dst_x == src_x - 1) { + vui_outf(CSI "\b%ud", dst_y + 1); + } else if (dst_x > 0) { + vui_outf(CSI "%u;%uH", dst_y + 1, dst_x + 1); + } else if (dst_y > 0) { + vui_outf(CSI "%uH", 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 "%uG", dst_x + 1); + } + } else if (src_y != dst_y) { + if (dst_y == src_y - 1 && dst_x == src_x + 1) { + vui_outs("\x1b" "M"); + abort(); + } else if (dst_y == src_y + 1 && dst_x == src_x + 1) { + vui_outs("\x1b" "D"); + abort(); + } else if (dst_y > 0) { + vui_outf(CSI "%ud", dst_y + 1); + } else if (dst_y == src_y - 1) { + vui_outs(CSI "A"); + } else if (dst_y == src_y -+1) { + vui_outs(CSI "B"); + } else { + vui_outs(CSI "d"); + } + } + *ptr_x = dst_x; + *ptr_y = dst_y; +} + +#define M(s) do {\ + if (sep) vui_outc(';');\ + vui_outs(s);\ + sep = 1;\ +} while(0) + +static void attr_chg(VuiAttr *ptr, VuiAttr to) { + VuiAttr from = *ptr; + if (from == to) return; + + /* bold colors will keep the terminal bold, even on changing after */ + if ((from & 8) && (~to & 8) && (~to & A_BOLD)) { + from |= A_BOLD; + } + + int attr_chg_count = stdc_count_ones(ATTR_A(from) ^ ATTR_A(to)); + int chg_attr = (attr_chg_count != 0); + 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 { + int sep = 0; + vui_outs(CSI); + if ((to ^ from) & A_BOLD) { + if (to & A_BOLD) M("1"); + else { + M("22"); + from &= (~A_BOLD & ~A_DIM); + } + } + if ((to ^ from) & A_DIM) { + if (to & A_BOLD) M("2"); + else { + M("22"); + from &= (~A_BOLD & ~A_DIM); + } + } + if ((to ^ from) & A_ITALIC) M((to & A_ITALIC) ? "3" : "23"); + if ((to ^ from) & A_UNDERSCORE) M((to & A_UNDERSCORE) ? "4" : "24"); + if ((to ^ from) & A_BLINK) M((to & A_BLINK) ? "5" : "25"); + if ((to ^ from) & A_REVERSE) M((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; +} + +void vui_scroll_buf(VuiBuffer *b, int dx, int dy); + +static VuiAttr attr_last = ATTR_DEFAULT; +static unsigned cur_x = 0; +static unsigned cur_y = 0; + +static void emit_scroll_x(u32 y) { + if (win.scroll_x < 0) { + /* delete chars at start of line */ + attr_chg(&attr_last, ATTR_DEFAULT); + curs_move(&cur_x, &cur_y, 0, y); + vui_outf(CSI "%dP", -win.scroll_x); + } else if (win.scroll_x > 0) { + /* insert spaces at start of line */ + attr_chg(&attr_last, ATTR_DEFAULT); + curs_move(&cur_x, &cur_y, 0, y); + vui_outf(CSI "%d@", win.scroll_x); + } +} + +static void emit_scroll_y(void) { + /* xterm has \x1b[S and \x1b[T to scroll directly */ + /* but these don't work in linux vtty */ + if (win.scroll_y < 0) { + /* delete lines at top */ + curs_move(&cur_x, &cur_y, 0, 0); + attr_chg(&attr_last, ATTR_DEFAULT); + if (win.scroll_y == -1) vui_outs(CSI "M"); + else vui_outf(CSI "%dM", -win.scroll_y); + } else if (win.scroll_y > 0) { + /* insert blank lines at top */ + curs_move(&cur_x, &cur_y, 0, 0); + attr_chg(&attr_last, ATTR_DEFAULT); + if (win.scroll_y == 1) vui_outs(CSI "L"); + else vui_outf(CSI "%dL", win.scroll_y); + } +} + +void vui_blit(void) { + VuiBuffer *front = win.front; + VuiBuffer *back = win.back; + + if (win.redraw_all) { + vui_outs(CSI "H" CSI "2J" CSI "0m"); + attr_last = ATTR_DEFAULT; + unsigned max = COLS * LINES; + for (unsigned i = 0; i < max; i++) { + attr_chg(&attr_last, front->attr[i]); + vui_outvc(front->chr[i]); + } + vui_outs(CSI "H"); + cur_x = 0; + cur_y = 0; + win.redraw_all = 0; + win.scroll_x = 0; + win.scroll_y = 0; + goto end; + } + + emit_scroll_y(); + vui_scroll_buf(back, win.scroll_x, win.scroll_y); + for (unsigned y = 0; y < LINES; y++) { + unsigned x = 0; + emit_scroll_x(y); + while (x < COLS) { + while (x < COLS && bchr_equiv(back, front, (y * COLS) + x)) x++; + if (x >= COLS) break; + unsigned x0 = x; + VuiAttr a = ATTR(x0, y); + while (x < COLS && !bchr_equiv(back, front, (y * COLS) + x) && BATTR(front, x, y) == a) x++; + if (x0 != x) { + curs_move(&cur_x, &cur_y, x0, y); + attr_chg(&attr_last, a); + vui_outvcn(&CHR(x0, y), x - x0); + cur_x = x; + } + } + } + +end: + win.scroll_x = 0; + win.scroll_y = 0; + + if (win.curs_vis) { + curs_move(&cur_x, &cur_y, win.curs_x, win.curs_y); + } + + 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)); + win.front = back; + win.back = front; +} + +void vui_chra(int x, int y, VuiChar 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, VuiChar c) { + vui_chra(x, y, c, ATTR_DEFAULT); +} + +static void truncate_span(int *x, unsigned *nptr) { + int n = (int)*nptr; + if (*x < 0) { + n += *x; + *x = 0; + } + if (*x >= (int)COLS) { + *nptr = 0; + return; + } + if (*x + n > (int)COLS) { + n = COLS - *x; + } + if (n < 0) n = 0; + assert(n >= 0); + //assert(*x + n < (int)COLS); + *nptr = n; +} + +void vui_putsna(int x, int y, const char *s, unsigned srcn, VuiAttr a) { + u32 n = utf8_decode_len(s, srcn); + truncate_span(&x, &n); + if (n < 1 || y < 0 || y >= (int)LINES) return; + utf8_decode(&CHR(x, y), s, n); + for (uint16_t *pa = &ATTR(x, y); n--;) *pa++ = a; +} + +void vui_putsn(int x, int y, const char *s, unsigned n) { + vui_putsna(x, y, s, n, ATTR_DEFAULT); +} + +void vui_putsa(int x, int y, const char *s, VuiAttr a) { + vui_putsna(x, y, s, strlen(s), a); +} + +void vui_puts(int x, int y, const char *s) { + vui_putsn(x, y, s, strlen(s)); +} + +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 + n > (int)COLS) n = COLS - x; + if (x < (int)COLS && y < (int)LINES && n > 0) { + char buf[n + 1]; + vsnprintf(buf, n + 1, fmt, ap2); + vui_putsna(x, y, buf, n, a); + } + va_end(ap2); + 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 vui_wait_for_input(int ms) { + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGWINCH); + sigprocmask(SIG_BLOCK, &set, NULL); + int r = poll(&(struct pollfd) { .fd = STDIN_FILENO, .events = POLLIN }, 1, ms); + sigprocmask(SIG_UNBLOCK, &set, NULL); + return r; +} + +int vui_has_input(void) { + return vui_wait_for_input(0); +} + +void vui_scroll_buf(VuiBuffer *b, int dx, int dy) { + if ((unsigned)abs(dx) >= b->width || (unsigned)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 > (int)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(win.front, dx, dy); + win.scroll_x += dx; + win.scroll_y += dy; +} + +static unsigned getk(void) { + char c; + int r; + do { + r = read(STDIN_FILENO, &c, 1); + } while (r == -1 && errno == EINTR); + if (r == -1) return KEY_EOF; + return c; +} + +static unsigned getk_utf8(uint8_t start) { + unsigned n = stdc_leading_ones(start); + if (n > 4) return KEY_INVALID; + static const uint8_t cpmask[4] = { 0x7f, 0x3f, 0x1f, 0xf }; + uint32_t ch = start & cpmask[n - 1]; + while (--n) { + uint8_t k = getk(); + if ((k & 0xc0) != 0x80) return KEY_INVALID; + ch = (ch << 6) | (k & 0x3f); + } + return ch; +} + +static VuiKey bad_key(void) { + /* yell */ + putchar('\a'); + fflush(stdout); + return KEY_INVALID; +} + +static VuiKey meta_key(u32 c) { + switch (c) { + case '2': return esc_key(getk()) | KEY_SHIFT_BIT; break; + case '3': return esc_key(getk()) | KEY_ALT_BIT; break; + case '4': return esc_key(getk()) | KEY_ALT_BIT | KEY_SHIFT_BIT; break; + case '5': return esc_key(getk()) | KEY_CTRL_BIT; break; + case '6': return esc_key(getk()) | KEY_CTRL_BIT | KEY_SHIFT_BIT; break; + case '7': return esc_key(getk()) | KEY_CTRL_BIT | KEY_ALT_BIT; break; + case '8': return esc_key(getk()) | KEY_CTRL_BIT | KEY_SHIFT_BIT | KEY_ALT_BIT; break; + case '9': return esc_key(getk()) | KEY_META_BIT; break; + case '1': + switch (getk()) { + case '0': return esc_key(getk()) | KEY_META_BIT | KEY_SHIFT_BIT; break; + case '1': return esc_key(getk()) | KEY_META_BIT | KEY_ALT_BIT; break; + case '3': return esc_key(getk()) | KEY_META_BIT | KEY_CTRL_BIT; break; + case '4': return esc_key(getk()) | KEY_META_BIT | KEY_SHIFT_BIT | KEY_CTRL_BIT; break; + case '5': return esc_key(getk()) | KEY_META_BIT | KEY_ALT_BIT | KEY_CTRL_BIT; break; + case '6': return esc_key(getk()) | KEY_META_BIT | KEY_SHIFT_BIT | KEY_ALT_BIT | KEY_CTRL_BIT; break; + default: return bad_key(); + } + default: return bad_key(); + } +} + +static VuiKey esc_key(u32 c) { + switch (c) { + case 'P': return KEY_F1; break; + case 'Q': return KEY_F2; break; + case 'R': return KEY_F3; break; + case 'S': return KEY_F4; break; + case '1': + switch (getk()) { + case ';': return meta_key(getk()); + case '5': + default: return bad_key(); + } + break; + case 'A': return KEY_UP; + case 'B': return KEY_DOWN; + case 'C': return KEY_RIGHT; + case 'D': return KEY_LEFT; + case 'H': return KEY_HOME; + case 'F': return KEY_END; + case '5': + if (getk() != '~') return bad_key(); + return KEY_PGUP; + case '6': + if (getk() != '~') return bad_key(); + return KEY_PGDN; + default: return bad_key(); + } +} + +VuiKey vui_key(void) { + int c = getk(); + switch (c) { + case '\n': + return KEY_RET; + case '\b': case 0x7f: + return KEY_BKSP; + case '\x1b': + if (vui_has_input()) { + c = getk(); + switch (c) { + case 'O': + switch (getk()) { + case 'P': return KEY_F1; break; + case 'Q': return KEY_F2; break; + case 'R': return KEY_F3; break; + case 'S': return KEY_F4; break; + default: return bad_key(); break; + } + case '[': return esc_key(getk()); + default: return bad_key(); + } + } else { + return KEY_ESC; + } + default: + return c & 0x80 ? getk_utf8(c) : c; + } +} diff --git a/vui.h b/vui.h new file mode 100644 index 0000000..c8f7a50 --- /dev/null +++ b/vui.h @@ -0,0 +1,144 @@ +#ifndef VUI_H +#define VUI_H + +#include +#include + +typedef enum { + KEY_EOF = 0, + KEY_BKSP = 0x08, + KEY_RET = 0x0d, + KEY_ESC = 0x1b, + KEY_DEL = 0x7f, + KEY_UTF8_MAX = 0x10ffff, + + KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, + KEY_HOME, KEY_END, KEY_PGUP, KEY_PGDN, + + KEY_F1, KEY_F2, KEY_F3, KEY_F4, + KEY_F5, KEY_F6, KEY_F7, KEY_F8, + KEY_F9, KEY_F10, KEY_F11, KEY_F12, + + KEY_SHIFT_BIT = 0x200000, + KEY_CTRL_BIT = 0x400000, + KEY_ALT_BIT = 0x800000, + KEY_META_BIT = 0x1000000, + KEY_INVALID = 0x7fffffff, + + KEY_CTRL_MASK = 0xe000000, + KEY_BASE_MASK = ~KEY_CTRL_MASK, +} VuiKey; + +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 uint32_t VuiChar; + +typedef struct { + unsigned width, height; + VuiChar *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; + int curs_vis, curs_x, curs_y; +} VuiWindow; + +VuiWindow *vui_win(void); + +#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_init(void); +void vui_fini(void); + +void vui_blit(void); +void vui_clear(void); +void vui_scroll(int dx, int dy); + +void vui_chr(int x, int y, VuiChar c); +void vui_chra(int x, int y, VuiChar c, VuiAttr a); + +void vui_puts(int x, int y, const char *s); +void vui_putsa(int x, int y, const char *s, VuiAttr a); +void vui_putsn(int x, int y, const char *s, unsigned n); +void vui_putsna(int x, int y, const char *s, unsigned n, VuiAttr a); + +int vui_avprintf(int x, int y, VuiAttr a, const char *fmt, va_list ap); +int vui_aprintf(int x, int y, VuiAttr a, const char *fmt, ...); +int vui_printf(int x, int y, const char *fmt, ...); + +void vui_fill(VuiChar c, VuiAttr a); +void vui_fill_rect(VuiChar c, VuiAttr a, int x0, int y0, int width, int height); + +void vui_curs_vis(int vis); +void vui_curs_pos(int x, int y); + +void vui_redraw_fn(void (*fn)(void *ctx)); +void vui_redraw_ctx(void *ctx); + +int vui_wait_for_input(int ms); +int vui_has_input(void); + +VuiKey vui_key(void); + +#endif -- cgit v1.2.3