From c99fc8f50508a7f733c0a658c5c2bb9df5990e61 Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:07:21 +0700 Subject: [PATCH 1/7] feat(compat): drop-in termbox2.h mapping the supported tb_/TB_ surface onto libterm --- compat/termbox2.h | 268 +++++++++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 8 ++ tests/compat_smoke.c | 39 +++++++ 3 files changed, 315 insertions(+) create mode 100644 compat/termbox2.h create mode 100644 tests/compat_smoke.c diff --git a/compat/termbox2.h b/compat/termbox2.h new file mode 100644 index 0000000..2695938 --- /dev/null +++ b/compat/termbox2.h @@ -0,0 +1,268 @@ +/* + * compat/termbox2.h — drop-in termbox2 compatibility layer for libterm. + * + * Replace `#include "termbox2.h"` with this header and link against libterm. + * It maps the termbox2 public API (tb_/TB_) onto libterm (lt_/LT_). + * + * SUPPORTED: every termbox2 public function with a libterm equivalent; all + * TB_KEY_* / TB_MOD_* / TB_EVENT_* / TB_OUTPUT_* / TB_INPUT_* constants, + * colors, attributes, and return codes; struct tb_cell / tb_event; + * uintattr_t. + * + * INPUT SEMANTICS: tb_init uses libterm's modern key model by default (single + * Esc event; Ctrl+letter as ch+mod where the terminal supports it). For exact + * termbox2 key semantics (two-event Esc/Alt, control-byte-in-key), call + * tb_set_input_mode(LT_INPUT_COMPAT); + * after tb_init. + * + * UNSUPPORTED (using these is a compile error naming the libterm alternative): + * tb_init_rwfd -> lt_init_fd / lt_init_file (libterm is single-fd) + * tb_set_func -> (none; deprecated upstream) + * tb_has_truecolor -> lt_detect_color_depth + * tb_cell_buffer -> lt_get_cell (no raw back-buffer pointer) + * tb_key_i -> (none; no terminfo cap table) + * + * tb_get_cell CAVEATS: returns a pointer to an internal snapshot valid only + * until the next tb_get_cell call (libterm copies; it does not expose a live + * buffer pointer); reads the BACK buffer only, so back==0 (front) -> LT_ERR. + * + * NOTE: struct tb_cell IS libterm's struct lt_cell (16-byte POD: ch/fg/bg + + * opaque _reserved). termbox2's per-cell ech/nech/cech EGC pointers are not + * present; use lt_set_cell_ex / lt_extend_cell for grapheme clusters. + */ +#ifndef LIBTERM_COMPAT_TERMBOX2_H +#define LIBTERM_COMPAT_TERMBOX2_H + +#include "libterm/libterm.h" + +#include /* for the tb_malloc/realloc/free aliases */ + +/* ---- types: alias the struct TAGS (a typedef would leave `struct tb_cell` + * a separate incomplete type and break tb_get_cell/tb_poll_event). ---- */ +#define tb_cell lt_cell +#define tb_event lt_event +typedef lt_attr uintattr_t; + +/* ---- return codes ---- */ +#define TB_OK LT_OK +#define TB_ERR LT_ERR +#define TB_ERR_NEED_MORE LT_ERR_NEED_MORE +#define TB_ERR_INIT_ALREADY LT_ERR_INIT_ALREADY +#define TB_ERR_INIT_OPEN LT_ERR_INIT_OPEN +#define TB_ERR_MEM LT_ERR_MEM +#define TB_ERR_NO_EVENT LT_ERR_NO_EVENT +#define TB_ERR_NO_TERM LT_ERR_NO_TERM +#define TB_ERR_NOT_INIT LT_ERR_NOT_INIT +#define TB_ERR_OUT_OF_BOUNDS LT_ERR_OUT_OF_BOUNDS +#define TB_ERR_READ LT_ERR_READ +#define TB_ERR_RESIZE_IOCTL LT_ERR_RESIZE_IOCTL +#define TB_ERR_RESIZE_PIPE LT_ERR_RESIZE_PIPE +#define TB_ERR_RESIZE_SIGACTION LT_ERR_RESIZE_SIGACTION +#define TB_ERR_POLL LT_ERR_POLL +#define TB_ERR_TCGETATTR LT_ERR_TCGETATTR +#define TB_ERR_TCSETATTR LT_ERR_TCSETATTR +#define TB_ERR_UNSUPPORTED_TERM LT_ERR_UNSUPPORTED_TERM +#define TB_ERR_RESIZE_WRITE LT_ERR_RESIZE_WRITE +#define TB_ERR_RESIZE_POLL LT_ERR_RESIZE_POLL +#define TB_ERR_RESIZE_READ LT_ERR_RESIZE_READ +#define TB_ERR_RESIZE_SSCANF LT_ERR_RESIZE_SSCANF +#define TB_ERR_CAP_COLLISION LT_ERR_CAP_COLLISION + +/* ---- event types ---- */ +#define TB_EVENT_KEY LT_EVENT_KEY +#define TB_EVENT_RESIZE LT_EVENT_RESIZE +#define TB_EVENT_MOUSE LT_EVENT_MOUSE + +/* ---- modifiers ---- */ +#define TB_MOD_ALT LT_MOD_ALT +#define TB_MOD_CTRL LT_MOD_CTRL +#define TB_MOD_SHIFT LT_MOD_SHIFT +#define TB_MOD_MOTION LT_MOD_MOTION + +/* ---- input modes ---- */ +#define TB_INPUT_CURRENT LT_INPUT_CURRENT +#define TB_INPUT_ESC LT_INPUT_ESC +#define TB_INPUT_ALT LT_INPUT_ALT +#define TB_INPUT_MOUSE LT_INPUT_MOUSE + +/* ---- output modes ---- */ +#define TB_OUTPUT_CURRENT LT_OUTPUT_CURRENT +#define TB_OUTPUT_NORMAL LT_OUTPUT_NORMAL +#define TB_OUTPUT_256 LT_OUTPUT_256 +#define TB_OUTPUT_216 LT_OUTPUT_216 +#define TB_OUTPUT_GRAYSCALE LT_OUTPUT_GRAYSCALE +#define TB_OUTPUT_TRUECOLOR LT_OUTPUT_TRUECOLOR + +/* ---- colors ---- */ +#define TB_DEFAULT LT_DEFAULT +#define TB_BLACK LT_BLACK +#define TB_RED LT_RED +#define TB_GREEN LT_GREEN +#define TB_YELLOW LT_YELLOW +#define TB_BLUE LT_BLUE +#define TB_MAGENTA LT_MAGENTA +#define TB_CYAN LT_CYAN +#define TB_WHITE LT_WHITE +#define TB_RGB(r, g, b) LT_RGB((r), (g), (b)) + +/* ---- attributes ---- */ +#define TB_BOLD LT_BOLD +#define TB_UNDERLINE LT_UNDERLINE +#define TB_REVERSE LT_REVERSE +#define TB_ITALIC LT_ITALIC +#define TB_BLINK LT_BLINK +#define TB_DIM LT_DIM +#define TB_STRIKE LT_STRIKE + +/* ---- keys (full termbox2 set; each has an LT_KEY_ twin) ---- */ +#define TB_KEY_CTRL_TILDE LT_KEY_CTRL_TILDE +#define TB_KEY_CTRL_2 LT_KEY_CTRL_2 +#define TB_KEY_CTRL_A LT_KEY_CTRL_A +#define TB_KEY_CTRL_B LT_KEY_CTRL_B +#define TB_KEY_CTRL_C LT_KEY_CTRL_C +#define TB_KEY_CTRL_D LT_KEY_CTRL_D +#define TB_KEY_CTRL_E LT_KEY_CTRL_E +#define TB_KEY_CTRL_F LT_KEY_CTRL_F +#define TB_KEY_CTRL_G LT_KEY_CTRL_G +#define TB_KEY_BACKSPACE LT_KEY_BACKSPACE +#define TB_KEY_CTRL_H LT_KEY_CTRL_H +#define TB_KEY_TAB LT_KEY_TAB +#define TB_KEY_CTRL_I LT_KEY_CTRL_I +#define TB_KEY_CTRL_J LT_KEY_CTRL_J +#define TB_KEY_CTRL_K LT_KEY_CTRL_K +#define TB_KEY_CTRL_L LT_KEY_CTRL_L +#define TB_KEY_ENTER LT_KEY_ENTER +#define TB_KEY_CTRL_M LT_KEY_CTRL_M +#define TB_KEY_CTRL_N LT_KEY_CTRL_N +#define TB_KEY_CTRL_O LT_KEY_CTRL_O +#define TB_KEY_CTRL_P LT_KEY_CTRL_P +#define TB_KEY_CTRL_Q LT_KEY_CTRL_Q +#define TB_KEY_CTRL_R LT_KEY_CTRL_R +#define TB_KEY_CTRL_S LT_KEY_CTRL_S +#define TB_KEY_CTRL_T LT_KEY_CTRL_T +#define TB_KEY_CTRL_U LT_KEY_CTRL_U +#define TB_KEY_CTRL_V LT_KEY_CTRL_V +#define TB_KEY_CTRL_W LT_KEY_CTRL_W +#define TB_KEY_CTRL_X LT_KEY_CTRL_X +#define TB_KEY_CTRL_Y LT_KEY_CTRL_Y +#define TB_KEY_CTRL_Z LT_KEY_CTRL_Z +#define TB_KEY_ESC LT_KEY_ESC +#define TB_KEY_CTRL_LSQ_BRACKET LT_KEY_CTRL_LSQ_BRACKET +#define TB_KEY_CTRL_3 LT_KEY_CTRL_3 +#define TB_KEY_CTRL_4 LT_KEY_CTRL_4 +#define TB_KEY_CTRL_BACKSLASH LT_KEY_CTRL_BACKSLASH +#define TB_KEY_CTRL_5 LT_KEY_CTRL_5 +#define TB_KEY_CTRL_RSQ_BRACKET LT_KEY_CTRL_RSQ_BRACKET +#define TB_KEY_CTRL_6 LT_KEY_CTRL_6 +#define TB_KEY_CTRL_7 LT_KEY_CTRL_7 +#define TB_KEY_CTRL_SLASH LT_KEY_CTRL_SLASH +#define TB_KEY_CTRL_UNDERSCORE LT_KEY_CTRL_UNDERSCORE +#define TB_KEY_SPACE LT_KEY_SPACE +#define TB_KEY_BACKSPACE2 LT_KEY_BACKSPACE2 +#define TB_KEY_CTRL_8 LT_KEY_CTRL_8 +#define TB_KEY_F1 LT_KEY_F1 +#define TB_KEY_F2 LT_KEY_F2 +#define TB_KEY_F3 LT_KEY_F3 +#define TB_KEY_F4 LT_KEY_F4 +#define TB_KEY_F5 LT_KEY_F5 +#define TB_KEY_F6 LT_KEY_F6 +#define TB_KEY_F7 LT_KEY_F7 +#define TB_KEY_F8 LT_KEY_F8 +#define TB_KEY_F9 LT_KEY_F9 +#define TB_KEY_F10 LT_KEY_F10 +#define TB_KEY_F11 LT_KEY_F11 +#define TB_KEY_F12 LT_KEY_F12 +#define TB_KEY_INSERT LT_KEY_INSERT +#define TB_KEY_DELETE LT_KEY_DELETE +#define TB_KEY_HOME LT_KEY_HOME +#define TB_KEY_END LT_KEY_END +#define TB_KEY_PGUP LT_KEY_PGUP +#define TB_KEY_PGDN LT_KEY_PGDN +#define TB_KEY_ARROW_UP LT_KEY_ARROW_UP +#define TB_KEY_ARROW_DOWN LT_KEY_ARROW_DOWN +#define TB_KEY_ARROW_LEFT LT_KEY_ARROW_LEFT +#define TB_KEY_ARROW_RIGHT LT_KEY_ARROW_RIGHT +#define TB_KEY_BACK_TAB LT_KEY_BACK_TAB +#define TB_KEY_MOUSE_LEFT LT_KEY_MOUSE_LEFT +#define TB_KEY_MOUSE_RIGHT LT_KEY_MOUSE_RIGHT +#define TB_KEY_MOUSE_MIDDLE LT_KEY_MOUSE_MIDDLE +#define TB_KEY_MOUSE_RELEASE LT_KEY_MOUSE_RELEASE +#define TB_KEY_MOUSE_WHEEL_UP LT_KEY_MOUSE_WHEEL_UP +#define TB_KEY_MOUSE_WHEEL_DOWN LT_KEY_MOUSE_WHEEL_DOWN + +/* ---- functions: clean 1:1 aliases ---- */ +#define tb_init lt_init +#define tb_init_fd lt_init_fd +#define tb_init_file lt_init_file +#define tb_shutdown lt_shutdown +#define tb_width lt_width +#define tb_height lt_height +#define tb_clear lt_clear +#define tb_set_clear_attrs lt_set_clear_attrs +#define tb_present lt_present +#define tb_invalidate lt_invalidate +#define tb_set_cursor lt_set_cursor +#define tb_hide_cursor lt_hide_cursor +#define tb_set_cell lt_set_cell +#define tb_set_cell_ex lt_set_cell_ex +#define tb_extend_cell lt_extend_cell +#define tb_print lt_print +#define tb_print_ex lt_print_ex +#define tb_printf lt_printf +#define tb_printf_ex lt_printf_ex +#define tb_send lt_send +#define tb_sendf lt_sendf +#define tb_peek_event lt_peek_event +#define tb_poll_event lt_poll_event +#define tb_get_fds lt_get_fds +#define tb_set_input_mode lt_set_input_mode +#define tb_set_output_mode lt_set_output_mode +#define tb_last_errno lt_last_errno +#define tb_strerror lt_strerror +#define tb_has_egc lt_has_egc +#define tb_attr_width lt_attr_width +#define tb_version lt_version +#define tb_iswprint lt_iswprint +#define tb_wcwidth lt_wcwidth +#define tb_utf8_char_length lt_utf8_char_length +#define tb_utf8_char_to_unicode lt_utf8_char_to_unicode +#define tb_utf8_unicode_to_char lt_utf8_unicode_to_char + +/* ---- allocator indirection -> libc ---- */ +#define tb_malloc malloc +#define tb_realloc realloc +#define tb_free free + +/* ---- bucket B: adapters (libterm-backed, divergent shape) ---- */ +#define tb_put_cell(x, y, c) lt_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) + +static inline int tb_get_cell(int x, int y, int back, struct tb_cell **cell) { + /* libterm copies into a caller struct rather than exposing a live buffer + * pointer. Snapshot into a function-local static (termbox2 is a single + * global instance, not thread-safe, so a plain static matches its model); + * the returned pointer is valid until the next tb_get_cell call. libterm + * reads the back buffer only, so back==0 (front) is unsupported. */ + static struct lt_cell scratch; + int rc; + if (back == 0) + return LT_ERR; + rc = lt_get_cell(x, y, &scratch); + if (rc == LT_OK && cell) + *cell = &scratch; + return rc; +} + +/* ---- bucket C: unsupported. Using one is a compile error; the message is + * carried in the (deliberately undeclared) identifier name. ---- */ +#define tb_init_rwfd(rfd, wfd) \ + LIBTERM_COMPAT_UNSUPPORTED_tb_init_rwfd__use_lt_init_fd_or_lt_init_file +#define tb_set_func(type, fn) \ + LIBTERM_COMPAT_UNSUPPORTED_tb_set_func__deprecated_upstream_no_libterm_equivalent +#define tb_has_truecolor() \ + LIBTERM_COMPAT_UNSUPPORTED_tb_has_truecolor__use_lt_detect_color_depth +#define tb_cell_buffer() \ + LIBTERM_COMPAT_UNSUPPORTED_tb_cell_buffer__no_raw_buffer_use_lt_get_cell +#define tb_key_i(i) \ + LIBTERM_COMPAT_UNSUPPORTED_tb_key_i__no_terminfo_cap_table + +#endif /* LIBTERM_COMPAT_TERMBOX2_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9d2cb3d..f07b635 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,14 @@ libterm_add_test(test_simd_diff) libterm_add_test(test_keymap) libterm_add_test(test_detect_color_depth) libterm_add_test(test_color_parse) +# compat/termbox2.h drop-in layer: compile+link proof that every aliased symbol +# resolves. Platform-agnostic (no terminal init), so it runs everywhere. +add_executable(compat_smoke compat_smoke.c) +target_link_libraries(compat_smoke PRIVATE ${LIBTERM_LINK_TARGET}) +target_include_directories(compat_smoke PRIVATE + ${PROJECT_SOURCE_DIR}/compat + ${PROJECT_SOURCE_DIR}/include) +add_test(NAME compat_smoke COMMAND compat_smoke) # Dispatch builds: tell the SIMD tests which backends were compiled and # register the dispatcher sanity test. (Static builds: no-op.) if(LIBTERM_SIMD_BACKENDS) diff --git a/tests/compat_smoke.c b/tests/compat_smoke.c new file mode 100644 index 0000000..5424cd8 --- /dev/null +++ b/tests/compat_smoke.c @@ -0,0 +1,39 @@ +/* Compile+link proof that compat/termbox2.h resolves every aliased symbol to a + * real libterm symbol. Does not initialize a terminal — it references the API + * surface (types, function addresses, constants) so the linker must find them. */ +#include "termbox2.h" /* resolves to compat/termbox2.h via the include path */ + +#include + +int main(void) { + struct tb_cell cell; + struct tb_event ev; + uintattr_t attr = TB_RED | TB_BOLD; + + /* Take function addresses so the aliases must resolve to real linkable + * symbols, without running anything. */ + int (*pinit)(void) = tb_init; + int (*pshutdown)(void) = tb_shutdown; + int (*ppresent)(void) = tb_present; + int (*pclear)(void) = tb_clear; + int (*pset)(int, int, uint32_t, uintattr_t, uintattr_t) = tb_set_cell; + int (*ppoll)(struct tb_event *) = tb_poll_event; + int (*pcursor)(int, int) = tb_set_cursor; + int (*pget)(int, int, int, struct tb_cell **) = tb_get_cell; + const char *(*pstrerror)(int) = tb_strerror; + + int keys = TB_KEY_ENTER + TB_KEY_ESC + TB_KEY_ARROW_UP + TB_KEY_F1 + + TB_KEY_CTRL_C + TB_KEY_BACKSPACE + TB_KEY_TAB + TB_KEY_HOME + + TB_KEY_BACK_TAB + TB_KEY_MOUSE_LEFT; + int misc = TB_OK + TB_ERR + TB_ERR_NOT_INIT + TB_EVENT_KEY + TB_EVENT_RESIZE + + TB_EVENT_MOUSE + TB_INPUT_ESC + TB_OUTPUT_NORMAL + + (int)(TB_MOD_ALT | TB_MOD_CTRL | TB_MOD_SHIFT | TB_MOD_MOTION); + + (void)cell; (void)ev; (void)attr; + (void)pinit; (void)pshutdown; (void)ppresent; (void)pclear; (void)pset; + (void)ppoll; (void)pcursor; (void)pget; (void)pstrerror; + (void)keys; (void)misc; + + if (0) { tb_put_cell(0, 0, &cell); } /* prove the macro expands + links */ + return 0; +} From fe01f2a4800ed538e197e3f6b1909bedbe4fdc24 Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:11:01 +0700 Subject: [PATCH 2/7] test(compat): pty-backed tb_get_cell adapter round-trip + back-buffer-only contract --- tests/CMakeLists.txt | 7 +++++ tests/test_compat_get_cell.c | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/test_compat_get_cell.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f07b635..a5d008a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -172,6 +172,13 @@ if(NOT WIN32) target_include_directories(test_color_query PRIVATE ${PROJECT_SOURCE_DIR}/src) add_test(NAME test_color_query COMMAND test_color_query) set_tests_properties(test_color_query PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + add_executable(test_compat_get_cell test_compat_get_cell.c) + target_link_libraries(test_compat_get_cell PRIVATE ${LIBTERM_LINK_TARGET} ${LIBTERM_PTY_LIBS}) + target_include_directories(test_compat_get_cell PRIVATE + ${PROJECT_SOURCE_DIR}/compat + ${PROJECT_SOURCE_DIR}/include) + add_test(NAME test_compat_get_cell COMMAND test_compat_get_cell) + set_tests_properties(test_compat_get_cell PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) endif() endif() diff --git a/tests/test_compat_get_cell.c b/tests/test_compat_get_cell.c new file mode 100644 index 0000000..aea9842 --- /dev/null +++ b/tests/test_compat_get_cell.c @@ -0,0 +1,57 @@ +/* tb_get_cell compat adapter: round-trips what tb_set_cell wrote into the back + * buffer via the snapshot pointer, and enforces the back-buffer-only contract + * (back==0 -> LT_ERR). POSIX-only (needs a pty for tb_init_fd); returns 77 + * (CTest "skip") when no pty is available. */ +#define _DEFAULT_SOURCE +#include "termbox2.h" /* compat layer */ + +#include +#if defined(__APPLE__) +#include +#else +#include +#endif +#include +#include + +int main(void) { + int master = -1, slave = -1; + struct winsize ws; + memset(&ws, 0, sizeof ws); + ws.ws_row = 24; + ws.ws_col = 80; + + if (openpty(&master, &slave, NULL, NULL, &ws) != 0) + return 77; /* CTest skip */ + + assert(tb_init_fd(slave) == TB_OK); + assert(tb_clear() == TB_OK); + + /* What tb_set_cell writes, tb_get_cell (back buffer) reads back. */ + assert(tb_set_cell(3, 4, 0x20AC, TB_RED, TB_BLUE) == TB_OK); /* euro sign */ + { + struct tb_cell *c = NULL; + assert(tb_get_cell(3, 4, 1, &c) == TB_OK); + assert(c != NULL); + assert(c->ch == 0x20AC); + assert(c->fg == (uintattr_t)TB_RED); + assert(c->bg == (uintattr_t)TB_BLUE); + } + + /* back==0 (front buffer) is unsupported and must report LT_ERR. */ + { + struct tb_cell *c = NULL; + assert(tb_get_cell(3, 4, 0, &c) == TB_ERR); + } + + /* Out-of-bounds passes lt_get_cell's own error through. */ + { + struct tb_cell *c = NULL; + assert(tb_get_cell(-1, 0, 1, &c) == TB_ERR_OUT_OF_BOUNDS); + } + + assert(tb_shutdown() == TB_OK); + close(slave); + close(master); + return 0; +} From 6b541fbadae914d833f993e57a5b6794327b30bd Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:14:23 +0700 Subject: [PATCH 3/7] examples(compat): minimal text editor built against compat/termbox2.h (DoD #8 proof) --- examples/CMakeLists.txt | 4 + examples/editor.c | 233 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 examples/editor.c diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7acd742..9ce4a86 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -17,3 +17,7 @@ libterm_add_example(truecolor) libterm_add_example(kbd) libterm_add_example(theme) libterm_add_example(resize) +libterm_add_example(editor) +# The editor builds against the termbox2 compat layer (drop-in proof, DoD #8), +# so it needs compat/ on its include path to resolve `#include "termbox2.h"`. +target_include_directories(editor PRIVATE ${PROJECT_SOURCE_DIR}/compat) diff --git a/examples/editor.c b/examples/editor.c new file mode 100644 index 0000000..d091fcf --- /dev/null +++ b/examples/editor.c @@ -0,0 +1,233 @@ +#define _DEFAULT_SOURCE +/* editor.c — a minimal but real text editor built ONLY against the termbox2 + * compat layer (no lt_/LT_ symbols). Opens the file named on argv[1] (or an + * empty buffer), supports cursor movement, character insert, backspace, + * newline, save (Ctrl-S) and quit (Ctrl-Q). DoD #8 drop-in proof. */ +#include "termbox2.h" + +#include +#include +#include +#include + +struct erow { + char *chars; + size_t len; +}; + +static struct erow *g_rows; +static size_t g_nrows; +static int g_cx, g_cy; /* cursor, in text coords */ +static int g_rowoff; /* first visible row */ +static const char *g_filename; +static int g_dirty; + +static void die(const char *msg) { + tb_shutdown(); + fprintf(stderr, "editor: %s\n", msg); + exit(1); +} + +static void append_row(const char *s, size_t len) { + struct erow *nr = + (struct erow *)realloc(g_rows, (g_nrows + 1) * sizeof *g_rows); + if (!nr) + die("out of memory"); + g_rows = nr; + g_rows[g_nrows].chars = (char *)malloc(len + 1); + if (!g_rows[g_nrows].chars) + die("out of memory"); + memcpy(g_rows[g_nrows].chars, s, len); + g_rows[g_nrows].chars[len] = '\0'; + g_rows[g_nrows].len = len; + g_nrows++; +} + +static void load_file(const char *path) { + FILE *f = fopen(path, "r"); + if (!f) { + append_row("", 0); /* new file: start with one empty line */ + return; + } + char *line = NULL; + size_t cap = 0; + ssize_t n; + while ((n = getline(&line, &cap, f)) != -1) { + size_t len = (size_t)n; + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + len--; + append_row(line, len); + } + free(line); + fclose(f); + if (g_nrows == 0) + append_row("", 0); +} + +static void save_file(void) { + if (!g_filename) + return; + FILE *f = fopen(g_filename, "w"); + if (!f) + return; + for (size_t i = 0; i < g_nrows; i++) { + fwrite(g_rows[i].chars, 1, g_rows[i].len, f); + fputc('\n', f); + } + fclose(f); + g_dirty = 0; +} + +static void row_insert_char(struct erow *row, int at, char c) { + char *nc = (char *)realloc(row->chars, row->len + 2); + if (!nc) + die("out of memory"); + row->chars = nc; + memmove(&row->chars[at + 1], &row->chars[at], row->len - (size_t)at + 1); + row->chars[at] = c; + row->len++; +} + +static void insert_char(char c) { + row_insert_char(&g_rows[g_cy], g_cx, c); + g_cx++; + g_dirty = 1; +} + +static void insert_newline(void) { + struct erow *row = &g_rows[g_cy]; + append_row("", 0); /* grow the array; positions are overwritten below */ + memmove(&g_rows[g_cy + 2], &g_rows[g_cy + 1], + (g_nrows - (size_t)g_cy - 2) * sizeof *g_rows); + size_t tail = row->len - (size_t)g_cx; + g_rows[g_cy + 1].chars = (char *)malloc(tail + 1); + if (!g_rows[g_cy + 1].chars) + die("out of memory"); + memcpy(g_rows[g_cy + 1].chars, &row->chars[g_cx], tail); + g_rows[g_cy + 1].chars[tail] = '\0'; + g_rows[g_cy + 1].len = tail; + row->chars[g_cx] = '\0'; + row->len = (size_t)g_cx; + g_cy++; + g_cx = 0; + g_dirty = 1; +} + +static void del_char(void) { + if (g_cx == 0 && g_cy == 0) + return; + struct erow *row = &g_rows[g_cy]; + if (g_cx > 0) { + memmove(&row->chars[g_cx - 1], &row->chars[g_cx], + row->len - (size_t)g_cx + 1); + row->len--; + g_cx--; + } else { + struct erow *prev = &g_rows[g_cy - 1]; + int newcx = (int)prev->len; + char *nc = (char *)realloc(prev->chars, prev->len + row->len + 1); + if (!nc) + die("out of memory"); + prev->chars = nc; + memcpy(&prev->chars[prev->len], row->chars, row->len + 1); + prev->len += row->len; + free(row->chars); + memmove(&g_rows[g_cy], &g_rows[g_cy + 1], + (g_nrows - (size_t)g_cy - 1) * sizeof *g_rows); + g_nrows--; + g_cy--; + g_cx = newcx; + } + g_dirty = 1; +} + +static void draw(void) { + int h = tb_height(); + int w = tb_width(); + tb_clear(); + for (int y = 0; y < h - 1; y++) { + size_t fr = (size_t)(y + g_rowoff); + if (fr >= g_nrows) + continue; + struct erow *row = &g_rows[fr]; + int limit = (int)row->len < w ? (int)row->len : w; + for (int x = 0; x < limit; x++) + tb_set_cell(x, y, (uint32_t)(unsigned char)row->chars[x], TB_DEFAULT, + TB_DEFAULT); + } + char status[128]; + int sl = snprintf(status, sizeof status, " %s %s | Ctrl-S save Ctrl-Q quit", + g_filename ? g_filename : "[no file]", + g_dirty ? "(modified)" : ""); + for (int x = 0; x < w; x++) { + char ch = x < sl ? status[x] : ' '; + tb_set_cell(x, h - 1, (uint32_t)(unsigned char)ch, TB_DEFAULT, TB_REVERSE); + } + tb_set_cursor(g_cx, g_cy - g_rowoff); + tb_present(); +} + +static void scroll_into_view(void) { + int h = tb_height(); + if (g_cy < g_rowoff) + g_rowoff = g_cy; + if (g_cy >= g_rowoff + h - 1) + g_rowoff = g_cy - (h - 1) + 1; +} + +static void move_cursor(uint16_t key) { + struct erow *row = &g_rows[g_cy]; + if (key == TB_KEY_ARROW_LEFT && g_cx > 0) + g_cx--; + else if (key == TB_KEY_ARROW_RIGHT && g_cx < (int)row->len) + g_cx++; + else if (key == TB_KEY_ARROW_UP && g_cy > 0) + g_cy--; + else if (key == TB_KEY_ARROW_DOWN && (size_t)(g_cy + 1) < g_nrows) + g_cy++; + if ((size_t)g_cx > g_rows[g_cy].len) + g_cx = (int)g_rows[g_cy].len; +} + +int main(int argc, char **argv) { + g_filename = argc > 1 ? argv[1] : NULL; + if (g_filename) + load_file(g_filename); + else + append_row("", 0); + + if (tb_init() != TB_OK) + die("tb_init failed"); + + for (;;) { + scroll_into_view(); + draw(); + struct tb_event ev; + if (tb_poll_event(&ev) != TB_OK) + continue; + if (ev.type != TB_EVENT_KEY) + continue; + if (ev.key == TB_KEY_CTRL_Q) + break; + if (ev.key == TB_KEY_CTRL_S) { + save_file(); + continue; + } + if (ev.key == TB_KEY_ENTER) { + insert_newline(); + } else if (ev.key == TB_KEY_BACKSPACE || ev.key == TB_KEY_BACKSPACE2) { + del_char(); + } else if (ev.key == TB_KEY_ARROW_LEFT || ev.key == TB_KEY_ARROW_RIGHT || + ev.key == TB_KEY_ARROW_UP || ev.key == TB_KEY_ARROW_DOWN) { + move_cursor(ev.key); + } else if (ev.ch != 0 && ev.ch < 128) { + insert_char((char)ev.ch); + } + } + + tb_shutdown(); + for (size_t i = 0; i < g_nrows; i++) + free(g_rows[i].chars); + free(g_rows); + return 0; +} From 5e625609272be987a186eeba4406637c69a390c8 Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:26:21 +0700 Subject: [PATCH 4/7] test(compat): exhaustively reference every alias in compat_smoke Reference all 130 value constants + 39 function/allocator aliases (plus TB_RGB and the tb_get_cell/tb_put_cell adapters), so a typo in any alias fails the build here instead of silently when a consumer first uses the symbol. Bucket-C unsupported macros are deliberately not referenced. --- tests/compat_smoke.c | 209 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 186 insertions(+), 23 deletions(-) diff --git a/tests/compat_smoke.c b/tests/compat_smoke.c index 5424cd8..5d807b9 100644 --- a/tests/compat_smoke.c +++ b/tests/compat_smoke.c @@ -1,6 +1,10 @@ -/* Compile+link proof that compat/termbox2.h resolves every aliased symbol to a - * real libterm symbol. Does not initialize a terminal — it references the API - * surface (types, function addresses, constants) so the linker must find them. */ +/* Exhaustive compile+link proof that compat/termbox2.h resolves EVERY aliased + * symbol to a real libterm symbol. References every value constant and every + * function alias, so a typo in any alias (e.g. TB_KEY_X -> a nonexistent + * LT_KEY_X) fails the build here rather than silently when a consumer first + * uses it. Does not initialize a terminal. The bucket-C unsupported macros + * (tb_init_rwfd / tb_set_func / tb_has_truecolor / tb_cell_buffer / tb_key_i) + * are deliberately NOT referenced — using one is a compile error by design. */ #include "termbox2.h" /* resolves to compat/termbox2.h via the include path */ #include @@ -8,32 +12,191 @@ int main(void) { struct tb_cell cell; struct tb_event ev; - uintattr_t attr = TB_RED | TB_BOLD; + uintattr_t attr = TB_RGB(1, 2, 3); + long long sink = 0; - /* Take function addresses so the aliases must resolve to real linkable - * symbols, without running anything. */ - int (*pinit)(void) = tb_init; - int (*pshutdown)(void) = tb_shutdown; - int (*ppresent)(void) = tb_present; - int (*pclear)(void) = tb_clear; + /* A few typed function pointers prove real linkage (not just declaration). */ int (*pset)(int, int, uint32_t, uintattr_t, uintattr_t) = tb_set_cell; int (*ppoll)(struct tb_event *) = tb_poll_event; - int (*pcursor)(int, int) = tb_set_cursor; int (*pget)(int, int, int, struct tb_cell **) = tb_get_cell; - const char *(*pstrerror)(int) = tb_strerror; + (void)cell; (void)ev; (void)attr; (void)pset; (void)ppoll; (void)pget; - int keys = TB_KEY_ENTER + TB_KEY_ESC + TB_KEY_ARROW_UP + TB_KEY_F1 + - TB_KEY_CTRL_C + TB_KEY_BACKSPACE + TB_KEY_TAB + TB_KEY_HOME + - TB_KEY_BACK_TAB + TB_KEY_MOUSE_LEFT; - int misc = TB_OK + TB_ERR + TB_ERR_NOT_INIT + TB_EVENT_KEY + TB_EVENT_RESIZE + - TB_EVENT_MOUSE + TB_INPUT_ESC + TB_OUTPUT_NORMAL + - (int)(TB_MOD_ALT | TB_MOD_CTRL | TB_MOD_SHIFT | TB_MOD_MOTION); + /* Every value constant resolves (sum forces each token to be defined). */ + sink += (long long)(TB_OK); + sink += (long long)(TB_ERR); + sink += (long long)(TB_ERR_NEED_MORE); + sink += (long long)(TB_ERR_INIT_ALREADY); + sink += (long long)(TB_ERR_INIT_OPEN); + sink += (long long)(TB_ERR_MEM); + sink += (long long)(TB_ERR_NO_EVENT); + sink += (long long)(TB_ERR_NO_TERM); + sink += (long long)(TB_ERR_NOT_INIT); + sink += (long long)(TB_ERR_OUT_OF_BOUNDS); + sink += (long long)(TB_ERR_READ); + sink += (long long)(TB_ERR_RESIZE_IOCTL); + sink += (long long)(TB_ERR_RESIZE_PIPE); + sink += (long long)(TB_ERR_RESIZE_SIGACTION); + sink += (long long)(TB_ERR_POLL); + sink += (long long)(TB_ERR_TCGETATTR); + sink += (long long)(TB_ERR_TCSETATTR); + sink += (long long)(TB_ERR_UNSUPPORTED_TERM); + sink += (long long)(TB_ERR_RESIZE_WRITE); + sink += (long long)(TB_ERR_RESIZE_POLL); + sink += (long long)(TB_ERR_RESIZE_READ); + sink += (long long)(TB_ERR_RESIZE_SSCANF); + sink += (long long)(TB_ERR_CAP_COLLISION); + sink += (long long)(TB_EVENT_KEY); + sink += (long long)(TB_EVENT_RESIZE); + sink += (long long)(TB_EVENT_MOUSE); + sink += (long long)(TB_MOD_ALT); + sink += (long long)(TB_MOD_CTRL); + sink += (long long)(TB_MOD_SHIFT); + sink += (long long)(TB_MOD_MOTION); + sink += (long long)(TB_INPUT_CURRENT); + sink += (long long)(TB_INPUT_ESC); + sink += (long long)(TB_INPUT_ALT); + sink += (long long)(TB_INPUT_MOUSE); + sink += (long long)(TB_OUTPUT_CURRENT); + sink += (long long)(TB_OUTPUT_NORMAL); + sink += (long long)(TB_OUTPUT_256); + sink += (long long)(TB_OUTPUT_216); + sink += (long long)(TB_OUTPUT_GRAYSCALE); + sink += (long long)(TB_OUTPUT_TRUECOLOR); + sink += (long long)(TB_DEFAULT); + sink += (long long)(TB_BLACK); + sink += (long long)(TB_RED); + sink += (long long)(TB_GREEN); + sink += (long long)(TB_YELLOW); + sink += (long long)(TB_BLUE); + sink += (long long)(TB_MAGENTA); + sink += (long long)(TB_CYAN); + sink += (long long)(TB_WHITE); + sink += (long long)(TB_BOLD); + sink += (long long)(TB_UNDERLINE); + sink += (long long)(TB_REVERSE); + sink += (long long)(TB_ITALIC); + sink += (long long)(TB_BLINK); + sink += (long long)(TB_DIM); + sink += (long long)(TB_STRIKE); + sink += (long long)(TB_KEY_CTRL_TILDE); + sink += (long long)(TB_KEY_CTRL_2); + sink += (long long)(TB_KEY_CTRL_A); + sink += (long long)(TB_KEY_CTRL_B); + sink += (long long)(TB_KEY_CTRL_C); + sink += (long long)(TB_KEY_CTRL_D); + sink += (long long)(TB_KEY_CTRL_E); + sink += (long long)(TB_KEY_CTRL_F); + sink += (long long)(TB_KEY_CTRL_G); + sink += (long long)(TB_KEY_BACKSPACE); + sink += (long long)(TB_KEY_CTRL_H); + sink += (long long)(TB_KEY_TAB); + sink += (long long)(TB_KEY_CTRL_I); + sink += (long long)(TB_KEY_CTRL_J); + sink += (long long)(TB_KEY_CTRL_K); + sink += (long long)(TB_KEY_CTRL_L); + sink += (long long)(TB_KEY_ENTER); + sink += (long long)(TB_KEY_CTRL_M); + sink += (long long)(TB_KEY_CTRL_N); + sink += (long long)(TB_KEY_CTRL_O); + sink += (long long)(TB_KEY_CTRL_P); + sink += (long long)(TB_KEY_CTRL_Q); + sink += (long long)(TB_KEY_CTRL_R); + sink += (long long)(TB_KEY_CTRL_S); + sink += (long long)(TB_KEY_CTRL_T); + sink += (long long)(TB_KEY_CTRL_U); + sink += (long long)(TB_KEY_CTRL_V); + sink += (long long)(TB_KEY_CTRL_W); + sink += (long long)(TB_KEY_CTRL_X); + sink += (long long)(TB_KEY_CTRL_Y); + sink += (long long)(TB_KEY_CTRL_Z); + sink += (long long)(TB_KEY_ESC); + sink += (long long)(TB_KEY_CTRL_LSQ_BRACKET); + sink += (long long)(TB_KEY_CTRL_3); + sink += (long long)(TB_KEY_CTRL_4); + sink += (long long)(TB_KEY_CTRL_BACKSLASH); + sink += (long long)(TB_KEY_CTRL_5); + sink += (long long)(TB_KEY_CTRL_RSQ_BRACKET); + sink += (long long)(TB_KEY_CTRL_6); + sink += (long long)(TB_KEY_CTRL_7); + sink += (long long)(TB_KEY_CTRL_SLASH); + sink += (long long)(TB_KEY_CTRL_UNDERSCORE); + sink += (long long)(TB_KEY_SPACE); + sink += (long long)(TB_KEY_BACKSPACE2); + sink += (long long)(TB_KEY_CTRL_8); + sink += (long long)(TB_KEY_F1); + sink += (long long)(TB_KEY_F2); + sink += (long long)(TB_KEY_F3); + sink += (long long)(TB_KEY_F4); + sink += (long long)(TB_KEY_F5); + sink += (long long)(TB_KEY_F6); + sink += (long long)(TB_KEY_F7); + sink += (long long)(TB_KEY_F8); + sink += (long long)(TB_KEY_F9); + sink += (long long)(TB_KEY_F10); + sink += (long long)(TB_KEY_F11); + sink += (long long)(TB_KEY_F12); + sink += (long long)(TB_KEY_INSERT); + sink += (long long)(TB_KEY_DELETE); + sink += (long long)(TB_KEY_HOME); + sink += (long long)(TB_KEY_END); + sink += (long long)(TB_KEY_PGUP); + sink += (long long)(TB_KEY_PGDN); + sink += (long long)(TB_KEY_ARROW_UP); + sink += (long long)(TB_KEY_ARROW_DOWN); + sink += (long long)(TB_KEY_ARROW_LEFT); + sink += (long long)(TB_KEY_ARROW_RIGHT); + sink += (long long)(TB_KEY_BACK_TAB); + sink += (long long)(TB_KEY_MOUSE_LEFT); + sink += (long long)(TB_KEY_MOUSE_RIGHT); + sink += (long long)(TB_KEY_MOUSE_MIDDLE); + sink += (long long)(TB_KEY_MOUSE_RELEASE); + sink += (long long)(TB_KEY_MOUSE_WHEEL_UP); + sink += (long long)(TB_KEY_MOUSE_WHEEL_DOWN); - (void)cell; (void)ev; (void)attr; - (void)pinit; (void)pshutdown; (void)ppresent; (void)pclear; (void)pset; - (void)ppoll; (void)pcursor; (void)pget; (void)pstrerror; - (void)keys; (void)misc; + /* Every function/allocator alias resolves (designator referenced + discarded). */ + (void)(tb_init); + (void)(tb_init_fd); + (void)(tb_init_file); + (void)(tb_shutdown); + (void)(tb_width); + (void)(tb_height); + (void)(tb_clear); + (void)(tb_set_clear_attrs); + (void)(tb_present); + (void)(tb_invalidate); + (void)(tb_set_cursor); + (void)(tb_hide_cursor); + (void)(tb_set_cell); + (void)(tb_set_cell_ex); + (void)(tb_extend_cell); + (void)(tb_print); + (void)(tb_print_ex); + (void)(tb_printf); + (void)(tb_printf_ex); + (void)(tb_send); + (void)(tb_sendf); + (void)(tb_peek_event); + (void)(tb_poll_event); + (void)(tb_get_fds); + (void)(tb_set_input_mode); + (void)(tb_set_output_mode); + (void)(tb_last_errno); + (void)(tb_strerror); + (void)(tb_has_egc); + (void)(tb_attr_width); + (void)(tb_version); + (void)(tb_iswprint); + (void)(tb_wcwidth); + (void)(tb_utf8_char_length); + (void)(tb_utf8_char_to_unicode); + (void)(tb_utf8_unicode_to_char); + (void)(tb_malloc); + (void)(tb_realloc); + (void)(tb_free); - if (0) { tb_put_cell(0, 0, &cell); } /* prove the macro expands + links */ + (void)tb_get_cell; /* static-inline adapter designator */ + if (0) { tb_put_cell(0, 0, &cell); } /* function-like macro expands + links */ + + (void)sink; return 0; } From c736bf608bf7c4b39c05e649706846087d67d288 Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:38:55 +0700 Subject: [PATCH 5/7] fix(compat-editor): use-after-free in insert_newline (Enter -> 'out of memory') MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit append_row() reallocs g_rows and may move it, but insert_newline held a pointer into the old array across that call and dereferenced it — a garbage length led to a huge failing malloc reported as 'out of memory' when Enter split a line. Realloc the array directly and re-index g_rows[g_cy] afterwards (also drops a latent 1-byte leak). Add a terminal-free white-box regression test that includes the editor TU (main() guarded by EDITOR_NO_MAIN) and asserts split/join contents; it trips ASan heap-use-after-free against the old code. --- examples/editor.c | 38 +++++++++++++----- tests/CMakeLists.txt | 12 ++++++ tests/test_compat_editor.c | 80 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 tests/test_compat_editor.c diff --git a/examples/editor.c b/examples/editor.c index d091fcf..2d42a50 100644 --- a/examples/editor.c +++ b/examples/editor.c @@ -95,19 +95,33 @@ static void insert_char(char c) { } static void insert_newline(void) { - struct erow *row = &g_rows[g_cy]; - append_row("", 0); /* grow the array; positions are overwritten below */ + /* Grow the row array by one. realloc may MOVE g_rows, so we must hold no + * pointer into it across this call and re-index g_rows[g_cy] afterwards. + * (The earlier version captured `&g_rows[g_cy]` before the grow and then + * dereferenced it — a use-after-free that read a garbage length and asked + * for a huge allocation, surfacing as "out of memory" when Enter was hit.) */ + struct erow *nr = + (struct erow *)realloc(g_rows, (g_nrows + 1) * sizeof *g_rows); + if (!nr) + die("out of memory"); + g_rows = nr; + /* Open a slot at g_cy+1 by shifting the rows after it down by one. */ memmove(&g_rows[g_cy + 2], &g_rows[g_cy + 1], - (g_nrows - (size_t)g_cy - 2) * sizeof *g_rows); - size_t tail = row->len - (size_t)g_cx; - g_rows[g_cy + 1].chars = (char *)malloc(tail + 1); - if (!g_rows[g_cy + 1].chars) + (g_nrows - (size_t)g_cy - 1) * sizeof *g_rows); + g_nrows++; + /* New row g_cy+1 takes the text from the split point onward. g_rows[g_cy] is + * re-indexed after the realloc; its .chars buffer survived the move. */ + size_t tail = g_rows[g_cy].len - (size_t)g_cx; + char *buf = (char *)malloc(tail + 1); + if (!buf) die("out of memory"); - memcpy(g_rows[g_cy + 1].chars, &row->chars[g_cx], tail); - g_rows[g_cy + 1].chars[tail] = '\0'; + memcpy(buf, &g_rows[g_cy].chars[g_cx], tail); + buf[tail] = '\0'; + g_rows[g_cy + 1].chars = buf; g_rows[g_cy + 1].len = tail; - row->chars[g_cx] = '\0'; - row->len = (size_t)g_cx; + /* Truncate the current row at the split point. */ + g_rows[g_cy].chars[g_cx] = '\0'; + g_rows[g_cy].len = (size_t)g_cx; g_cy++; g_cx = 0; g_dirty = 1; @@ -189,6 +203,9 @@ static void move_cursor(uint16_t key) { g_cx = (int)g_rows[g_cy].len; } +/* EDITOR_NO_MAIN lets a white-box test (#include "editor.c") exercise the + * buffer functions directly, without a terminal. */ +#ifndef EDITOR_NO_MAIN int main(int argc, char **argv) { g_filename = argc > 1 ? argv[1] : NULL; if (g_filename) @@ -231,3 +248,4 @@ int main(int argc, char **argv) { free(g_rows); return 0; } +#endif /* EDITOR_NO_MAIN */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a5d008a..6878280 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,18 @@ target_include_directories(compat_smoke PRIVATE ${PROJECT_SOURCE_DIR}/compat ${PROJECT_SOURCE_DIR}/include) add_test(NAME compat_smoke COMMAND compat_smoke) + +# White-box regression test for examples/editor.c's buffer model (#includes the +# editor TU with main() compiled out). Guards the insert_newline use-after-free. +# Platform-agnostic — no terminal needed. +add_executable(test_compat_editor test_compat_editor.c) +target_link_libraries(test_compat_editor PRIVATE ${LIBTERM_LINK_TARGET}) +target_include_directories(test_compat_editor PRIVATE + ${PROJECT_SOURCE_DIR}/examples + ${PROJECT_SOURCE_DIR}/compat + ${PROJECT_SOURCE_DIR}/include) +add_test(NAME test_compat_editor COMMAND test_compat_editor) + # Dispatch builds: tell the SIMD tests which backends were compiled and # register the dispatcher sanity test. (Static builds: no-op.) if(LIBTERM_SIMD_BACKENDS) diff --git a/tests/test_compat_editor.c b/tests/test_compat_editor.c new file mode 100644 index 0000000..f54c2d2 --- /dev/null +++ b/tests/test_compat_editor.c @@ -0,0 +1,80 @@ +/* White-box regression test for the compat editor's buffer model. Includes the + * editor translation unit with its main() compiled out, then drives the row + * functions directly — no terminal needed, so it runs everywhere. + * + * Guards the use-after-free fixed in insert_newline: it captured a pointer into + * g_rows before append_row()'s realloc could move the array, then dereferenced + * the stale pointer — a garbage length led to a huge allocation ("out of + * memory" when Enter split a line). This test splits and rejoins lines and + * asserts the contents (incl. the row AFTER the split, which the stale-pointer + * path corrupted), so a regression fails here (and trips ASan in CI). */ +#define EDITOR_NO_MAIN +#include "editor.c" + +#include +#include + +static void reset_buffer(void) { + for (size_t i = 0; i < g_nrows; i++) + free(g_rows[i].chars); + free(g_rows); + g_rows = NULL; + g_nrows = 0; + g_cx = 0; + g_cy = 0; + g_rowoff = 0; +} + +int main(void) { + /* Build two rows: "hello", "world". */ + append_row("hello", 5); + append_row("world", 5); + assert(g_nrows == 2); + + /* Split "hello" after "he" (Enter at row 0, col 2). This is the exact path + * that used to OOM. */ + g_cy = 0; + g_cx = 2; + insert_newline(); + + assert(g_nrows == 3); + assert(g_rows[0].len == 2 && strcmp(g_rows[0].chars, "he") == 0); + assert(g_rows[1].len == 3 && strcmp(g_rows[1].chars, "llo") == 0); + /* The row after the split point must be preserved — the stale-pointer bug + * corrupted exactly this. */ + assert(g_rows[2].len == 5 && strcmp(g_rows[2].chars, "world") == 0); + assert(g_cy == 1 && g_cx == 0); + + /* Backspace at the start of "llo" rejoins it onto "he" -> "hello". */ + del_char(); + assert(g_nrows == 2); + assert(g_rows[0].len == 5 && strcmp(g_rows[0].chars, "hello") == 0); + assert(g_rows[1].len == 5 && strcmp(g_rows[1].chars, "world") == 0); + assert(g_cy == 0 && g_cx == 2); + + /* Splitting at the very end of the last row (tail length 0) must not + * underflow or over-read. */ + g_cy = 1; + g_cx = (int)g_rows[1].len; + insert_newline(); + assert(g_nrows == 3); + assert(g_rows[1].len == 5 && strcmp(g_rows[1].chars, "world") == 0); + assert(g_rows[2].len == 0 && g_rows[2].chars[0] == '\0'); + + /* Type a character into the empty trailing row. */ + g_cy = 2; + g_cx = 0; + insert_char('!'); + assert(g_rows[2].len == 1 && g_rows[2].chars[0] == '!'); + + reset_buffer(); + + /* Reference the static functions that are only reachable from the compiled- + * out main(), so -Werror -Wunused-function stays satisfied. */ + (void)&load_file; + (void)&save_file; + (void)&draw; + (void)&scroll_into_view; + (void)&move_cursor; + return 0; +} From e068386b4138a9637d12549d22c4d5e02a35a97d Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 17:49:02 +0700 Subject: [PATCH 6/7] fix(compat-editor): enable LT_INPUT_COMPAT so Ctrl-S/Ctrl-Q fire libterm defaults to its modern key model (Ctrl+letter -> ch + LT_MOD_CTRL, key == 0), so the editor's termbox2-idiom checks (ev.key == TB_KEY_CTRL_S/Q) never matched and Ctrl-S/Ctrl-Q typed 's'/'q' instead. Opt into termbox2 control-byte semantics with the documented one-line adaptation. --- examples/editor.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/editor.c b/examples/editor.c index 2d42a50..e1f53f6 100644 --- a/examples/editor.c +++ b/examples/editor.c @@ -216,6 +216,13 @@ int main(int argc, char **argv) { if (tb_init() != TB_OK) die("tb_init failed"); + /* libterm defaults to its modern key model, where Ctrl+letter arrives as + * ch + LT_MOD_CTRL (key == 0). This editor is written in the termbox2 idiom + * (ev.key == TB_KEY_CTRL_S/Q/...), so opt into termbox2 control-byte + * semantics — the one documented adaptation a termbox2 program needs. This + * is the only LT_* symbol the editor references; everything else is tb_/TB_. */ + tb_set_input_mode(LT_INPUT_COMPAT); + for (;;) { scroll_into_view(); draw(); From 8d9073dd667a20e30455d5e06a5b205e2595e9cf Mon Sep 17 00:00:00 2001 From: rizukirr Date: Sat, 13 Jun 2026 18:16:06 +0700 Subject: [PATCH 7/7] ci(compat): clang-format the compat files; gate editor test to POSIX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clang-format: reflow compat/termbox2.h, compat_smoke.c, editor.c to satisfy the format check. windows-mingw-native: test_compat_editor #includes editor.c, which uses POSIX getline() (undeclared on MinGW); gate it to if(NOT WIN32) — examples are already EXAMPLES=OFF on the Windows lanes, and the buffer logic is covered on Linux/macOS. --- compat/termbox2.h | 315 +++++++++++++++++++++---------------------- examples/editor.c | 3 +- tests/CMakeLists.txt | 19 +-- tests/compat_smoke.c | 14 +- 4 files changed, 181 insertions(+), 170 deletions(-) diff --git a/compat/termbox2.h b/compat/termbox2.h index 2695938..1c5ffdd 100644 --- a/compat/termbox2.h +++ b/compat/termbox2.h @@ -39,199 +39,199 @@ /* ---- types: alias the struct TAGS (a typedef would leave `struct tb_cell` * a separate incomplete type and break tb_get_cell/tb_poll_event). ---- */ -#define tb_cell lt_cell +#define tb_cell lt_cell #define tb_event lt_event typedef lt_attr uintattr_t; /* ---- return codes ---- */ -#define TB_OK LT_OK -#define TB_ERR LT_ERR -#define TB_ERR_NEED_MORE LT_ERR_NEED_MORE -#define TB_ERR_INIT_ALREADY LT_ERR_INIT_ALREADY -#define TB_ERR_INIT_OPEN LT_ERR_INIT_OPEN -#define TB_ERR_MEM LT_ERR_MEM -#define TB_ERR_NO_EVENT LT_ERR_NO_EVENT -#define TB_ERR_NO_TERM LT_ERR_NO_TERM -#define TB_ERR_NOT_INIT LT_ERR_NOT_INIT -#define TB_ERR_OUT_OF_BOUNDS LT_ERR_OUT_OF_BOUNDS -#define TB_ERR_READ LT_ERR_READ -#define TB_ERR_RESIZE_IOCTL LT_ERR_RESIZE_IOCTL -#define TB_ERR_RESIZE_PIPE LT_ERR_RESIZE_PIPE +#define TB_OK LT_OK +#define TB_ERR LT_ERR +#define TB_ERR_NEED_MORE LT_ERR_NEED_MORE +#define TB_ERR_INIT_ALREADY LT_ERR_INIT_ALREADY +#define TB_ERR_INIT_OPEN LT_ERR_INIT_OPEN +#define TB_ERR_MEM LT_ERR_MEM +#define TB_ERR_NO_EVENT LT_ERR_NO_EVENT +#define TB_ERR_NO_TERM LT_ERR_NO_TERM +#define TB_ERR_NOT_INIT LT_ERR_NOT_INIT +#define TB_ERR_OUT_OF_BOUNDS LT_ERR_OUT_OF_BOUNDS +#define TB_ERR_READ LT_ERR_READ +#define TB_ERR_RESIZE_IOCTL LT_ERR_RESIZE_IOCTL +#define TB_ERR_RESIZE_PIPE LT_ERR_RESIZE_PIPE #define TB_ERR_RESIZE_SIGACTION LT_ERR_RESIZE_SIGACTION -#define TB_ERR_POLL LT_ERR_POLL -#define TB_ERR_TCGETATTR LT_ERR_TCGETATTR -#define TB_ERR_TCSETATTR LT_ERR_TCSETATTR +#define TB_ERR_POLL LT_ERR_POLL +#define TB_ERR_TCGETATTR LT_ERR_TCGETATTR +#define TB_ERR_TCSETATTR LT_ERR_TCSETATTR #define TB_ERR_UNSUPPORTED_TERM LT_ERR_UNSUPPORTED_TERM -#define TB_ERR_RESIZE_WRITE LT_ERR_RESIZE_WRITE -#define TB_ERR_RESIZE_POLL LT_ERR_RESIZE_POLL -#define TB_ERR_RESIZE_READ LT_ERR_RESIZE_READ -#define TB_ERR_RESIZE_SSCANF LT_ERR_RESIZE_SSCANF -#define TB_ERR_CAP_COLLISION LT_ERR_CAP_COLLISION +#define TB_ERR_RESIZE_WRITE LT_ERR_RESIZE_WRITE +#define TB_ERR_RESIZE_POLL LT_ERR_RESIZE_POLL +#define TB_ERR_RESIZE_READ LT_ERR_RESIZE_READ +#define TB_ERR_RESIZE_SSCANF LT_ERR_RESIZE_SSCANF +#define TB_ERR_CAP_COLLISION LT_ERR_CAP_COLLISION /* ---- event types ---- */ -#define TB_EVENT_KEY LT_EVENT_KEY +#define TB_EVENT_KEY LT_EVENT_KEY #define TB_EVENT_RESIZE LT_EVENT_RESIZE -#define TB_EVENT_MOUSE LT_EVENT_MOUSE +#define TB_EVENT_MOUSE LT_EVENT_MOUSE /* ---- modifiers ---- */ -#define TB_MOD_ALT LT_MOD_ALT -#define TB_MOD_CTRL LT_MOD_CTRL -#define TB_MOD_SHIFT LT_MOD_SHIFT +#define TB_MOD_ALT LT_MOD_ALT +#define TB_MOD_CTRL LT_MOD_CTRL +#define TB_MOD_SHIFT LT_MOD_SHIFT #define TB_MOD_MOTION LT_MOD_MOTION /* ---- input modes ---- */ #define TB_INPUT_CURRENT LT_INPUT_CURRENT -#define TB_INPUT_ESC LT_INPUT_ESC -#define TB_INPUT_ALT LT_INPUT_ALT -#define TB_INPUT_MOUSE LT_INPUT_MOUSE +#define TB_INPUT_ESC LT_INPUT_ESC +#define TB_INPUT_ALT LT_INPUT_ALT +#define TB_INPUT_MOUSE LT_INPUT_MOUSE /* ---- output modes ---- */ -#define TB_OUTPUT_CURRENT LT_OUTPUT_CURRENT -#define TB_OUTPUT_NORMAL LT_OUTPUT_NORMAL -#define TB_OUTPUT_256 LT_OUTPUT_256 -#define TB_OUTPUT_216 LT_OUTPUT_216 +#define TB_OUTPUT_CURRENT LT_OUTPUT_CURRENT +#define TB_OUTPUT_NORMAL LT_OUTPUT_NORMAL +#define TB_OUTPUT_256 LT_OUTPUT_256 +#define TB_OUTPUT_216 LT_OUTPUT_216 #define TB_OUTPUT_GRAYSCALE LT_OUTPUT_GRAYSCALE #define TB_OUTPUT_TRUECOLOR LT_OUTPUT_TRUECOLOR /* ---- colors ---- */ #define TB_DEFAULT LT_DEFAULT -#define TB_BLACK LT_BLACK -#define TB_RED LT_RED -#define TB_GREEN LT_GREEN -#define TB_YELLOW LT_YELLOW -#define TB_BLUE LT_BLUE +#define TB_BLACK LT_BLACK +#define TB_RED LT_RED +#define TB_GREEN LT_GREEN +#define TB_YELLOW LT_YELLOW +#define TB_BLUE LT_BLUE #define TB_MAGENTA LT_MAGENTA -#define TB_CYAN LT_CYAN -#define TB_WHITE LT_WHITE +#define TB_CYAN LT_CYAN +#define TB_WHITE LT_WHITE #define TB_RGB(r, g, b) LT_RGB((r), (g), (b)) /* ---- attributes ---- */ -#define TB_BOLD LT_BOLD +#define TB_BOLD LT_BOLD #define TB_UNDERLINE LT_UNDERLINE -#define TB_REVERSE LT_REVERSE -#define TB_ITALIC LT_ITALIC -#define TB_BLINK LT_BLINK -#define TB_DIM LT_DIM -#define TB_STRIKE LT_STRIKE +#define TB_REVERSE LT_REVERSE +#define TB_ITALIC LT_ITALIC +#define TB_BLINK LT_BLINK +#define TB_DIM LT_DIM +#define TB_STRIKE LT_STRIKE /* ---- keys (full termbox2 set; each has an LT_KEY_ twin) ---- */ -#define TB_KEY_CTRL_TILDE LT_KEY_CTRL_TILDE -#define TB_KEY_CTRL_2 LT_KEY_CTRL_2 -#define TB_KEY_CTRL_A LT_KEY_CTRL_A -#define TB_KEY_CTRL_B LT_KEY_CTRL_B -#define TB_KEY_CTRL_C LT_KEY_CTRL_C -#define TB_KEY_CTRL_D LT_KEY_CTRL_D -#define TB_KEY_CTRL_E LT_KEY_CTRL_E -#define TB_KEY_CTRL_F LT_KEY_CTRL_F -#define TB_KEY_CTRL_G LT_KEY_CTRL_G -#define TB_KEY_BACKSPACE LT_KEY_BACKSPACE -#define TB_KEY_CTRL_H LT_KEY_CTRL_H -#define TB_KEY_TAB LT_KEY_TAB -#define TB_KEY_CTRL_I LT_KEY_CTRL_I -#define TB_KEY_CTRL_J LT_KEY_CTRL_J -#define TB_KEY_CTRL_K LT_KEY_CTRL_K -#define TB_KEY_CTRL_L LT_KEY_CTRL_L -#define TB_KEY_ENTER LT_KEY_ENTER -#define TB_KEY_CTRL_M LT_KEY_CTRL_M -#define TB_KEY_CTRL_N LT_KEY_CTRL_N -#define TB_KEY_CTRL_O LT_KEY_CTRL_O -#define TB_KEY_CTRL_P LT_KEY_CTRL_P -#define TB_KEY_CTRL_Q LT_KEY_CTRL_Q -#define TB_KEY_CTRL_R LT_KEY_CTRL_R -#define TB_KEY_CTRL_S LT_KEY_CTRL_S -#define TB_KEY_CTRL_T LT_KEY_CTRL_T -#define TB_KEY_CTRL_U LT_KEY_CTRL_U -#define TB_KEY_CTRL_V LT_KEY_CTRL_V -#define TB_KEY_CTRL_W LT_KEY_CTRL_W -#define TB_KEY_CTRL_X LT_KEY_CTRL_X -#define TB_KEY_CTRL_Y LT_KEY_CTRL_Y -#define TB_KEY_CTRL_Z LT_KEY_CTRL_Z -#define TB_KEY_ESC LT_KEY_ESC +#define TB_KEY_CTRL_TILDE LT_KEY_CTRL_TILDE +#define TB_KEY_CTRL_2 LT_KEY_CTRL_2 +#define TB_KEY_CTRL_A LT_KEY_CTRL_A +#define TB_KEY_CTRL_B LT_KEY_CTRL_B +#define TB_KEY_CTRL_C LT_KEY_CTRL_C +#define TB_KEY_CTRL_D LT_KEY_CTRL_D +#define TB_KEY_CTRL_E LT_KEY_CTRL_E +#define TB_KEY_CTRL_F LT_KEY_CTRL_F +#define TB_KEY_CTRL_G LT_KEY_CTRL_G +#define TB_KEY_BACKSPACE LT_KEY_BACKSPACE +#define TB_KEY_CTRL_H LT_KEY_CTRL_H +#define TB_KEY_TAB LT_KEY_TAB +#define TB_KEY_CTRL_I LT_KEY_CTRL_I +#define TB_KEY_CTRL_J LT_KEY_CTRL_J +#define TB_KEY_CTRL_K LT_KEY_CTRL_K +#define TB_KEY_CTRL_L LT_KEY_CTRL_L +#define TB_KEY_ENTER LT_KEY_ENTER +#define TB_KEY_CTRL_M LT_KEY_CTRL_M +#define TB_KEY_CTRL_N LT_KEY_CTRL_N +#define TB_KEY_CTRL_O LT_KEY_CTRL_O +#define TB_KEY_CTRL_P LT_KEY_CTRL_P +#define TB_KEY_CTRL_Q LT_KEY_CTRL_Q +#define TB_KEY_CTRL_R LT_KEY_CTRL_R +#define TB_KEY_CTRL_S LT_KEY_CTRL_S +#define TB_KEY_CTRL_T LT_KEY_CTRL_T +#define TB_KEY_CTRL_U LT_KEY_CTRL_U +#define TB_KEY_CTRL_V LT_KEY_CTRL_V +#define TB_KEY_CTRL_W LT_KEY_CTRL_W +#define TB_KEY_CTRL_X LT_KEY_CTRL_X +#define TB_KEY_CTRL_Y LT_KEY_CTRL_Y +#define TB_KEY_CTRL_Z LT_KEY_CTRL_Z +#define TB_KEY_ESC LT_KEY_ESC #define TB_KEY_CTRL_LSQ_BRACKET LT_KEY_CTRL_LSQ_BRACKET -#define TB_KEY_CTRL_3 LT_KEY_CTRL_3 -#define TB_KEY_CTRL_4 LT_KEY_CTRL_4 -#define TB_KEY_CTRL_BACKSLASH LT_KEY_CTRL_BACKSLASH -#define TB_KEY_CTRL_5 LT_KEY_CTRL_5 +#define TB_KEY_CTRL_3 LT_KEY_CTRL_3 +#define TB_KEY_CTRL_4 LT_KEY_CTRL_4 +#define TB_KEY_CTRL_BACKSLASH LT_KEY_CTRL_BACKSLASH +#define TB_KEY_CTRL_5 LT_KEY_CTRL_5 #define TB_KEY_CTRL_RSQ_BRACKET LT_KEY_CTRL_RSQ_BRACKET -#define TB_KEY_CTRL_6 LT_KEY_CTRL_6 -#define TB_KEY_CTRL_7 LT_KEY_CTRL_7 -#define TB_KEY_CTRL_SLASH LT_KEY_CTRL_SLASH -#define TB_KEY_CTRL_UNDERSCORE LT_KEY_CTRL_UNDERSCORE -#define TB_KEY_SPACE LT_KEY_SPACE -#define TB_KEY_BACKSPACE2 LT_KEY_BACKSPACE2 -#define TB_KEY_CTRL_8 LT_KEY_CTRL_8 -#define TB_KEY_F1 LT_KEY_F1 -#define TB_KEY_F2 LT_KEY_F2 -#define TB_KEY_F3 LT_KEY_F3 -#define TB_KEY_F4 LT_KEY_F4 -#define TB_KEY_F5 LT_KEY_F5 -#define TB_KEY_F6 LT_KEY_F6 -#define TB_KEY_F7 LT_KEY_F7 -#define TB_KEY_F8 LT_KEY_F8 -#define TB_KEY_F9 LT_KEY_F9 -#define TB_KEY_F10 LT_KEY_F10 -#define TB_KEY_F11 LT_KEY_F11 -#define TB_KEY_F12 LT_KEY_F12 -#define TB_KEY_INSERT LT_KEY_INSERT -#define TB_KEY_DELETE LT_KEY_DELETE -#define TB_KEY_HOME LT_KEY_HOME -#define TB_KEY_END LT_KEY_END -#define TB_KEY_PGUP LT_KEY_PGUP -#define TB_KEY_PGDN LT_KEY_PGDN -#define TB_KEY_ARROW_UP LT_KEY_ARROW_UP -#define TB_KEY_ARROW_DOWN LT_KEY_ARROW_DOWN -#define TB_KEY_ARROW_LEFT LT_KEY_ARROW_LEFT -#define TB_KEY_ARROW_RIGHT LT_KEY_ARROW_RIGHT -#define TB_KEY_BACK_TAB LT_KEY_BACK_TAB -#define TB_KEY_MOUSE_LEFT LT_KEY_MOUSE_LEFT -#define TB_KEY_MOUSE_RIGHT LT_KEY_MOUSE_RIGHT -#define TB_KEY_MOUSE_MIDDLE LT_KEY_MOUSE_MIDDLE -#define TB_KEY_MOUSE_RELEASE LT_KEY_MOUSE_RELEASE -#define TB_KEY_MOUSE_WHEEL_UP LT_KEY_MOUSE_WHEEL_UP +#define TB_KEY_CTRL_6 LT_KEY_CTRL_6 +#define TB_KEY_CTRL_7 LT_KEY_CTRL_7 +#define TB_KEY_CTRL_SLASH LT_KEY_CTRL_SLASH +#define TB_KEY_CTRL_UNDERSCORE LT_KEY_CTRL_UNDERSCORE +#define TB_KEY_SPACE LT_KEY_SPACE +#define TB_KEY_BACKSPACE2 LT_KEY_BACKSPACE2 +#define TB_KEY_CTRL_8 LT_KEY_CTRL_8 +#define TB_KEY_F1 LT_KEY_F1 +#define TB_KEY_F2 LT_KEY_F2 +#define TB_KEY_F3 LT_KEY_F3 +#define TB_KEY_F4 LT_KEY_F4 +#define TB_KEY_F5 LT_KEY_F5 +#define TB_KEY_F6 LT_KEY_F6 +#define TB_KEY_F7 LT_KEY_F7 +#define TB_KEY_F8 LT_KEY_F8 +#define TB_KEY_F9 LT_KEY_F9 +#define TB_KEY_F10 LT_KEY_F10 +#define TB_KEY_F11 LT_KEY_F11 +#define TB_KEY_F12 LT_KEY_F12 +#define TB_KEY_INSERT LT_KEY_INSERT +#define TB_KEY_DELETE LT_KEY_DELETE +#define TB_KEY_HOME LT_KEY_HOME +#define TB_KEY_END LT_KEY_END +#define TB_KEY_PGUP LT_KEY_PGUP +#define TB_KEY_PGDN LT_KEY_PGDN +#define TB_KEY_ARROW_UP LT_KEY_ARROW_UP +#define TB_KEY_ARROW_DOWN LT_KEY_ARROW_DOWN +#define TB_KEY_ARROW_LEFT LT_KEY_ARROW_LEFT +#define TB_KEY_ARROW_RIGHT LT_KEY_ARROW_RIGHT +#define TB_KEY_BACK_TAB LT_KEY_BACK_TAB +#define TB_KEY_MOUSE_LEFT LT_KEY_MOUSE_LEFT +#define TB_KEY_MOUSE_RIGHT LT_KEY_MOUSE_RIGHT +#define TB_KEY_MOUSE_MIDDLE LT_KEY_MOUSE_MIDDLE +#define TB_KEY_MOUSE_RELEASE LT_KEY_MOUSE_RELEASE +#define TB_KEY_MOUSE_WHEEL_UP LT_KEY_MOUSE_WHEEL_UP #define TB_KEY_MOUSE_WHEEL_DOWN LT_KEY_MOUSE_WHEEL_DOWN /* ---- functions: clean 1:1 aliases ---- */ -#define tb_init lt_init -#define tb_init_fd lt_init_fd -#define tb_init_file lt_init_file -#define tb_shutdown lt_shutdown -#define tb_width lt_width -#define tb_height lt_height -#define tb_clear lt_clear +#define tb_init lt_init +#define tb_init_fd lt_init_fd +#define tb_init_file lt_init_file +#define tb_shutdown lt_shutdown +#define tb_width lt_width +#define tb_height lt_height +#define tb_clear lt_clear #define tb_set_clear_attrs lt_set_clear_attrs -#define tb_present lt_present -#define tb_invalidate lt_invalidate -#define tb_set_cursor lt_set_cursor -#define tb_hide_cursor lt_hide_cursor -#define tb_set_cell lt_set_cell -#define tb_set_cell_ex lt_set_cell_ex -#define tb_extend_cell lt_extend_cell -#define tb_print lt_print -#define tb_print_ex lt_print_ex -#define tb_printf lt_printf -#define tb_printf_ex lt_printf_ex -#define tb_send lt_send -#define tb_sendf lt_sendf -#define tb_peek_event lt_peek_event -#define tb_poll_event lt_poll_event -#define tb_get_fds lt_get_fds -#define tb_set_input_mode lt_set_input_mode +#define tb_present lt_present +#define tb_invalidate lt_invalidate +#define tb_set_cursor lt_set_cursor +#define tb_hide_cursor lt_hide_cursor +#define tb_set_cell lt_set_cell +#define tb_set_cell_ex lt_set_cell_ex +#define tb_extend_cell lt_extend_cell +#define tb_print lt_print +#define tb_print_ex lt_print_ex +#define tb_printf lt_printf +#define tb_printf_ex lt_printf_ex +#define tb_send lt_send +#define tb_sendf lt_sendf +#define tb_peek_event lt_peek_event +#define tb_poll_event lt_poll_event +#define tb_get_fds lt_get_fds +#define tb_set_input_mode lt_set_input_mode #define tb_set_output_mode lt_set_output_mode -#define tb_last_errno lt_last_errno -#define tb_strerror lt_strerror -#define tb_has_egc lt_has_egc -#define tb_attr_width lt_attr_width -#define tb_version lt_version -#define tb_iswprint lt_iswprint -#define tb_wcwidth lt_wcwidth -#define tb_utf8_char_length lt_utf8_char_length +#define tb_last_errno lt_last_errno +#define tb_strerror lt_strerror +#define tb_has_egc lt_has_egc +#define tb_attr_width lt_attr_width +#define tb_version lt_version +#define tb_iswprint lt_iswprint +#define tb_wcwidth lt_wcwidth +#define tb_utf8_char_length lt_utf8_char_length #define tb_utf8_char_to_unicode lt_utf8_char_to_unicode #define tb_utf8_unicode_to_char lt_utf8_unicode_to_char /* ---- allocator indirection -> libc ---- */ -#define tb_malloc malloc +#define tb_malloc malloc #define tb_realloc realloc -#define tb_free free +#define tb_free free /* ---- bucket B: adapters (libterm-backed, divergent shape) ---- */ #define tb_put_cell(x, y, c) lt_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) @@ -254,15 +254,14 @@ static inline int tb_get_cell(int x, int y, int back, struct tb_cell **cell) { /* ---- bucket C: unsupported. Using one is a compile error; the message is * carried in the (deliberately undeclared) identifier name. ---- */ -#define tb_init_rwfd(rfd, wfd) \ +#define tb_init_rwfd(rfd, wfd) \ LIBTERM_COMPAT_UNSUPPORTED_tb_init_rwfd__use_lt_init_fd_or_lt_init_file -#define tb_set_func(type, fn) \ +#define tb_set_func(type, fn) \ LIBTERM_COMPAT_UNSUPPORTED_tb_set_func__deprecated_upstream_no_libterm_equivalent -#define tb_has_truecolor() \ +#define tb_has_truecolor() \ LIBTERM_COMPAT_UNSUPPORTED_tb_has_truecolor__use_lt_detect_color_depth -#define tb_cell_buffer() \ +#define tb_cell_buffer() \ LIBTERM_COMPAT_UNSUPPORTED_tb_cell_buffer__no_raw_buffer_use_lt_get_cell -#define tb_key_i(i) \ - LIBTERM_COMPAT_UNSUPPORTED_tb_key_i__no_terminfo_cap_table +#define tb_key_i(i) LIBTERM_COMPAT_UNSUPPORTED_tb_key_i__no_terminfo_cap_table #endif /* LIBTERM_COMPAT_TERMBOX2_H */ diff --git a/examples/editor.c b/examples/editor.c index e1f53f6..3672e05 100644 --- a/examples/editor.c +++ b/examples/editor.c @@ -220,7 +220,8 @@ int main(int argc, char **argv) { * ch + LT_MOD_CTRL (key == 0). This editor is written in the termbox2 idiom * (ev.key == TB_KEY_CTRL_S/Q/...), so opt into termbox2 control-byte * semantics — the one documented adaptation a termbox2 program needs. This - * is the only LT_* symbol the editor references; everything else is tb_/TB_. */ + * is the only LT_* symbol the editor references; everything else is tb_/TB_. + */ tb_set_input_mode(LT_INPUT_COMPAT); for (;;) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6878280..19eafcb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,14 +45,17 @@ add_test(NAME compat_smoke COMMAND compat_smoke) # White-box regression test for examples/editor.c's buffer model (#includes the # editor TU with main() compiled out). Guards the insert_newline use-after-free. -# Platform-agnostic — no terminal needed. -add_executable(test_compat_editor test_compat_editor.c) -target_link_libraries(test_compat_editor PRIVATE ${LIBTERM_LINK_TARGET}) -target_include_directories(test_compat_editor PRIVATE - ${PROJECT_SOURCE_DIR}/examples - ${PROJECT_SOURCE_DIR}/compat - ${PROJECT_SOURCE_DIR}/include) -add_test(NAME test_compat_editor COMMAND test_compat_editor) +# POSIX-only: the editor uses getline(), which MinGW does not declare; examples +# themselves are already EXAMPLES=OFF on the Windows CI lanes. +if(NOT WIN32) + add_executable(test_compat_editor test_compat_editor.c) + target_link_libraries(test_compat_editor PRIVATE ${LIBTERM_LINK_TARGET}) + target_include_directories(test_compat_editor PRIVATE + ${PROJECT_SOURCE_DIR}/examples + ${PROJECT_SOURCE_DIR}/compat + ${PROJECT_SOURCE_DIR}/include) + add_test(NAME test_compat_editor COMMAND test_compat_editor) +endif() # Dispatch builds: tell the SIMD tests which backends were compiled and # register the dispatcher sanity test. (Static builds: no-op.) diff --git a/tests/compat_smoke.c b/tests/compat_smoke.c index 5d807b9..d965732 100644 --- a/tests/compat_smoke.c +++ b/tests/compat_smoke.c @@ -19,7 +19,12 @@ int main(void) { int (*pset)(int, int, uint32_t, uintattr_t, uintattr_t) = tb_set_cell; int (*ppoll)(struct tb_event *) = tb_poll_event; int (*pget)(int, int, int, struct tb_cell **) = tb_get_cell; - (void)cell; (void)ev; (void)attr; (void)pset; (void)ppoll; (void)pget; + (void)cell; + (void)ev; + (void)attr; + (void)pset; + (void)ppoll; + (void)pget; /* Every value constant resolves (sum forces each token to be defined). */ sink += (long long)(TB_OK); @@ -153,7 +158,8 @@ int main(void) { sink += (long long)(TB_KEY_MOUSE_WHEEL_UP); sink += (long long)(TB_KEY_MOUSE_WHEEL_DOWN); - /* Every function/allocator alias resolves (designator referenced + discarded). */ + /* Every function/allocator alias resolves (designator referenced + + * discarded). */ (void)(tb_init); (void)(tb_init_fd); (void)(tb_init_file); @@ -195,7 +201,9 @@ int main(void) { (void)(tb_free); (void)tb_get_cell; /* static-inline adapter designator */ - if (0) { tb_put_cell(0, 0, &cell); } /* function-like macro expands + links */ + if (0) { + tb_put_cell(0, 0, &cell); + } /* function-like macro expands + links */ (void)sink; return 0;