/* clio.h — minimal pseudo-TUI layout layer over escape.h. * * escape.h owns styling (SGR, cursor motion, clear, color); clio.h owns * layout: where text goes, how it wraps and clips, and how a composed * frame is blitted to the terminal with the minimal diff of escape * sequences. Apps never emit raw escape codes themselves. * * Model: * clio_term the real terminal: size, capabilities, raw mode, alt * screen, cursor, bracketed-paste/kitty state, key queue, * and the last flushed frame (for diffing). * clio_buf an in-memory cell grid (the canvas). Apps compose frames * here; nothing is written until clio_flush. * clio_flush diff the canvas against the previous frame and blit only * changed rows through escape.h. Non-tty (pipe, CI) falls * back to plain text lines so piped output stays clean. * input a pure, chunk-fed key decoder (testable without a tty), * raw-mode key reading with a bounded queue, and a * single-line editor (clio_readline) that renders into the * canvas. * * Conventions: C99, header-only (static inline), no global state, * extern "C", POSIX-first (termios/poll/TIOCGWINSZ) — Windows is a * documented limitation, as in escape.h. * * Usage: #include "clio.h". The header is complete on its own; escape.h * is pulled in for the styling layer. The esc_* function symbols need one * defining TU with ESCAPE_IMPLEMENTATION (llshcpp's src/escape.c, or the * test TU itself for zero-link builds). * * Layout is column-based: one cell per column, one code point per cell. * Wide characters (width 2) occupy one cell; a wide character landing * in the last column may be clipped by the terminal (v1 limitation). * * API: * style: clio_style, clio_style_default, clio_style_eq * text: clio_utf8_next_n, clio_utf8_enc, clio_cp_width, * clio_text_width, clio_char_start, clio_char_end, * clio_word_back * canvas: clio_buf, clio_cell, clio_buf_init/free/clear, * clio_buf_at, clio_buf_clear_region, clio_buf_fill_region, * clio_buf_put, clio_buf_box, clio_buf_scroll_up * keys: clio_key, CLIOK_*, clio_keydec, clio_keydec_init, * clio_keydec_feed, clio_keydec_flush * terminal: clio_term, clio_color_policy, clio_term_init, * clio_term_resize, clio_term_raw_on/off, * clio_term_alt_on/off, clio_term_cursor_hide/show, * clio_term_paste_on/off, clio_term_kitty_on/off, * clio_term_bell, clio_term_shutdown, clio_term_free * output: clio_fstyle, clio_flush, clio_cursor * input: clio_next_key, clio_next_key_stdin, clio_read_key, * clio_readline * * Concurrency: no shared state; each clio_term is independent. */ #ifndef CLIO_H #define CLIO_H #include "escape.h" #include "type.h" #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* ------------------------------------------------------------------ */ /* Style */ /* ------------------------------------------------------------------ */ /* Attribute bits (OR-combined into clio_style.attr). */ enum { CLIO_BOLD = 1u, CLIO_DIM = 2u, CLIO_ITALIC = 4u, CLIO_UNDERLINE = 8u, CLIO_REVERSE = 16u, CLIO_STRIKE = 32u, }; /* fg/bg: -1 = terminal default, otherwise a 0..255 palette index. * attr: OR of CLIO_* bits. */ typedef struct clio_style { int fg; int bg; unsigned attr; } clio_style; static inline clio_style clio_style_default(void) { clio_style s; s.fg = -1; s.bg = -1; s.attr = 0; return s; } static inline int clio_style_eq(const clio_style *a, const clio_style *b) { return a->fg == b->fg && a->bg == b->bg && a->attr == b->attr; } /* ------------------------------------------------------------------ */ /* Text (UTF-8 + display width) */ /* ------------------------------------------------------------------ */ /* Decode the next code point in [p, end); returns the byte length used. * Truncated or invalid sequences still consume at least one byte. */ static inline size_t clio_utf8_next_n(const char *p, const char *end, u32 *cp) { unsigned char b = (unsigned char) p[0]; size_t n; if (b < 0x80) n = 1; else if ((b & 0xE0) == 0xC0) n = 2; else if ((b & 0xF0) == 0xE0) n = 3; else if ((b & 0xF8) == 0xF0) n = 4; else { *cp = 0; return 1; } if ((size_t)(end - p) < n) n = (size_t)(end - p); u32 c = 0; for (size_t i = 0; i < n; i++) { unsigned char ch = (unsigned char) p[i]; unsigned int mask = (i == 0) ? (n == 1 ? 0x7Fu : n == 2 ? 0x1Fu : n == 3 ? 0x0Fu : 0x07u) : 0x3Fu; c = (c << 6) | (u32)(ch & mask); } *cp = c; return n; } static inline size_t clio_utf8_next(const char *s, u32 *cp) { return clio_utf8_next_n(s, s + strlen(s), cp); } /* Encode a code point into out (at least 5 bytes). -1 on invalid input. */ static inline int clio_utf8_enc(u32 cp, char *out) { size_t n; if (cp < 0x80) n = 1; else if (cp < 0x800) n = 2; else if (cp < 0x10000) n = 3; else if (cp <= 0x10FFFF) n = 4; else return -1; if (cp >= 0xD800 && cp <= 0xDFFF) return -1; unsigned char b[4]; switch (n) { case 1: b[0] = (unsigned char) cp; break; case 2: b[0] = (unsigned char)(0xC0 | (cp >> 6)); b[1] = (unsigned char)(0x80 | (cp & 0x3F)); break; case 3: b[0] = (unsigned char)(0xE0 | (cp >> 12)); b[1] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F)); b[2] = (unsigned char)(0x80 | (cp & 0x3F)); break; default: b[0] = (unsigned char)(0xF0 | (cp >> 18)); b[1] = (unsigned char)(0x80 | ((cp >> 12) & 0x3F)); b[2] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F)); b[3] = (unsigned char)(0x80 | (cp & 0x3F)); break; } memcpy(out, b, n); out[n] = '\0'; return (int) n; } /* Display width of a code point, locale-independent (wcwidth is not: * glibc's C locale reports 0 for CJK, musl reports 2 — we must be the * same everywhere). 0 = zero-width, 2 = wide (East Asian Wide/Fullwidth * + common emoji), 1 = everything else. Simplified W=|F table. */ static inline size_t clio_cp_width(u32 cp) { if (cp == 0 || cp < 0x20 || (cp >= 0x7F && cp <= 0x9F)) return 0; if (cp == 0xAD || cp == 0x200B || cp == 0x200C || cp == 0x200D || cp == 0x2060 || (cp >= 0x300 && cp <= 0x36F)) return 0; if ((cp >= 0x1100 && cp <= 0x115F) || (cp >= 0x2E80 && cp <= 0x303E) || (cp >= 0x3041 && cp <= 0x33FF) || (cp >= 0x3400 && cp <= 0x4DBF) || (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) || (cp >= 0xA960 && cp <= 0xA97F) || (cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFE10 && cp <= 0xFE19) || (cp >= 0xFE30 && cp <= 0xFE6F) || (cp >= 0xFF00 && cp <= 0xFF60) || (cp >= 0xFFE0 && cp <= 0xFFE6) || (cp >= 0x1F300 && cp <= 0x1F64F) || (cp >= 0x1F900 && cp <= 0x1F9FF) || (cp >= 0x20000 && cp <= 0x3FFFD)) return 2; return 1; } static inline size_t clio_text_width_n(const char *s, size_t n) { size_t w = 0; const char *p = s; const char *end = s + n; while (p < end) { u32 cp; size_t len = clio_utf8_next_n(p, end, &cp); if (cp != 0) w += clio_cp_width(cp); p += len; } return w; } static inline size_t clio_text_width(const char *s) { return clio_text_width_n(s, strlen(s)); } /* Byte offset of the start of the code point containing pos. */ static inline size_t clio_char_start(const char *s, size_t pos) { if (pos == 0) return 0; size_t i = pos; while (i > 0 && (((unsigned char) s[i]) & 0xC0) == 0x80) i--; return i; } /* Byte offset of the start of the code point immediately before pos * (pos is a cursor position, i.e. a char boundary). */ static inline size_t clio_char_before(const char *s, size_t pos) { size_t i = pos; if (i == 0) return 0; i--; /* last byte belongs to the previous code point */ while (i > 0 && (((unsigned char) s[i - 1]) & 0xC0) == 0x80) i--; return i; } /* Byte offset just past the code point at pos. */ static inline size_t clio_char_end(const char *s, size_t len, size_t pos) { if (pos >= len) return len; u32 cp; size_t n = clio_utf8_next_n(s + pos, s + len, &cp); return pos + n; } /* Byte offset of the start of the word before pos (ed-style Ctrl+W). */ static inline size_t clio_word_back(const char *s, size_t pos) { size_t i = pos; while (i > 0 && s[i - 1] == ' ') i--; while (i > 0 && (isalnum((unsigned char) s[i - 1]) || s[i - 1] == '_')) i--; while (i > 0 && s[i - 1] == ' ') i--; return i; } /* ------------------------------------------------------------------ */ /* Canvas (cell grid) */ /* ------------------------------------------------------------------ */ /* One column of a row. ch is a code point (0 = blank slot; bg/attr still * apply). fg/bg of 0xFFFF mean "terminal default". A blank default cell * is {0, 0xFFFF, 0xFFFF, 0}. */ typedef struct clio_cell { u32 ch; u16 fg; u16 bg; u16 attr; } clio_cell; typedef struct clio_buf { clio_cell *cells; size_t rows; size_t cols; } clio_buf; static inline u16 clio_style_u16(int v) { return (u16)(v < 0 ? 0xFFFF : (v > 255 ? 255 : v)); } static inline void clio_cell_set(clio_cell *c, u32 ch, clio_style st) { c->ch = ch; c->fg = clio_style_u16(st.fg); c->bg = clio_style_u16(st.bg); c->attr = (u16) st.attr; } static inline clio_style clio_cell_style(const clio_cell *c) { clio_style s; s.fg = (c->fg == 0xFFFF) ? -1 : (int) c->fg; s.bg = (c->bg == 0xFFFF) ? -1 : (int) c->bg; s.attr = c->attr; return s; } static inline int clio_cell_eq(const clio_cell *a, const clio_cell *b) { return memcmp(a, b, sizeof(*a)) == 0; } static inline int clio_buf_init(clio_buf *b, size_t rows, size_t cols) { if (rows == 0) rows = 1; if (cols == 0) cols = 1; b->rows = rows; b->cols = cols; b->cells = (clio_cell *) calloc(rows * cols, sizeof(clio_cell)); if (!b->cells) { b->rows = b->cols = 0; return -1; } for (size_t i = 0; i < rows * cols; i++) { b->cells[i].fg = 0xFFFF; b->cells[i].bg = 0xFFFF; } return 0; } static inline void clio_buf_free(clio_buf *b) { free(b->cells); b->cells = NULL; b->rows = b->cols = 0; } static inline void clio_buf_clear(clio_buf *b) { for (size_t i = 0; i < b->rows * b->cols; i++) { b->cells[i].ch = 0; b->cells[i].attr = 0; b->cells[i].fg = 0xFFFF; b->cells[i].bg = 0xFFFF; } } static inline clio_cell *clio_buf_at(clio_buf *b, size_t row, size_t col) { if (row >= b->rows || col >= b->cols) return NULL; return &b->cells[row * b->cols + col]; } static inline void clio_buf_clear_region(clio_buf *b, size_t r0, size_t c0, size_t r1, size_t c1) { if (r1 < r0 || c1 < c0 || r0 >= b->rows || c0 >= b->cols) return; if (r1 > b->rows - 1) r1 = b->rows - 1; if (c1 > b->cols - 1) c1 = b->cols - 1; for (size_t r = r0; r <= r1; r++) for (size_t c = c0; c <= c1; c++) { clio_cell *cell = &b->cells[r * b->cols + c]; cell->ch = 0; cell->attr = 0; cell->fg = 0xFFFF; cell->bg = 0xFFFF; } } static inline void clio_buf_fill_region(clio_buf *b, size_t r0, size_t c0, size_t r1, size_t c1, u32 ch, clio_style st) { if (r1 < r0 || c1 < c0 || r0 >= b->rows || c0 >= b->cols) return; if (r1 > b->rows - 1) r1 = b->rows - 1; if (c1 > b->cols - 1) c1 = b->cols - 1; for (size_t r = r0; r <= r1; r++) for (size_t c = c0; c <= c1; c++) clio_cell_set(&b->cells[r * b->cols + c], ch, st); } /* Write text at (row, col); wraps onto the next row (when wrap != 0 and * rows remain) and clips at the buffer edges. '\n' moves to the next row * at column 0. Zero-width code points occupy no cell. Returns the number * of bytes written (including newlines). */ static inline size_t clio_buf_put(clio_buf *b, size_t row, size_t col, const char *text, clio_style st, int wrap) { size_t written = 0; size_t len = strlen(text); const char *p = text; while (p < text + len) { u32 cp; size_t n = clio_utf8_next_n(p, text + len, &cp); p += n; if (cp == '\n') { row++; col = 0; if (row >= b->rows) break; written += n; continue; } size_t w = clio_cp_width(cp); if (w == 0) { written += n; continue; } if (col + w > b->cols) { if (!wrap || row + 1 >= b->rows) break; row++; col = 0; } for (size_t i = 0; i < w; i++) { if (col + i >= b->cols) break; clio_cell *cell = &b->cells[row * b->cols + (col + i)]; cell->ch = (i == 0) ? cp : 0; cell->fg = clio_style_u16(st.fg); cell->bg = clio_style_u16(st.bg); cell->attr = (u16) st.attr; } col += w; written += n; } return written; } /* Draw a single-character border (ch, e.g. U+2502) around the rectangle * (r0, c0) with width w and height h. */ static inline void clio_buf_box(clio_buf *b, size_t r0, size_t c0, size_t w, size_t h, u32 ch, clio_style st) { if (r0 >= b->rows || c0 >= b->cols || w == 0 || h == 0) return; if (w > b->cols - c0) w = b->cols - c0; if (h > b->rows - r0) h = b->rows - r0; for (size_t x = 0; x < w; x++) { clio_buf_fill_region(b, r0, c0 + x, r0, c0 + x, ch, st); clio_buf_fill_region(b, r0 + h - 1, c0 + x, r0 + h - 1, c0 + x, ch, st); } for (size_t y = 1; y + 1 < h; y++) { clio_buf_fill_region(b, r0 + y, c0, r0 + y, c0, ch, st); clio_buf_fill_region(b, r0 + y, c0 + w - 1, r0 + y, c0 + w - 1, ch, st); } } /* Shift rows [r0, r1) up by one row within columns [c0, c1]; the bottom * row becomes blank. */ static inline void clio_buf_scroll_up(clio_buf *b, size_t r0, size_t c0, size_t r1, size_t c1) { if (r1 <= r0 || r0 >= b->rows || c0 >= b->cols) return; if (r1 > b->rows) r1 = b->rows; if (c1 > b->cols - 1) c1 = b->cols - 1; for (size_t r = r0; r + 1 <= r1; r++) memcpy(&b->cells[r * b->cols + c0], &b->cells[(r + 1) * b->cols + c0], (c1 - c0 + 1) * sizeof(clio_cell)); for (size_t c = c0; c <= c1; c++) { clio_cell *cell = &b->cells[r1 * b->cols + c]; cell->ch = 0; cell->attr = 0; cell->fg = 0xFFFF; cell->bg = 0xFFFF; } } /* ------------------------------------------------------------------ */ /* Key types + pure decoder */ /* ------------------------------------------------------------------ */ typedef struct clio_key { int kind; u32 cp; /* CLIOK_CHAR: the code point */ } clio_key; enum { CLIOK_UNKNOWN = 0, CLIOK_CHAR, CLIOK_ENTER, CLIOK_TAB, CLIOK_BACKTAB, CLIOK_BACKSPACE, CLIOK_DELETE, CLIOK_LEFT, CLIOK_RIGHT, CLIOK_UP, CLIOK_DOWN, CLIOK_HOME, CLIOK_END, CLIOK_PAGEUP, CLIOK_PAGEDOWN, CLIOK_ESC, CLIOK_CTRL_A, CLIOK_CTRL_C, CLIOK_CTRL_D, CLIOK_CTRL_E, CLIOK_CTRL_K, CLIOK_CTRL_L, CLIOK_CTRL_U, CLIOK_CTRL_W, CLIOK_CTRL_Y, CLIOK_CTRL_Z, CLIOK_CTRL_G, }; /* State machine for decoding raw terminal input into clio_key values. * Pure: no I/O, no allocation. Feed chunks of any size (byte boundaries * may split UTF-8 or escape sequences); state persists across feeds. */ typedef struct clio_keydec { int state; /* 0 ground, 1 escape, 2 csi, 3 ss3 */ int utf8_n; int utf8_need; u32 utf8[4]; int csi_n; unsigned char csi[32]; int pasting; } clio_keydec; static inline void clio_keydec_init(clio_keydec *d) { memset(d, 0, sizeof *d); } static inline int clio_kd_ground(clio_keydec *d, unsigned char b, clio_key *k) { if (b == 0x1B) { d->state = 1; return 0; } if (b == 0x0D) { k->kind = CLIOK_ENTER; k->cp = 0; return 1; } if (b == 0x0A) { if (d->pasting) { k->kind = CLIOK_CHAR; k->cp = '\n'; return 1; } return 0; /* bare LF outside paste: dead key */ } if (b == 0x7F || b == 0x08) { k->kind = CLIOK_BACKSPACE; k->cp = 0; return 1; } if (b == 0x09) { k->kind = CLIOK_TAB; k->cp = 0; return 1; } if (b < 0x20) { switch (b) { case 0x01u: k->kind = CLIOK_CTRL_A; break; case 0x03u: k->kind = CLIOK_CTRL_C; d->pasting = 0; /* Ctrl+C interrupts a dangling paste */ break; case 0x04u: k->kind = CLIOK_CTRL_D; break; case 0x05u: k->kind = CLIOK_CTRL_E; break; case 0x0Bu: k->kind = CLIOK_CTRL_K; break; case 0x0Cu: k->kind = CLIOK_CTRL_L; break; case 0x15u: k->kind = CLIOK_CTRL_U; break; case 0x17u: k->kind = CLIOK_CTRL_W; break; case 0x19u: k->kind = CLIOK_CTRL_Y; break; case 0x1Au: k->kind = CLIOK_CTRL_Z; break; case 0x07u: k->kind = CLIOK_CTRL_G; break; default: k->kind = CLIOK_UNKNOWN; break; } k->cp = 0; return 1; } if (b < 0x80) { k->kind = CLIOK_CHAR; k->cp = b; return 1; } if (b >= 0xC0 && b < 0xF8) { d->utf8_need = (b >= 0xF0) ? 4 : (b >= 0xE0) ? 3 : 2; d->utf8[0] = b; d->utf8_n = 1; return 0; } if (d->utf8_n > 0 && b >= 0x80 && b < 0xC0) { d->utf8[d->utf8_n++] = b; if (d->utf8_n < d->utf8_need) return 0; int n = d->utf8_n; d->utf8_n = 0; u32 cp = d->utf8[0] & (n == 2 ? 0x1Fu : n == 3 ? 0x0Fu : 0x07u); for (int i = 1; i < n; i++) cp = (cp << 6) | (d->utf8[i] & 0x3Fu); k->kind = CLIOK_CHAR; k->cp = cp; return 1; } d->utf8_n = 0; k->kind = CLIOK_UNKNOWN; k->cp = b; return 1; } static inline int clio_kd_escape(clio_keydec *d, unsigned char b, clio_key *k) { d->state = 0; if (b == '[') { d->state = 2; d->csi_n = 0; return 0; } if (b == 'O') { d->state = 3; return 0; } if (b >= 0x20 && b <= 0x7E) { k->kind = CLIOK_CHAR; k->cp = b; return 1; } k->kind = CLIOK_ESC; k->cp = 0; return 1; } static inline int clio_kd_csi(clio_keydec *d, unsigned char b, clio_key *k) { if (d->csi_n < (int) sizeof d->csi) d->csi[d->csi_n++] = b; if ((b >= '0' && b <= '9') || b == ';' || b == '?' || b == '<' || b == '=' || b == '>') return 0; d->state = 0; unsigned long num = 0; int any = 0; for (int i = 0; i < d->csi_n - 1; i++) { unsigned char p = d->csi[i]; if (p < '0' || p > '9') break; if (any) num = num * 10 + (unsigned long)(p - '0'); else num = (unsigned long)(p - '0'); any = 1; if (num > 0xFFFFu) break; } d->csi_n = 0; switch (b) { case 'A': k->kind = CLIOK_UP; k->cp = 0; return 1; case 'B': k->kind = CLIOK_DOWN; k->cp = 0; return 1; case 'C': k->kind = CLIOK_RIGHT; k->cp = 0; return 1; case 'D': k->kind = CLIOK_LEFT; k->cp = 0; return 1; case 'H': k->kind = CLIOK_HOME; k->cp = 0; return 1; case 'F': k->kind = CLIOK_END; k->cp = 0; return 1; case 'Z': k->kind = CLIOK_BACKTAB; k->cp = 0; return 1; case '~': switch (num) { case 1u: case 7u: k->kind = CLIOK_HOME; break; case 3u: k->kind = CLIOK_DELETE; break; case 4u: case 8u: k->kind = CLIOK_END; break; case 5u: k->kind = CLIOK_PAGEUP; break; case 6u: k->kind = CLIOK_PAGEDOWN; break; case 200u: d->pasting = 1; return 0; case 201u: d->pasting = 0; return 0; default: k->kind = CLIOK_UNKNOWN; break; } k->cp = 0; return 1; case 'u': /* kitty keyboard protocol */ switch (num) { case 9u: k->kind = CLIOK_TAB; break; case 12u: k->kind = CLIOK_BACKSPACE; break; case 13u: k->kind = CLIOK_ENTER; break; case 27u: k->kind = CLIOK_ESC; break; case 127u: k->kind = CLIOK_DELETE; break; case 0x2190u: k->kind = CLIOK_LEFT; break; case 0x2191u: k->kind = CLIOK_UP; break; case 0x2192u: k->kind = CLIOK_RIGHT; break; case 0x2193u: k->kind = CLIOK_DOWN; break; default: k->cp = 0; return 0; } k->cp = 0; return 1; case 'R': /* cursor position report: swallow */ case 'M': case 'h': case 'l': case 'm': return 0; default: k->kind = CLIOK_UNKNOWN; k->cp = 0; return 1; } } static inline int clio_kd_ss3(clio_keydec *d, unsigned char b, clio_key *k) { d->state = 0; switch (b) { case 'A': k->kind = CLIOK_UP; break; case 'B': k->kind = CLIOK_DOWN; break; case 'C': k->kind = CLIOK_RIGHT; break; case 'D': k->kind = CLIOK_LEFT; break; case 'H': k->kind = CLIOK_HOME; break; case 'F': k->kind = CLIOK_END; break; default: k->kind = CLIOK_UNKNOWN; break; } k->cp = 0; return 1; } static inline int clio_kd_step(clio_keydec *d, unsigned char b, clio_key *k) { switch (d->state) { case 1: return clio_kd_escape(d, b, k); case 2: return clio_kd_csi(d, b, k); case 3: return clio_kd_ss3(d, b, k); default: return clio_kd_ground(d, b, k); } } /* Feed raw bytes; emits up to cap decoded keys into out; returns the * number emitted. */ static inline size_t clio_keydec_feed(clio_keydec *d, const unsigned char *in, size_t n, clio_key *out, size_t cap) { size_t emitted = 0; for (size_t i = 0; i < n; i++) { clio_key k; if (clio_kd_step(d, in[i], &k) && emitted < cap) out[emitted++] = k; } return emitted; } /* Emit a trailing bare ESC (state 1) as a key. 1 if emitted. */ static inline int clio_keydec_flush(clio_keydec *d, clio_key *k) { if (d->state != 1) return 0; d->state = 0; k->kind = CLIOK_ESC; k->cp = 0; return 1; } /* ------------------------------------------------------------------ */ /* Terminal state */ /* ------------------------------------------------------------------ */ #define CLIO_KEYQ 32 typedef struct clio_term { int is_tty; int color; /* clio_color_policy() result */ size_t rows; size_t cols; int raw; struct termios saved; int alt; int cursor_hidden; int paste; int kitty; clio_keydec dec; clio_key q[CLIO_KEYQ]; size_t qh; size_t qt; clio_buf last; int has_last; } clio_term; /* CLICOLOR_FORCE (non-empty, not "0") wins; otherwise color only on a tty * without NO_COLOR, TERM=dumb, or CI. */ static inline int clio_color_policy(int tty) { const char *force = getenv("CLICOLOR_FORCE"); if (force && force[0]) { if (strcmp(force, "0") == 0) return 0; return 1; } if (!tty) return 0; if (getenv("NO_COLOR")) return 0; const char *term = getenv("TERM"); if (term && strcmp(term, "dumb") == 0) return 0; if (getenv("CI")) return 0; return 1; } static inline int clio_term_init(clio_term *t) { memset(t, 0, sizeof *t); t->is_tty = isatty(STDIN_FILENO) != 0 && isatty(STDOUT_FILENO) != 0; t->color = clio_color_policy(t->is_tty); t->rows = 24; t->cols = 80; struct winsize ws; if (t->is_tty && ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0 && ws.ws_col > 0) { t->rows = (size_t) ws.ws_row; t->cols = (size_t) ws.ws_col; } clio_keydec_init(&t->dec); return 0; } /* Re-read the window size; invalidates the flushed-frame cache. -1 on * failure (previous size kept). */ static inline int clio_term_resize(clio_term *t) { struct winsize ws; if (!t->is_tty) return -1; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) return -1; if (ws.ws_row == 0 || ws.ws_col == 0) return -1; t->rows = (size_t) ws.ws_row; t->cols = (size_t) ws.ws_col; clio_buf_free(&t->last); t->has_last = 0; return 0; } static inline int clio_term_raw_on(clio_term *t) { if (t->raw || !t->is_tty) return -1; if (tcgetattr(STDIN_FILENO, &t->saved) != 0) return -1; struct termios raw = t->saved; raw.c_iflag &= ~((tcflag_t)(BRKINT | ICRNL | INPCK | ISTRIP | IXON)); raw.c_lflag &= ~((tcflag_t)(ECHO | ICANON | IEXTEN | ISIG)); raw.c_oflag &= ~((tcflag_t) OPOST); raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) != 0) return -1; t->raw = 1; return 0; } static inline void clio_term_raw_off(clio_term *t) { if (!t->raw) return; tcsetattr(STDIN_FILENO, TCSANOW, &t->saved); t->raw = 0; } static inline void clio_term_alt_on(clio_term *t) { if (t->alt) return; esc_falt_screen_on(stdout); fflush(stdout); t->alt = 1; clio_buf_free(&t->last); t->has_last = 0; } static inline void clio_term_alt_off(clio_term *t) { if (!t->alt) return; esc_falt_screen_off(stdout); fflush(stdout); t->alt = 0; clio_buf_free(&t->last); t->has_last = 0; } static inline void clio_term_cursor_hide(clio_term *t) { if (t->cursor_hidden) return; esc_fcursor_hide(stdout); fflush(stdout); t->cursor_hidden = 1; } static inline void clio_term_cursor_show(clio_term *t) { if (!t->cursor_hidden) return; esc_fcursor_show(stdout); fflush(stdout); t->cursor_hidden = 0; } static inline void clio_term_paste_on(clio_term *t) { if (t->paste) return; esc_fbracketed_paste_on(stdout); fflush(stdout); t->paste = 1; t->dec.pasting = 1; } static inline void clio_term_paste_off(clio_term *t) { if (!t->paste) return; esc_fbracketed_paste_off(stdout); fflush(stdout); t->paste = 0; t->dec.pasting = 0; } static inline void clio_term_kitty_on(clio_term *t) { if (t->kitty) return; esc_fkitty_on(stdout); fflush(stdout); t->kitty = 1; } static inline void clio_term_kitty_off(clio_term *t) { if (!t->kitty) return; esc_fkitty_off(stdout); fflush(stdout); t->kitty = 0; } static inline void clio_term_bell(clio_term *t) { (void) t; esc_fbell(stdout); fflush(stdout); } /* Restore everything clio_term turned on (idempotent). */ static inline void clio_term_shutdown(clio_term *t) { if (t->kitty) clio_term_kitty_off(t); if (t->paste) clio_term_paste_off(t); if (t->cursor_hidden) clio_term_cursor_show(t); if (t->alt) clio_term_alt_off(t); clio_term_raw_off(t); } static inline void clio_term_free(clio_term *t) { clio_term_shutdown(t); clio_buf_free(&t->last); memset(t, 0, sizeof *t); } /* ------------------------------------------------------------------ */ /* Output (diff flush) */ /* ------------------------------------------------------------------ */ /* Map a style onto the stream: reset, then attributes, then 256-color * fg/bg. escape.h emits the codes; clio.h only decides what goes. */ static inline void clio_fstyle(FILE *f, clio_style st) { if (!f) return; fputs(ESC_RESET, f); if (st.attr & CLIO_BOLD) fputs(ESC_BOLD, f); if (st.attr & CLIO_DIM) fputs(ESC_DIM, f); if (st.attr & CLIO_ITALIC) fputs(ESC_ITALIC, f); if (st.attr & CLIO_UNDERLINE) fputs(ESC_UNDERLINE, f); if (st.attr & CLIO_REVERSE) fputs(ESC_REVERSE, f); if (st.attr & CLIO_STRIKE) fputs(ESC_STRIKETHROUGH, f); if (st.fg >= 0) esc_fcolor_fg_256(f, (u32)(st.fg & 0xFF)); if (st.bg >= 0) esc_fcolor_bg_256(f, (u32)(st.bg & 0xFF)); } /* Render one row: style runs + text. Blank cells print as spaces. */ static inline void clio_emit_row(FILE *f, const clio_buf *b, size_t row) { clio_style cur = clio_style_default(); for (size_t c = 0; c < b->cols; c++) { const clio_cell *cell = &b->cells[row * b->cols + c]; clio_style st = clio_cell_style(cell); if (!clio_style_eq(&st, &cur)) { clio_fstyle(f, st); cur = st; } if (cell->ch == 0) { fputc(' ', f); } else if (cell->ch < 0x80) { fputc((int)(cell->ch & 0xFF), f); } else { char tmp[8]; if (clio_utf8_enc(cell->ch, tmp) > 0) fputs(tmp, f); else fputc(' ', f); } } } /* Non-tty fallback: plain text lines, trailing blanks trimmed. */ static inline void clio_flush_plain(const clio_buf *b) { size_t cap = b->cols * 4 + 1; char *row = (char *) malloc(cap); if (!row) return; for (size_t r = 0; r < b->rows; r++) { size_t n = 0; for (size_t c = 0; c < b->cols; c++) { const clio_cell *cell = &b->cells[r * b->cols + c]; if (cell->ch == 0) { row[n++] = ' '; } else if (cell->ch < 0x80) { row[n++] = (char)(cell->ch & 0xFF); } else { int e = clio_utf8_enc(cell->ch, row + n); if (e > 0) n += (size_t) e; else row[n++] = ' '; } } while (n > 0 && row[n - 1] == ' ') n--; fwrite(row, 1, n, stdout); fputc('\n', stdout); } free(row); fflush(stdout); } /* Blit the canvas to the terminal. TTY: diff against the previous frame, * clear screen + full repaint on first flush or size change, otherwise * only changed rows. Non-tty: plain text lines (no escapes, safe to pipe). */ static inline void clio_flush(clio_term *t, const clio_buf *b) { if (!b || !b->cells) return; if (!t->is_tty) { clio_flush_plain(b); return; } int full = 1; if (t->has_last && t->last.rows == b->rows && t->last.cols == b->cols) full = 0; if (full) { clio_buf_free(&t->last); if (clio_buf_init(&t->last, b->rows, b->cols) != 0) { t->has_last = 0; return; } t->has_last = 1; esc_fclear_screen(stdout); for (size_t r = 0; r < b->rows; r++) { esc_fcursor_position(stdout, 1, (u32)(r + 1)); clio_emit_row(stdout, b, r); } } else { for (size_t r = 0; r < b->rows; r++) { int same = 1; for (size_t c = 0; c < b->cols; c++) { if (!clio_cell_eq(&t->last.cells[r * b->cols + c], &b->cells[r * b->cols + c])) { same = 0; break; } } if (same) continue; esc_fcursor_position(stdout, 1, (u32)(r + 1)); clio_emit_row(stdout, b, r); } } memcpy(t->last.cells, b->cells, b->rows * b->cols * sizeof(clio_cell)); fflush(stdout); } /* Move the visible cursor (0-based row/col, clamped to the terminal). */ static inline void clio_cursor(clio_term *t, size_t row, size_t col) { if (!t->is_tty) return; if (row > t->rows) row = t->rows; if (col > t->cols) col = t->cols; esc_fcursor_position(stdout, (u32)(col + 1), (u32)(row + 1)); fflush(stdout); } /* ------------------------------------------------------------------ */ /* Key input */ /* ------------------------------------------------------------------ */ /* Key source: 1 + *k set on success, 0 on timeout/EOF. */ typedef int (*clio_next_key)(void *ud, clio_key *k, int timeout_ms); static inline int clio_read_key(clio_term *t, clio_key *out, int timeout_ms) { for (;;) { if (t->qt != t->qh) { *out = t->q[t->qh]; t->qh++; if (t->qh == CLIO_KEYQ) t->qh = 0; return 1; } if (timeout_ms == 0) return clio_keydec_flush(&t->dec, out); struct pollfd pfd; pfd.fd = STDIN_FILENO; pfd.events = POLLIN; pfd.revents = 0; int pr = poll(&pfd, 1, timeout_ms); if (pr <= 0) return clio_keydec_flush(&t->dec, out); unsigned char in[16]; ssize_t n = read(STDIN_FILENO, in, sizeof in); if (n <= 0) return clio_keydec_flush(&t->dec, out); clio_key tmp[16]; size_t got = clio_keydec_feed(&t->dec, in, (size_t) n, tmp, 16); for (size_t i = 0; i < got; i++) { if (t->qt == t->qh) return clio_keydec_flush(&t->dec, out); size_t slot = t->qt; if (slot == CLIO_KEYQ) slot = 0; if (slot == t->qh) { t->qh++; if (t->qh == CLIO_KEYQ) t->qh = 0; } /* queue full: drop the oldest */ t->q[slot] = tmp[i]; t->qt = (slot + 1 == CLIO_KEYQ) ? 0 : slot + 1; } } } static inline int clio_next_key_stdin(void *ud, clio_key *k, int timeout_ms) { clio_term *t = (clio_term *) ud; return clio_read_key(t, k, timeout_ms); } /* ------------------------------------------------------------------ */ /* Single-line editor */ /* ------------------------------------------------------------------ */ static inline void *clio_xrealloc(void *p, size_t n) { void *q = realloc(p, n ? n : 1); if (!q) { fprintf(stderr, "clio: out of memory\n"); abort(); } return q; } typedef struct clio_str { char *s; size_t len; size_t cap; } clio_str; static inline void clio_str_init(clio_str *x) { x->s = NULL; x->len = 0; x->cap = 0; } static inline void clio_str_free(clio_str *x) { free(x->s); x->s = NULL; x->len = x->cap = 0; } static inline void clio_str_ins(clio_str *x, size_t at, const char *s, size_t n) { if (x->len + n + 1 > x->cap) { size_t nc = x->cap ? x->cap : 16; while (x->len + n + 1 > nc) nc *= 2; x->s = (char *) clio_xrealloc(x->s, nc); x->cap = nc; } memmove(x->s + at + n, x->s + at, x->len - at); memcpy(x->s + at, s, n); x->len += n; x->s[x->len] = '\0'; } static inline void clio_str_del(clio_str *x, size_t at, size_t n) { if (at > x->len) at = x->len; if (at + n > x->len) n = x->len - at; memmove(x->s + at, x->s + at + n, x->len - at - n); x->len -= n; x->s[x->len] = '\0'; } /* Count visible (non-zero-width) code points in the first n bytes. */ static inline size_t clio_visible_n(const char *s, size_t n) { size_t c = 0; const char *p = s; const char *end = s + n; while (p < end) { u32 cp; size_t len = clio_utf8_next_n(p, end, &cp); if (cp != 0 && clio_cp_width(cp) > 0) c++; p += len; } return c; } /* Paint the prompt + text (echo, or one '*' per visible code point) into * the canvas at row; returns the caret column. * * The input is ONE visual line: it never wraps. When the caret passes the * right edge the window slides (bash/readline style) so the caret always * stays visible, so backspace/left-arrow work across the "wrap". */ static inline size_t clio_readline_paint(clio_buf *b, size_t row, const char *prompt, clio_style pstyle, const clio_str *txt, size_t caret, clio_style tstyle, int echo) { clio_buf_clear_region(b, row, 0, row, b->cols - 1); size_t pw = prompt ? clio_text_width(prompt) : 0; if (prompt && prompt[0]) clio_buf_put(b, row, 0, prompt, pstyle, 0); size_t area = (pw < b->cols) ? b->cols - pw : 0; if (txt->len == 0 || area == 0) return pw; if (echo) { size_t cw = clio_text_width_n(txt->s, caret); /* Window left edge (display cells): 0 unless the caret does not * fit, in which case pin the caret to the last column. */ size_t L = (cw + 1 > area) ? cw - (area - 1) : 0; /* Largest char boundary <= L (a wide char straddling L is kept * whole, window starts at it). */ size_t off = 0, start = 0; const char *p = txt->s; const char *end = txt->s + txt->len; while (p < end) { u32 cp; size_t n = clio_utf8_next_n(p, end, &cp); size_t w = (cp != 0) ? clio_cp_width(cp) : 0; if (off + w > L) { start = (size_t)(p - txt->s); /* `off` is the last boundary <= L */ break; } off += w; p += n; } clio_buf_put(b, row, pw, txt->s + start, tstyle, 0); size_t c = pw + (cw - off); if (c >= b->cols) /* wide char straddled the left edge */ c = b->cols - 1; return c; } size_t total = clio_visible_n(txt->s, txt->len); size_t cw = clio_visible_n(txt->s, caret); size_t first = (cw + 1 > area) ? cw - (area - 1) : 0; for (size_t i = first; i < total; i++) clio_buf_put(b, row, pw + (i - first), "*", tstyle, 0); return pw + (cw - first); } /* Single-line editor. Renders into ONE canvas row (no wrapping; long * input scrolls to keep the caret visible); reads keys from nk (NULL * = clio_next_key_stdin with ud = t). Editing: printable insert, Tab, * backspace/delete, left/right/home/end, Ctrl+A/E/U/K/W, Ctrl+C and ESC * cancel (NULL), Ctrl+D deletes (EOF/submit when empty), Ctrl+L repaints, * Enter submits. Returns the input (caller frees) or NULL. */ static inline char *clio_readline(clio_term *t, clio_buf *b, size_t row, const char *prompt, clio_style pstyle, clio_style tstyle, int echo, clio_next_key nk, void *ud, int timeout_ms) { if (!nk) { nk = clio_next_key_stdin; ud = t; } clio_str txt; clio_str_init(&txt); size_t caret = 0; size_t col = clio_readline_paint(b, row, prompt, pstyle, &txt, caret, tstyle, echo); if (t->is_tty) clio_cursor(t, row, col); int quit = 0; for (;;) { clio_key k; if (!nk(ud, &k, timeout_ms)) break; /* timeout / EOF / no key */ int done = 0; switch (k.kind) { case CLIOK_CHAR: { char tmp[8]; int e = clio_utf8_enc(k.cp, tmp); if (e > 0) { clio_str_ins(&txt, caret, tmp, (size_t) e); caret += (size_t) e; } break; } case CLIOK_TAB: clio_str_ins(&txt, caret, "\t", 1); caret++; break; case CLIOK_ENTER: done = 1; break; case CLIOK_BACKSPACE: if (caret > 0) { size_t s = clio_char_before(txt.s, caret); clio_str_del(&txt, s, caret - s); caret = s; } break; case CLIOK_DELETE: if (caret < txt.len) clio_str_del(&txt, caret, clio_char_end(txt.s, txt.len, caret) - caret); break; case CLIOK_LEFT: if (caret > 0) caret = clio_char_before(txt.s, caret); break; case CLIOK_RIGHT: if (caret < txt.len) caret = clio_char_end(txt.s, txt.len, caret); break; case CLIOK_HOME: case CLIOK_CTRL_A: caret = 0; break; case CLIOK_END: case CLIOK_CTRL_E: caret = txt.len; break; case CLIOK_CTRL_U: clio_str_del(&txt, 0, caret); caret = 0; break; case CLIOK_CTRL_K: clio_str_del(&txt, caret, txt.len - caret); break; case CLIOK_CTRL_W: { size_t s = clio_word_back(txt.s, caret); clio_str_del(&txt, s, caret - s); caret = s; break; } case CLIOK_CTRL_C: quit = 1; break; case CLIOK_CTRL_D: if (txt.len == 0) done = 1; else if (caret < txt.len) clio_str_del(&txt, caret, clio_char_end(txt.s, txt.len, caret) - caret); break; case CLIOK_CTRL_L: if (t->is_tty) { esc_fclear_screen(stdout); esc_fcursor_position(stdout, 1, 1); fflush(stdout); } break; case CLIOK_ESC: quit = 1; break; default: break; } col = clio_readline_paint(b, row, prompt, pstyle, &txt, caret, tstyle, echo); if (t->is_tty) clio_cursor(t, row, col); if (done || quit) break; } if (quit || txt.len == 0) { clio_str_free(&txt); return NULL; /* cancelled, or submitted empty */ } return txt.s; /* caller frees */ } #ifdef __cplusplus } #endif #endif /* CLIO_H */