/* 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 #include #include #include #include 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; }