diff --git a/compat/termbox2.h b/compat/termbox2.h new file mode 100644 index 0000000..1c5ffdd --- /dev/null +++ b/compat/termbox2.h @@ -0,0 +1,267 @@ +/* + * 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/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..3672e05 --- /dev/null +++ b/examples/editor.c @@ -0,0 +1,259 @@ +#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) { + /* 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 - 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(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; + /* 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; +} + +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; +} + +/* 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) + load_file(g_filename); + else + append_row("", 0); + + 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(); + 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; +} +#endif /* EDITOR_NO_MAIN */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9d2cb3d..19eafcb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,29 @@ 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) + +# 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. +# 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.) if(LIBTERM_SIMD_BACKENDS) @@ -164,6 +187,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/compat_smoke.c b/tests/compat_smoke.c new file mode 100644 index 0000000..d965732 --- /dev/null +++ b/tests/compat_smoke.c @@ -0,0 +1,210 @@ +/* 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 + +int main(void) { + struct tb_cell cell; + struct tb_event ev; + uintattr_t attr = TB_RGB(1, 2, 3); + long long sink = 0; + + /* 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 (*pget)(int, int, int, struct tb_cell **) = tb_get_cell; + (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); + 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); + + /* 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); + + (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; +} 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; +} 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; +}