diff options
Diffstat (limited to 'tests/test_clio.c')
| -rw-r--r-- | tests/test_clio.c | 884 |
1 files changed, 884 insertions, 0 deletions
diff --git a/tests/test_clio.c b/tests/test_clio.c new file mode 100644 index 0000000..535df1d --- /dev/null +++ b/tests/test_clio.c @@ -0,0 +1,884 @@ +/* test_clio.c — standalone test suite for clio.h (+ escape.h). + * Zero dependencies: single TU, no linked libraries (defines + * ESCAPE_IMPLEMENTATION to pull in the esc_* symbols). */ +#define ESCAPE_IMPLEMENTATION +#include "clio.h" + +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +/* Capture stdout (fd 1) into a malloc'd buffer across the test body. */ +typedef struct cap { + int pfd[2]; + int saved; +} cap_t; + +/* dup2() returns the target fd (STDOUT_FILENO == 1) on success, -1 on + * failure — compare against -1, not 0. */ +static int cap_begin(cap_t *c) { + if (pipe(c->pfd) != 0) + return -1; + c->saved = dup(STDOUT_FILENO); + if (c->saved < 0) + return -1; + if (dup2(c->pfd[1], STDOUT_FILENO) == -1) { + close(c->pfd[0]); + close(c->pfd[1]); + close(c->saved); + return -1; + } + close(c->pfd[1]); + return 0; +} + +static char *cap_end(cap_t *c, size_t *len) { + fflush(stdout); + if (dup2(c->saved, STDOUT_FILENO) == -1) { + close(c->saved); + return NULL; + } + close(c->saved); + size_t cap = 256; + size_t n = 0; + char *buf = (char *) malloc(cap); + char tmp[1024]; + ssize_t r; + while ((r = read(c->pfd[0], tmp, sizeof tmp)) > 0) { + if (n + (size_t) r > cap) { + cap = (cap + (size_t) r) * 2; + buf = (char *) realloc(buf, cap); + } + memcpy(buf + n, tmp, (size_t) r); + n += (size_t) r; + } + close(c->pfd[0]); + buf[n] = '\0'; /* callers use both strlen() and len */ + *len = n; + return buf; +} + +static int contains(const char *hay, size_t len, const char *needle) { + size_t nl = strlen(needle); + if (nl > len) + return 0; + for (size_t i = 0; i + nl <= len; i++) + if (memcmp(hay + i, needle, nl) == 0) + return 1; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Text: UTF-8 + width */ +/* ------------------------------------------------------------------ */ + +static void test_utf8(void) { + u32 cp; + char out[8]; + CHECK(clio_utf8_next("a", &cp) == 1 && cp == 'a', "ascii decode"); + CHECK(clio_utf8_next("\xC3\xA9", &cp) == 2 && cp == 0xE9, "latin1 decode"); + CHECK(clio_utf8_next("\xE4\xB8\xAD", &cp) == 3 && cp == 0x4E2D, + "cjk decode"); + CHECK(clio_utf8_next("\xF0\x9F\x98\x80", &cp) == 4 && cp == 0x1F600, + "emoji decode"); + /* truncated sequence: consumes what is left */ + CHECK(clio_utf8_next("\xE4\xB8", &cp) == 2, "truncated decode"); + CHECK(clio_utf8_enc(0x4E2D, out) == 3 && memcmp(out, "\xE4\xB8\xAD", 3) == 0, + "cjk encode"); + CHECK(clio_utf8_enc(0x1F600, out) == 4, "emoji encode"); + CHECK(clio_utf8_enc(0xD800, out) == -1, "surrogate rejected"); + CHECK(clio_utf8_enc(0x110000, out) == -1, "out of range rejected"); + + CHECK(clio_text_width("abc") == 3, "ascii width"); + CHECK(clio_text_width("h\xC3\xA9llo") == 5, "latin width"); + CHECK(clio_text_width("\xE4\xB8\xAD\xE6\x96\x87") == 4, "cjk width"); + CHECK(clio_text_width("\xF0\x9F\x98\x80") == 2, "emoji width"); + CHECK(clio_cp_width('\n') == 0, "newline zero width"); + + const char *s = "ab\xE4\xB8\xAD" "c"; /* a b 中 c */ + CHECK(clio_char_start(s, 4) == 2, "char_start cjk"); + CHECK(clio_char_start(s, 2) == 2, "char_start at boundary"); + CHECK(clio_char_start(s, 1) == 1, "char_start ascii"); + CHECK(clio_char_end(s, 6, 2) == 5, "char_end cjk"); + CHECK(clio_char_end(s, 6, 5) == 6, "char_end last"); + + /* semantics verified against bash/readline Ctrl+W via pty: + * "foo bar baz"@12 -> 7, "foo bar"@8 -> 3, "a_b c"@5 -> 3 */ + CHECK(clio_word_back("foo bar baz", 12) == 7, "word_back word"); + CHECK(clio_word_back("foo bar", 8) == 3, "word_back across spaces"); + CHECK(clio_word_back("a_b c", 5) == 3, "word_back underscore"); +} + +/* ------------------------------------------------------------------ */ +/* Canvas */ +/* ------------------------------------------------------------------ */ + +static void test_buf(void) { + clio_buf b; + clio_style s = clio_style_default(); + s.fg = 248; + s.bg = 238; + s.attr = CLIO_BOLD | CLIO_REVERSE; + + CHECK(clio_buf_init(&b, 3, 8) == 0, "buf init"); + CHECK(b.rows == 3 && b.cols == 8, "buf dims"); + + /* default cell is blank + default style */ + clio_cell *c0 = b.cells; + CHECK(c0->ch == 0 && c0->fg == 0xFFFF && c0->bg == 0xFFFF && c0->attr == 0, + "default cell"); + + CHECK(clio_buf_put(&b, 0, 0, "hi", s, 0) == 2, "put ascii"); + CHECK(b.cells[0].ch == 'h' && b.cells[1].ch == 'i', "put stored"); + CHECK(b.cells[0].fg == 248 && b.cells[0].bg == 238, "put style"); + CHECK(b.cells[0].attr == (CLIO_BOLD | CLIO_REVERSE), "put attr"); + CHECK(b.cells[2].ch == 0, "put bounds"); + + /* clipping: no wrap */ + CHECK(clio_buf_put(&b, 0, 6, "abc", s, 0) == 2, "clip no wrap"); + CHECK(b.cells[7].ch == 'b', "clip stored edge"); + clio_buf_clear(&b); + + /* wrapping onto next row */ + CHECK(clio_buf_put(&b, 0, 6, "abcd", s, 1) == 4, "wrap put"); + CHECK(b.cells[7].ch == 'b', "wrap first row edge"); + CHECK(b.cells[8].ch == 'c' && b.cells[9].ch == 'd', "wrap second row"); + + clio_buf_clear(&b); + + /* newline moves to next row at col 0 */ + clio_buf_put(&b, 0, 0, "a\nb", s, 1); + CHECK(b.cells[0].ch == 'a' && b.cells[8].ch == 'b', "newline put"); + + clio_buf_clear(&b); + + /* wide char occupies two cells */ + clio_buf_put(&b, 0, 0, "\xE4\xB8\xAD\xE6\x96\x87", s, 1); + CHECK(b.cells[0].ch == 0x4E2D && b.cells[1].ch == 0, "wide first cell"); + CHECK(b.cells[2].ch == 0x6587 && b.cells[3].ch == 0, "wide second cell"); + + clio_buf_clear(&b); + + /* zero-width chars consume no cell */ + clio_buf_put(&b, 0, 0, "\xE2\x80\x8Bx", s, 1); /* ZWSP + x */ + CHECK(b.cells[0].ch == 'x', "zero width skips"); + + clio_buf_clear(&b); + + /* fill + clear region */ + clio_buf_fill_region(&b, 0, 0, 1, 3, '*', s); + CHECK(b.cells[0].ch == '*' && b.cells[3].ch == '*', "fill row 0"); + CHECK(b.cells[8].ch == '*' && b.cells[11].ch == '*', "fill row 1"); + CHECK(b.cells[4].ch == 0, "fill bounds"); + clio_buf_clear_region(&b, 0, 1, 1, 2); + CHECK(b.cells[0].ch == '*' && b.cells[1].ch == 0 && b.cells[2].ch == 0, + "clear region"); + CHECK(b.cells[10].ch == 0 && b.cells[11].ch == '*', "clear region row 1"); + clio_buf_clear(&b); + + /* box 2x4 in top-left */ + clio_buf_box(&b, 0, 0, 4, 2, '#', s); + CHECK(b.cells[0].ch == '#' && b.cells[3].ch == '#', "box top"); + CHECK(b.cells[8].ch == '#' && b.cells[11].ch == '#', "box bottom"); + clio_buf_clear(&b); + + /* scroll up: rows 0..2 shift, bottom becomes blank */ + clio_buf_put(&b, 0, 0, "aa", s, 0); + clio_buf_put(&b, 1, 0, "bb", s, 0); + clio_buf_put(&b, 2, 0, "cc", s, 0); + clio_buf_scroll_up(&b, 0, 0, 2, 1); + CHECK(b.cells[0].ch == 'b' && b.cells[8].ch == 'c', "scroll moved"); + CHECK(b.cells[16].ch == 0, "scroll bottom blank"); + + clio_buf_free(&b); + CHECK(b.cells == NULL, "buf free"); + + /* init with zero dims clamps to 1x1 */ + CHECK(clio_buf_init(&b, 0, 0) == 0 && b.rows == 1 && b.cols == 1, + "buf init clamps"); + clio_buf_free(&b); +} + +/* ------------------------------------------------------------------ */ +/* Key decoder */ +/* ------------------------------------------------------------------ */ + +static size_t decode(clio_keydec *d, const char *in, clio_key *k, size_t cap) { + clio_keydec_init(d); + return clio_keydec_feed(d, (const unsigned char *) in, strlen(in), k, cap); +} + +static void test_keydec(void) { + clio_keydec d; + clio_key k[16]; + size_t n; + + n = decode(&d, "\x1B[C", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_RIGHT, "csi right"); + n = decode(&d, "\x1B[D", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_LEFT, "csi left"); + n = decode(&d, "\x1B[A", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UP, "csi up"); + n = decode(&d, "\x1B[B", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DOWN, "csi down"); + n = decode(&d, "\x1B[H", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi home"); + n = decode(&d, "\x1B[F", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "csi end"); + n = decode(&d, "\x1B[Z", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_BACKTAB, "csi backtab"); + n = decode(&d, "\x1B[1~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi 1~ home"); + n = decode(&d, "\x1B[7~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi 7~ home"); + n = decode(&d, "\x1B[3~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DELETE, "csi 3~ delete"); + n = decode(&d, "\x1B[4~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "csi 4~ end"); + n = decode(&d, "\x1B[5~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_PAGEUP, "csi 5~ pageup"); + n = decode(&d, "\x1B[6~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_PAGEDOWN, "csi 6~ pagedown"); + + n = decode(&d, "\x1BOA", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UP, "ss3 up"); + n = decode(&d, "\x1BOF", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "ss3 end"); + + n = decode(&d, "\t\r\x7F\x08", k, 16); + CHECK(n == 4 && k[0].kind == CLIOK_TAB && k[1].kind == CLIOK_ENTER && + k[2].kind == CLIOK_BACKSPACE && k[3].kind == CLIOK_BACKSPACE, + "tab enter backspace del8"); + + n = decode(&d, "\x01\x03\x04\x05\x0B\x0C\x15\x17\x19\x1A", k, 16); + CHECK(n == 10 && k[0].kind == CLIOK_CTRL_A && k[1].kind == CLIOK_CTRL_C && + k[2].kind == CLIOK_CTRL_D && k[3].kind == CLIOK_CTRL_E && + k[4].kind == CLIOK_CTRL_K && k[5].kind == CLIOK_CTRL_L && + k[6].kind == CLIOK_CTRL_U && k[7].kind == CLIOK_CTRL_W && + k[8].kind == CLIOK_CTRL_Y && k[9].kind == CLIOK_CTRL_Z, + "ctrl letters"); + + n = decode(&d, "\x07", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_CTRL_G, "ctrl g"); + n = decode(&d, "\x1B", k, 16); + CHECK(n == 0, "bare esc not emitted yet"); + CHECK(clio_keydec_flush(&d, &k[0]) == 1 && k[0].kind == CLIOK_ESC, + "esc flush"); + + n = decode(&d, "h\xC3\xA9\xE4\xB8\xAD", k, 16); + CHECK(n == 3 && k[0].cp == 'h' && k[1].cp == 0xE9 && k[2].cp == 0x4E2D, + "utf8 keys"); + + /* kitty protocol */ + n = decode(&d, "\x1B[13u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_ENTER, "kitty enter"); + n = decode(&d, "\x1B[127u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DELETE, "kitty delete"); + n = decode(&d, "\x1B[9u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_TAB, "kitty tab"); + n = decode(&d, "\x1B[27u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_ESC, "kitty esc"); + + /* bracketed paste: LF inside paste is a char; start/end emit no key */ + n = decode(&d, "\x1B[200~a\nb\x1B[201~", k, 16); + CHECK(n == 3 && k[0].cp == 'a' && k[1].cp == '\n' && k[2].cp == 'b', + "bracketed paste"); + + /* bare LF outside paste: dead key */ + n = decode(&d, "\n", k, 16); + CHECK(n == 0, "bare lf dead"); + + /* split feeds: escape sequence across chunk boundaries */ + clio_keydec_init(&d); + n = clio_keydec_feed(&d, (const unsigned char *) "\x1B", 1, k, 16); + CHECK(n == 0, "split esc"); + n = clio_keydec_feed(&d, (const unsigned char *) "[", 1, k, 16); + CHECK(n == 0, "split ["); + n = clio_keydec_feed(&d, (const unsigned char *) "C", 1, k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_RIGHT, "split C"); + + /* split UTF-8 across chunk boundaries */ + clio_keydec_init(&d); + n = clio_keydec_feed(&d, (const unsigned char *) "\xC3", 1, k, 16); + CHECK(n == 0, "split utf8 lead"); + n = clio_keydec_feed(&d, (const unsigned char *) "\xA9", 1, k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_CHAR && k[0].cp == 0xE9, "split utf8"); + + /* unknown CSI emits UNKNOWN so the app can react (e.g. beep) */ + n = decode(&d, "\x1B[15~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UNKNOWN, "unknown csi -> UNKNOWN"); + /* cursor position report swallowed */ + n = decode(&d, "\x1B[12;40R", k, 16); + CHECK(n == 0, "cpr swallowed"); +} + +/* ------------------------------------------------------------------ */ +/* Color policy */ +/* ------------------------------------------------------------------ */ + +static void setenv0(const char *k, const char *v) { + if (v) + setenv(k, v, 1); + else + unsetenv(k); +} + +static void test_color_policy(void) { + setenv0("CLICOLOR_FORCE", NULL); + setenv0("NO_COLOR", NULL); + setenv0("CI", NULL); + setenv0("TERM", NULL); + CHECK(clio_color_policy(1) == 1, "tty on"); + CHECK(clio_color_policy(0) == 0, "notty off"); + setenv0("NO_COLOR", "1"); + CHECK(clio_color_policy(1) == 0, "NO_COLOR off"); + setenv0("NO_COLOR", NULL); + setenv0("TERM", "dumb"); + CHECK(clio_color_policy(1) == 0, "TERM=dumb off"); + setenv0("TERM", NULL); + setenv0("CI", "true"); + CHECK(clio_color_policy(1) == 0, "CI off"); + setenv0("CI", NULL); + setenv0("CLICOLOR_FORCE", "1"); + CHECK(clio_color_policy(0) == 1, "force on, notty"); + setenv0("NO_COLOR", "1"); + CHECK(clio_color_policy(1) == 1, "force beats NO_COLOR"); + setenv0("CLICOLOR_FORCE", "0"); + CHECK(clio_color_policy(1) == 0, "force 0 off"); + setenv0("CLICOLOR_FORCE", NULL); +} + +/* ------------------------------------------------------------------ */ +/* Style emission */ +/* ------------------------------------------------------------------ */ + +static void test_fstyle(void) { + cap_t c; + clio_style s = clio_style_default(); + s.fg = 38; + s.bg = 48; + s.attr = CLIO_BOLD | CLIO_UNDERLINE; + + CHECK(cap_begin(&c) == 0, "cap begin 1"); + clio_fstyle(stdout, s); + size_t len; + char *out = cap_end(&c, &len); + CHECK(out != NULL && len > 0, "fstyle wrote"); + if (out) { + /* exact sequence: reset, bold, underline, fg, bg (in that order) */ + CHECK(contains(out, len, ESC_RESET) == 1, "fstyle reset"); + CHECK(contains(out, len, ESC_BOLD) == 1, "fstyle bold"); + CHECK(contains(out, len, ESC_UNDERLINE) == 1, "fstyle underline"); + CHECK(contains(out, len, ESC_COLOR_FG_256(38)) == 1, "fstyle fg256"); + CHECK(contains(out, len, ESC_COLOR_BG_256(48)) == 1, "fstyle bg256"); + CHECK(contains(out, len, "48;5;48m") == 1, "bg uses 48 not 38"); + CHECK(contains(out, len, ESC_ITALIC) == 0, "no italic"); + /* reset first, fg before bg */ + const char *rp = strstr(out, ESC_RESET); + const char *fp = strstr(out, ESC_COLOR_FG_256(38)); + const char *bp = strstr(out, ESC_COLOR_BG_256(48)); + CHECK(rp && fp && bp && rp < fp && fp < bp, "fstyle order"); + free(out); + } + + /* default style: only reset */ + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_fstyle(stdout, clio_style_default()); + out = cap_end(&c, &len); + CHECK(out != NULL && strcmp(out, ESC_RESET) == 0, "default = reset only"); + free(out); +} + +/* ------------------------------------------------------------------ */ +/* Flush (non-tty plain + tty diff) */ +/* ------------------------------------------------------------------ */ + +static void test_flush_plain(void) { + clio_term t; + clio_term_init(&t); + clio_buf b; + clio_buf_init(&b, 2, 8); + clio_style s = clio_style_default(); + s.fg = 248; + clio_buf_put(&b, 0, 0, "hi", s, 0); + clio_buf_put(&b, 1, 0, "yo", s, 0); + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin"); + clio_flush(&t, &b); + size_t len; + char *out = cap_end(&c, &len); + CHECK(out != NULL, "cap end"); + CHECK(len > 0, "plain wrote"); + CHECK(contains(out, len, "hi"), "plain row 0"); + CHECK(contains(out, len, "yo"), "plain row 1"); + CHECK(contains(out, len, "\x1B") == 0, "plain has no escapes"); + CHECK(contains(out, len, "hi\nyo\n") == 1, "plain exact lines"); + free(out); + clio_buf_free(&b); + clio_term_free(&t); +} + +static void test_flush_diff(void) { + clio_term t; + clio_term_init(&t); + /* force tty mode + fixed size for a deterministic test */ + t.is_tty = 1; + t.rows = 3; + t.cols = 8; + + clio_buf a; + clio_buf_init(&a, 3, 8); + clio_style s = clio_style_default(); + clio_buf_put(&a, 0, 0, "row0", s, 0); + clio_buf_put(&a, 1, 0, "row1", s, 0); + clio_buf_put(&a, 2, 0, "row2", s, 0); + + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin 1"); + clio_flush(&t, &a); + size_t len; + char *out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 1, "first flush clears"); + CHECK(contains(out, len, "\x1B[1;1H") == 1, "first flush row 1 pos"); + CHECK(contains(out, len, "\x1B[3;1H") == 1, "first flush row 3 pos"); + CHECK(contains(out, len, "row0") == 1, "first flush content"); + free(out); + + /* change only row 2 */ + clio_buf b; + clio_buf_init(&b, 3, 8); + clio_buf_put(&b, 0, 0, "row0", s, 0); + clio_buf_put(&b, 1, 0, "row1", s, 0); + clio_buf_put(&b, 2, 0, "CHG2", s, 0); + + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_flush(&t, &b); + out = cap_end(&c, &len); + CHECK(contains(out, len, "\x1B[3;1H") == 1, "diff row 3 pos"); + CHECK(contains(out, len, "CHG2") == 1, "diff row 2 content"); + CHECK(contains(out, len, "\x1B[1;1H") == 0, "diff skips row 1"); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 0, "diff no clear"); + CHECK(contains(out, len, "row0") == 0, "diff skips row 0 content"); + free(out); + + /* size change forces full repaint */ + clio_buf d2; + clio_buf_init(&d2, 2, 4); + clio_buf_put(&d2, 0, 0, "xy", s, 0); + clio_buf_put(&d2, 1, 0, "zw", s, 0); + clio_buf_free(&t.last); + t.has_last = 0; + t.rows = 2; + t.cols = 4; + CHECK(cap_begin(&c) == 0, "cap begin 3"); + clio_flush(&t, &d2); + out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 1, "resize full repaint"); + CHECK(contains(out, len, "\x1B[1;1H") == 1, "resize row 1"); + free(out); + + clio_buf_free(&a); + clio_buf_free(&b); + clio_buf_free(&d2); + clio_term_free(&t); +} + +/* ------------------------------------------------------------------ */ +/* Term toggles emit the right codes */ +/* ------------------------------------------------------------------ */ + +static void test_term_toggles(void) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin"); + clio_term_alt_on(&t); + clio_term_cursor_hide(&t); + clio_term_paste_on(&t); + clio_term_kitty_on(&t); + clio_term_bell(&t); + clio_term_alt_on(&t); /* idempotent: no second emission */ + size_t len; + char *out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_ALT_SCREEN_ON) == 1, "alt on"); + CHECK(contains(out, len, ESC_ALT_SCREEN_ON) == 1, "alt on once"); + CHECK(contains(out, len, ESC_CURSOR_HIDE) == 1, "cursor hide"); + CHECK(contains(out, len, ESC_BRACKETED_PASTE_ON) == 1, "paste on"); + CHECK(contains(out, len, ESC_KITTY_ON) == 1, "kitty on"); + CHECK(contains(out, len, ESC_BELL) == 1, "bell"); + free(out); + + /* shutdown restores everything, in reverse, once each */ + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_term_shutdown(&t); + out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_KITTY_OFF) == 1, "kitty off"); + CHECK(contains(out, len, ESC_BRACKETED_PASTE_OFF) == 1, "paste off"); + CHECK(contains(out, len, ESC_CURSOR_SHOW) == 1, "cursor show"); + CHECK(contains(out, len, ESC_ALT_SCREEN_OFF) == 1, "alt off"); + /* order: kitty first, alt last */ + if (out) { + const char *kp = strstr(out, ESC_KITTY_OFF); + const char *ap = strstr(out, ESC_ALT_SCREEN_OFF); + CHECK(kp && ap && kp < ap, "shutdown order"); + } + free(out); + clio_term_free(&t); +} + +/* ------------------------------------------------------------------ */ +/* Readline (scripted key source) */ +/* ------------------------------------------------------------------ */ + +typedef struct keys { + clio_key *ks; + size_t n; + size_t i; +} keys_t; + +static int keys_next(void *ud, clio_key *k, int timeout_ms) { + (void) timeout_ms; + keys_t *s = (keys_t *) ud; + if (s->i >= s->n) + return 0; + *k = s->ks[s->i++]; + return 1; +} + +static void push(keys_t *s, int kind, u32 cp) { + s->ks[s->n].kind = kind; + s->ks[s->n].cp = cp; + s->n++; +} + +static char *run_readline(keys_t *s, int echo, int rows, int cols) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = (size_t) rows; + t.cols = (size_t) cols; + clio_buf b; + clio_buf_init(&b, (size_t) rows, (size_t) cols); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + s->i = 0; + char *r = clio_readline(&t, &b, 0, "> ", p, st, echo, keys_next, s, 0); + clio_buf_free(&b); + clio_term_free(&t); + return r; +} + +static void test_readline(void) { + keys_t s; + clio_key ks[64]; + s.ks = ks; + s.n = 0; + char *r; + + /* type "ab", backspace, "c" -> "ac" */ + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r != NULL && strcmp(r, "ac") == 0, "insert backspace"); + free(r); + + /* left arrow + insert: caret between a and c, x goes there -> "axc" */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CHAR, 'x'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "axc") == 0, "left insert"); + free(r); + + /* ctrl+a, 'z' -> "zaxc" */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'x'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_CTRL_A, 0); + push(&s, CLIOK_CHAR, 'z'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "zaxc") == 0, "ctrl+a insert"); + free(r); + + /* ctrl+u clears line -> "" (submit empty) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CTRL_U, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+u clears all"); + + /* ctrl+a then ctrl+k kills whole line -> empty submit */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_CTRL_A, 0); + push(&s, CLIOK_CTRL_K, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+k empty after"); + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CTRL_K, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "ab") == 0, "ctrl+k kills rest"); + free(r); + + /* ctrl+w deletes a word */ + s.n = 0; + push(&s, CLIOK_CHAR, 'h'); + push(&s, CLIOK_CHAR, 'e'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'o'); + push(&s, CLIOK_CHAR, ' '); + push(&s, CLIOK_CHAR, 'w'); + push(&s, CLIOK_CHAR, 'o'); + push(&s, CLIOK_CHAR, 'r'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'd'); + push(&s, CLIOK_CTRL_W, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + /* matches bash/readline: the preceding space is killed too */ + CHECK(r && strcmp(r, "hello") == 0, "ctrl+w word"); + free(r); + + /* ctrl+c cancels */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CTRL_C, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+c cancels"); + + /* escape cancels */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_ESC, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "esc cancels"); + + /* ctrl+d on empty submits */ + s.n = 0; + push(&s, CLIOK_CTRL_D, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+d empty submits"); + + /* ctrl+d deletes the char after the caret (left first) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CTRL_D, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "a") == 0, "ctrl+d delete"); + free(r); + + /* delete key removes forward char (caret in the middle) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_DELETE, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "a") == 0, "delete forward"); + free(r); + + /* utf8 input round trip */ + s.n = 0; + push(&s, CLIOK_CHAR, 0x4E2D); /* 中 */ + push(&s, CLIOK_CHAR, 0x6587); /* 文 */ + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "\xE4\xB8\xAD\xE6\x96\x87") == 0, "utf8 input"); + free(r); + + /* echo=0: stars rendered, but value is real text */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 0, 24, 80); + CHECK(r && strcmp(r, "abc") == 0, "echo off value"); + free(r); +} + +/* ------------------------------------------------------------------ */ +/* Long lines: no wrap, window scrolls, backspace spans the "wrap" */ +/* ------------------------------------------------------------------ */ + +static void test_readline_long(void) { + /* 10 cols, prompt "> " => 8-cell text area. */ + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = 3; + t.cols = 10; + clio_buf b; + clio_buf_init(&b, 3, 10); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + clio_str txt; + clio_str_init(&txt); + clio_str_ins(&txt, 0, "abcdefghijkl", 12); + size_t col; + + /* caret at end: caret pinned to last col, tail visible, head scrolled */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 12, st, 1); + CHECK(col == 9, "caret pinned last col"); + CHECK(b.cells[2].ch == 'f' && b.cells[8].ch == 'l', "tail window"); + CHECK(b.cells[9].ch == 0, "nothing past caret"); + + /* one backspace of scroll: window slides left by one */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 11, st, 1); + CHECK(col == 9, "caret stays pinned"); + CHECK(b.cells[2].ch == 'e' && b.cells[9].ch == 'l', "window slid"); + + /* caret in the middle: anchored at 0, right edge clipped */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 3, st, 1); + CHECK(col == 5, "caret middle"); + CHECK(b.cells[2].ch == 'a' && b.cells[9].ch == 'h', "head window clipped"); + + /* wide chars: 8 CJK = 16 cells; window must start on a char + * boundary and the caret clamp inside the row */ + clio_str_free(&txt); + clio_str_init(&txt); + static const char cjk[] = "\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD" + "\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD"; + clio_str_ins(&txt, 0, cjk, sizeof cjk - 1); + col = clio_readline_paint(&b, 0, "> ", p, &txt, sizeof cjk - 1, st, 1); + CHECK(col == 9, "wide caret clamped"); + CHECK(b.cells[2].ch == 0x4E2D && b.cells[8].ch == 0x4E2D, + "wide window on boundary"); + + /* noecho: same windowing over star count */ + clio_str_free(&txt); + clio_str_init(&txt); + clio_str_ins(&txt, 0, "abcdefghijkl", 12); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 12, st, 0); + CHECK(col == 9, "star caret pinned"); + CHECK(b.cells[2].ch == '*' && b.cells[8].ch == '*' && b.cells[9].ch == 0, + "star window"); + + clio_str_free(&txt); + clio_buf_free(&b); + clio_term_free(&t); + + /* integration: type past the edge, backspace across the scroll + * boundary — the logical result must be exact */ + keys_t s; + clio_key ks[64]; + s.ks = ks; + s.n = 0; + for (char ch = 'a'; ch <= 'l'; ch++) + push(&s, CLIOK_CHAR, (u32) ch); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_ENTER, 0); + char *r = run_readline(&s, 1, 3, 10); + CHECK(r && strcmp(r, "abcdefghi") == 0, "backspace across wrap"); + free(r); + + /* left-arrow into the scrolled region, then backspace */ + s.n = 0; + for (char ch = 'a'; ch <= 'l'; ch++) + push(&s, CLIOK_CHAR, (u32) ch); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 3, 10); + CHECK(r && strcmp(r, "abcdefghijl") == 0, "left+backspace across wrap"); + free(r); +} + +/* ------------------------------------------------------------------ */ +/* Readline canvas paint */ +/* ------------------------------------------------------------------ */ + +static void test_readline_paint(void) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = 4; + t.cols = 20; + clio_buf b; + clio_buf_init(&b, 4, 20); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + clio_str txt; + clio_str_init(&txt); + + /* type "ab", caret after 'b' (col 4) */ + clio_str_ins(&txt, 0, "ab", 2); + size_t col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 1); + CHECK(col == 4, "caret col echo"); + CHECK(b.cells[0].ch == '>' && b.cells[1].ch == ' ', "prompt painted"); + CHECK(b.cells[2].ch == 'a' && b.cells[3].ch == 'b', "text painted"); + + /* echo off: stars */ + clio_buf_clear(&b); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 0); + CHECK(col == 4, "caret col noecho"); + CHECK(b.cells[2].ch == '*' && b.cells[3].ch == '*', "stars painted"); + + /* caret in the middle */ + clio_str_ins(&txt, 0, "ab", 2); + clio_str_ins(&txt, 1, "X", 1); + clio_str_ins(&txt, 2, "c", 1); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 1); + CHECK(col == 4, "caret middle"); + + clio_str_free(&txt); + clio_buf_free(&b); + clio_term_free(&t); +} + +int main(void) { + test_utf8(); + test_buf(); + test_keydec(); + test_color_policy(); + test_fstyle(); + test_flush_plain(); + test_flush_diff(); + test_term_toggles(); + test_readline(); + test_readline_long(); + test_readline_paint(); + + printf("%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} |