diff options
41 files changed, 997 insertions, 361 deletions
@@ -2,7 +2,7 @@ term is a headless terminal core: the parser, screen ring, logical-line history store, write core, and viewport snapshots. It is a C11 library -with no dependency but libm. +with no dependency but libm and, on POSIX, the thread library (`-pthread`). | Path | Contents | | --- | --- | @@ -23,14 +23,16 @@ Pick one of these: sets the lowest instruction set, such as `-march=x86-64-v3`. - **Amalgamation**: run `tools/amalgamate.py . OUT`. It writes `OUT/term.c` and `OUT/term.h`. Compile `term.c` with any C11 compiler - and link libm. A Rust `build.rs` needs one + and link libm and the thread library: `cc -std=c11 -pthread -c term.c` + on POSIX. A Rust `build.rs` needs one `cc::Build::new().file("term.c").compile("term")`. - **Sources**: compile `src/*.c` and `src/osc/*.c` once, with `src` and - `include/term` on the include path. + `include/term` on the include path. Link the thread library too. -The library needs nothing else. The `*-amalgamation` tests run the term -tests against the generated `term.c`, and `term-cross-test` compiles it -for aarch64, macOS, and Windows. +The library needs nothing else. On Windows the writer lock is an SRWLOCK +from the system library, so it needs no extra link. The `*-amalgamation` +tests run the term tests against the generated `term.c`, and +`term-cross-test` compiles it for aarch64, macOS, and Windows. ### SIMD level @@ -50,8 +52,11 @@ no flags to get them: `simd.h` states the rules for a new kernel. -- `term.h` is the only header a host includes. It includes - `term_input.h` and `term_types.h`. It compiles as C11 and as C++. +- `term.h` is the only header a host includes. A host writes + `#include <term/term.h>`. The package adds only the include directory, + so `term.h` from another library, such as ncurses, is not shadowed. The + header includes `term_input.h` and `term_types.h`. It compiles as C11 + and as C++. - Every global symbol starts with `term_`. The library builds with hidden symbols, so a shared build exports only the `TERM_API` functions of `term.h`. `term-symbols-test` checks both. @@ -65,7 +70,8 @@ no flags to get them: A host wires the terminal in four places: 1. Create it with `term_new()`. Set `term_options.callbacks.reply` to - send replies to the program. + send replies to the program. Set `term_options.name` to the name + programs read with XTVERSION and XTGETTCAP. It defaults to `term`. 2. Feed program output with `term_feed()`. 3. Translate key and mouse events into `struct term_key_event` and `struct term_mouse_event`. Encode them with `term_input_encode_key()` diff --git a/benchmarks/term_write_bench.c b/benchmarks/term_write_bench.c index 50853f6..7649ed8 100644 --- a/benchmarks/term_write_bench.c +++ b/benchmarks/term_write_bench.c @@ -152,7 +152,10 @@ int main(int argc, char **argv) .size = { .x = cols_val, .y = 40 }, .scrollback = BENCH_SCROLLBACK, }); - if (!t) return EXIT_FAILURE; + if (!t) { + fprintf(stderr, "term_new failed: TERM_COLS must be 0 to %d\n", TERM_MAX_COLS); + return EXIT_FAILURE; + } /* manyline has bare LFs: each one also returns the cursor to * column 0, as a tty with ONLCR does. */ unsigned flags = !strcmp(cases[c].name, "manyline") ? TERM_FEED_ONLCR : 0; diff --git a/include/term/term.h b/include/term/term.h index 2447b72..380dfd3 100644 --- a/include/term/term.h +++ b/include/term/term.h @@ -17,8 +17,9 @@ * * Threads. Any thread can call any function. Each call that reads or changes * a terminal enters its writer section, so calls on one terminal run one at a - * time. A snapshot belongs to one thread at a time. term_free() is the last - * call on a terminal. + * time. A thread that waits for the section sleeps. On POSIX the section is a + * mutex with priority inheritance where the platform has it. A snapshot + * belongs to one thread at a time. term_free() is the last call on a terminal. */ #include <stdbool.h> @@ -41,6 +42,16 @@ struct term_snapshot; #define TERM_SCROLLBACK_DEFAULT 10000 /* + * The version of this header. A host compares TERM_VERSION_CODE with + * term_version() to check that the library matches the header. + */ +#define TERM_VERSION(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) +#define TERM_VERSION_MAJOR 0 +#define TERM_VERSION_MINOR 1 +#define TERM_VERSION_PATCH 0 +#define TERM_VERSION_CODE TERM_VERSION(TERM_VERSION_MAJOR, TERM_VERSION_MINOR, TERM_VERSION_PATCH) + +/* * Colors. A term_color is either a direct color (alpha 0xFF, then RGB) or a * palette slot: an index 0-255, the default foreground, or the default * background. A slot resolves when a snapshot is painted, so a theme change @@ -139,20 +150,36 @@ struct term_callbacks { void (*trace)(void *ud, const char *line); }; -/* Options for term_new(). An all-zero value is valid. */ +/* + * Options for term_new(). An all-zero value is valid. A size component of 0 + * selects 80 columns or 24 rows. A negative component, or columns above + * TERM_MAX_COLS, makes term_new() fail. A negative scrollback fails too. + * cursor_style must be a value of enum term_cursor_style. + */ struct term_options { - struct term_vector2i size; /* columns and rows; each is at least 1 */ + struct term_vector2i size; /* columns and rows */ int scrollback; /* history lines; 0 selects TERM_SCROLLBACK_DEFAULT */ struct term_theme theme; enum term_cursor_style cursor_style; bool cursor_steady; /* false: the cursor blinks */ + /* + * The terminal's name. Programs read it with XTVERSION and XTGETTCAP. + * It is also the title of a notification that has none. NULL or "" + * selects "term". Otherwise 1 to 63 printable ASCII bytes, or term_new() + * returns NULL. term_new() copies the name. + */ + const char *name; struct term_callbacks callbacks; + /* Must be zero. term_new() returns NULL if any word is nonzero, so a + * later version can give a word a meaning. */ + uint32_t reserved[8]; }; -/* NULL when memory runs out. */ /* The Unicode version of the width tables, such as "18.0.0". A host can * pass it to programs as UNICODE_VERSION. */ TERM_API const char *term_unicode_version(void); +/* The TERM_VERSION_CODE the library was built with. */ +TERM_API unsigned term_version(void); /* The length of the prefix of buf made of bytes in [0x20, hi). With hi * 0x7f, it finds a run of printable ASCII in UTF-8 text, so a host can @@ -165,18 +192,21 @@ TERM_API size_t term_scan_printable_span(const char *buf, size_t len, unsigned c * and comparisons. */ TERM_API const char *term_isa(void); +/* NULL when memory runs out or an option is invalid. */ TERM_API struct term *term_new(const struct term_options *opt); TERM_API void term_free(struct term *t); /* Hold the writer section across several calls, so that no other thread - * changes the terminal between them. The section is a spin lock: keep it - * short. The thread that holds it can call every function, and it can enter - * the section again. */ + * changes the terminal between them. The section is a mutex, and a waiting + * thread sleeps. Keep it short. The thread that holds it can call every + * function, and it can enter the section again. Each term_lock() needs one + * term_unlock(). */ TERM_API void term_lock(struct term *t); TERM_API void term_unlock(struct term *t); -/* Change the size in cells. Returns false when memory runs out; the terminal - * is then unchanged. The screen reflows to the new width. */ +/* Change the size in cells. Returns false, and the terminal is unchanged, + * when a component is below 1, when cols is above TERM_MAX_COLS, or when + * memory runs out. The screen reflows to the new width. */ TERM_API bool term_resize(struct term *t, struct term_vector2i size); /* Replace the theme. A different theme also drops the colors that the @@ -197,7 +227,12 @@ enum term_feed_flags { TERM_FEED_ONLCR = 1u << 0, }; -TERM_API void term_feed(struct term *t, const char *bytes, size_t len, unsigned flags); +/* + * Feed bytes from the program. Returns false, and changes nothing, when t is + * NULL, when bytes is NULL and len is not 0, or when flags has a bit that is + * not in enum term_feed_flags. A len of 0 is a no-op that returns true. + */ +TERM_API bool term_feed(struct term *t, const char *bytes, size_t len, unsigned flags); /* Report a focus change. The terminal replies only in focus-event mode. */ TERM_API void term_focus(struct term *t, bool focused); @@ -408,11 +443,16 @@ struct term_view { const uint32_t *palette; /* TERM_PALETTE_SIZE ARGB entries */ }; +/* The view of s, NULL for a NULL s. The view and view->palette keep their + * address until term_snapshot_free(); their contents change on each update. */ TERM_API const struct term_view *term_snapshot_view(const struct term_snapshot *s); -/* The cells of viewport row y: view->size.x entries. */ +/* The cells of viewport row y: view->size.x entries. NULL when y is outside + * the viewport or s is NULL. An update can move the cells, so the pointer is + * valid only until the next term_snapshot_update() or term_snapshot_free() of s. */ TERM_API const struct term_cell *term_snapshot_row(const struct term_snapshot *s, int y); /* An identity for the content of row y. Two rows with the same key hold the - * same cells, also when the content moved to another row. */ + * same cells, also when the content moved to another row. 0 when y is outside + * the viewport or s is NULL. */ TERM_API uint64_t term_snapshot_row_key(const struct term_snapshot *s, int y); /* Number of rows in history above the screen. 0 if alternate screen or no history. */ @@ -423,10 +463,13 @@ TERM_API int term_history_rows(const struct term *t); * an empty string; NULL means allocation failed or t is NULL. Release the * result with term_text_free(). */ TERM_API char *term_history_text(struct term *t, int max_rows); +/* The selected columns of viewport row y, end exclusive. An empty span when y + * is outside the viewport or s is NULL. */ TERM_API struct term_span term_snapshot_row_selection(const struct term_snapshot *s, int y); /* The code points of a cluster cell. Returns the number of code points and - * writes at most cap of them. */ + * writes at most cap of them. Writes nothing when cps is NULL or cap is 0. + * Returns 0 when s is NULL. */ TERM_API int term_snapshot_cluster(const struct term_snapshot *s, uint32_t text, uint32_t *cps, int cap); /* The ARGB value of a color in a view. */ diff --git a/include/term/term_input.h b/include/term/term_input.h index c317bea..19d9355 100644 --- a/include/term/term_input.h +++ b/include/term/term_input.h @@ -1,5 +1,5 @@ -#ifndef TERM_INPUT_TYPES_H -#define TERM_INPUT_TYPES_H +#ifndef TERM_INPUT_H +#define TERM_INPUT_H #include <stdbool.h> #include <stddef.h> @@ -199,4 +199,4 @@ struct term_input_result { } #endif -#endif /* TERM_INPUT_TYPES_H */ +#endif /* TERM_INPUT_H */ diff --git a/include/term/unicode/grapheme.h b/include/term/unicode/grapheme.h index 5573949..0946bb6 100644 --- a/include/term/unicode/grapheme.h +++ b/include/term/unicode/grapheme.h @@ -5,20 +5,20 @@ #include <stddef.h> #include <stdint.h> -#define GRAPHEME_MAX_LEN 20 -#define GRAPHEME_HANDLE_BASE 0x7F000000u +#define TERM_GRAPHEME_MAX_LEN 20 +#define TERM_GRAPHEME_HANDLE_BASE 0x7F000000u -static inline bool grapheme_is_handle(uint32_t c) +static inline bool term_grapheme_is_handle(uint32_t c) { - return c >= GRAPHEME_HANDLE_BASE; + return c >= TERM_GRAPHEME_HANDLE_BASE; } -static inline bool grapheme_regional_indicator(uint32_t cp) +static inline bool term_grapheme_regional_indicator(uint32_t cp) { return cp >= 0x1F1E6 && cp <= 0x1F1FF; } -static inline bool grapheme_emoji_modifier(uint32_t cp) +static inline bool term_grapheme_emoji_modifier(uint32_t cp) { return cp >= 0x1F3FB && cp <= 0x1F3FF; } diff --git a/include/term/unicode/unicode_data.inc b/include/term/unicode/unicode_data.inc index 8053b01..4ab1461 100644 --- a/include/term/unicode/unicode_data.inc +++ b/include/term/unicode/unicode_data.inc @@ -1,8 +1,8 @@ /* Generated from Unicode 18.0.0 data by tools/generate_unicode.py. */ -#define UNICODE_DATA_VERSION "18.0.0" +#define TERM_UNICODE_DATA_VERSION "18.0.0" -static const struct unicode_range zero_width[] = { +static const struct term_unicode_range term_zero_width[] = { {0xAD,0xAD},{0x300,0x36F},{0x483,0x489},{0x591,0x5BD}, {0x5BF,0x5BF},{0x5C1,0x5C2},{0x5C4,0x5C5},{0x5C7,0x5C9}, {0x600,0x605},{0x610,0x61A},{0x61C,0x61C},{0x64B,0x65F}, @@ -101,11 +101,11 @@ static const struct unicode_range zero_width[] = { {0xE0020,0xE007F},{0xE0100,0xE01EF}, }; -static const struct unicode_range nonprintable[] = { +static const struct term_unicode_range term_nonprintable[] = { {0x0,0x1F},{0x7F,0x9F},{0xD800,0xDFFF}, }; -static const struct unicode_range unicode_wide[] = { +static const struct term_unicode_range term_unicode_wide[] = { {0x1100,0x115F},{0x231A,0x231B},{0x2329,0x232A},{0x23E9,0x23EC}, {0x23F0,0x23F0},{0x23F3,0x23F3},{0x25FD,0x25FE},{0x2614,0x2615}, {0x2630,0x2637},{0x2648,0x2653},{0x267F,0x267F},{0x268A,0x268F}, @@ -140,7 +140,7 @@ static const struct unicode_range unicode_wide[] = { {0x20000,0x2FFFD},{0x30000,0x3FFFD}, }; -static const struct unicode_range spacing_mark[] = { +static const struct term_unicode_range term_spacing_mark[] = { {0x903,0x903},{0x93B,0x93B},{0x93E,0x940},{0x949,0x94C}, {0x94E,0x94F},{0x982,0x983},{0x9BE,0x9C0},{0x9C7,0x9C8}, {0x9CB,0x9CC},{0x9D7,0x9D7},{0xA03,0xA03},{0xA3E,0xA40}, @@ -192,7 +192,7 @@ static const struct unicode_range spacing_mark[] = { {0x1D16D,0x1D172},{0x1D250,0x1D252},{0x1D25F,0x1D25F},{0x1D280,0x1D281}, }; -static const struct unicode_range extended_pictographic[] = { +static const struct term_unicode_range term_extended_pictographic[] = { {0xA9,0xA9},{0xAE,0xAE},{0x203C,0x203C},{0x2049,0x2049}, {0x2122,0x2122},{0x2139,0x2139},{0x2194,0x2199},{0x21A9,0x21AA}, {0x231A,0x231B},{0x2328,0x2328},{0x23CF,0x23CF},{0x23E9,0x23F3}, @@ -234,7 +234,7 @@ static const struct unicode_range extended_pictographic[] = { {0x1F947,0x1F9FF},{0x1FA58,0x1FA5F},{0x1FA6E,0x1FAFF},{0x1FC00,0x1FFFD}, }; -static const struct unicode_range emoji_presentation[] = { +static const struct term_unicode_range term_emoji_presentation[] = { {0x231A,0x231B},{0x23E9,0x23EC},{0x23F0,0x23F0},{0x23F3,0x23F3}, {0x25FD,0x25FE},{0x2614,0x2615},{0x2648,0x2653},{0x267F,0x267F}, {0x2693,0x2693},{0x26A1,0x26A1},{0x26AA,0x26AB},{0x26BD,0x26BE}, diff --git a/include/term/unicode/utf8.h b/include/term/unicode/utf8.h index 1c3cc3f..ad44465 100644 --- a/include/term/unicode/utf8.h +++ b/include/term/unicode/utf8.h @@ -1,19 +1,19 @@ -#ifndef UTF_8_H -#define UTF_8_H +#ifndef TERM_UNICODE_UTF8_H +#define TERM_UNICODE_UTF8_H #include <stdint.h> #include <stddef.h> #include <stdlib.h> #include <string.h> -#define UTF8_ACCEPT 0 -#define UTF8_REJECT 1 +#define TERM_UTF8_ACCEPT 0 +#define TERM_UTF8_REJECT 1 -#define UTF8_3BYTE_MIN 0x800u -#define UTF8_SURROGATE_LO 0xd800u -#define UTF8_SURROGATE_HI 0xdfffu +#define TERM_UTF8_3BYTE_MIN 0x800u +#define TERM_UTF8_SURROGATE_LO 0xd800u +#define TERM_UTF8_SURROGATE_HI 0xdfffu -static const uint8_t utf8d[] = { +static const uint8_t term_utf8d[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -30,19 +30,19 @@ static const uint8_t utf8d[] = { 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, }; -static inline uint32_t utf8_decode(uint32_t *state, uint32_t *codep, uint32_t byte) { +static inline uint32_t term_utf8_decode(uint32_t *state, uint32_t *codep, uint32_t byte) { byte &= 0xff; - uint32_t type = utf8d[byte]; + uint32_t type = term_utf8d[byte]; - *codep = (*state != UTF8_ACCEPT) ? + *codep = (*state != TERM_UTF8_ACCEPT) ? (byte & 0x3fu) | (*codep << 6) : (0xff >> type) & (byte); - *state = utf8d[256 + *state*16 + type]; + *state = term_utf8d[256 + *state*16 + type]; return *state; } -static const uint8_t utf8_acc_state[256] = { +static const uint8_t term_utf8_acc_state[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -60,7 +60,7 @@ static const uint8_t utf8_acc_state[256] = { 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 6, 7, 7, 7, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, }; -static const uint8_t utf8_acc_codep[256] = { +static const uint8_t term_utf8_acc_codep[256] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, @@ -86,7 +86,7 @@ static const uint8_t utf8_acc_codep[256] = { * ends an unfinished sequence the same way. An unfinished sequence at the * end of buf gives *cp = 0 and keeps the state. */ -static inline int utf8_decode_ground(uint32_t *state, uint32_t *codep, +static inline int term_utf8_decode_ground(uint32_t *state, uint32_t *codep, const char *buf, size_t len, uint32_t *cp) { uint32_t st = *state; uint32_t cpv = *codep; @@ -97,25 +97,25 @@ static inline int utf8_decode_ground(uint32_t *state, uint32_t *codep, do { unsigned char c = (unsigned char)buf[n]; if (c < 0x20) { - if (st != UTF8_ACCEPT) *cp = 0xFFFD; - st = UTF8_ACCEPT; + if (st != TERM_UTF8_ACCEPT) *cp = 0xFFFD; + st = TERM_UTF8_ACCEPT; break; } uint32_t before = st; n++; - if (st == UTF8_ACCEPT) { - cpv = utf8_acc_codep[c]; - st = utf8_acc_state[c]; + if (st == TERM_UTF8_ACCEPT) { + cpv = term_utf8_acc_codep[c]; + st = term_utf8_acc_state[c]; } else { - uint32_t type = utf8d[c]; + uint32_t type = term_utf8d[c]; cpv = (c & 0x3fu) | (cpv << 6); - st = utf8d[256 + st*16 + type]; + st = term_utf8d[256 + st*16 + type]; } - if (st == UTF8_ACCEPT) { *cp = cpv; break; } - if (st == UTF8_REJECT) { - st = UTF8_ACCEPT; + if (st == TERM_UTF8_ACCEPT) { *cp = cpv; break; } + if (st == TERM_UTF8_REJECT) { + st = TERM_UTF8_ACCEPT; *cp = 0xFFFD; - if (before != UTF8_ACCEPT) n--; + if (before != TERM_UTF8_ACCEPT) n--; break; } } while ((size_t)n < len); @@ -125,7 +125,7 @@ static inline int utf8_decode_ground(uint32_t *state, uint32_t *codep, } static inline int -utf8_decode_one(const char *buf, size_t len, uint32_t *codep) +term_utf8_decode_one(const char *buf, size_t len, uint32_t *codep) { const unsigned char *s = (const unsigned char *)buf; unsigned c0, c1, c2, c3; @@ -159,8 +159,8 @@ utf8_decode_one(const char *buf, size_t len, uint32_t *codep) if ((c1 & 0xc0) != 0x80 || (c2 & 0xc0) != 0x80) return 0; cp = ((c0 & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); - if (cp < UTF8_3BYTE_MIN || - (cp >= UTF8_SURROGATE_LO && cp <= UTF8_SURROGATE_HI)) + if (cp < TERM_UTF8_3BYTE_MIN || + (cp >= TERM_UTF8_SURROGATE_LO && cp <= TERM_UTF8_SURROGATE_HI)) return 0; *codep = cp; return 3; @@ -183,14 +183,14 @@ utf8_decode_one(const char *buf, size_t len, uint32_t *codep) } static inline void -utf8_sanitize(char *dst, size_t cap, const char *src) +term_utf8_sanitize(char *dst, size_t cap, const char *src) { if (!cap) return; size_t i = 0, len = strlen(src), off = 0; while (off < len && i + 1 < cap) { uint32_t cp; - int n = utf8_decode_one(src + off, len - off, &cp); + int n = term_utf8_decode_one(src + off, len - off, &cp); if (n <= 0 || i + (size_t)n >= cap) { off++; continue; @@ -204,17 +204,17 @@ utf8_sanitize(char *dst, size_t cap, const char *src) /* A sanitized copy of src. Sanitizing only drops bytes, so the copy is * never longer than src. */ -static inline char *utf8_sanitize_dup(const char *src) +static inline char *term_utf8_sanitize_dup(const char *src) { size_t cap = strlen(src) + 1; - char *dst = malloc(cap); + char *dst = (char *)malloc(cap); if (dst) - utf8_sanitize(dst, cap, src); + term_utf8_sanitize(dst, cap, src); return dst; } -#define UTF8_MAX_BYTES 4 -static inline int utf8_encode(uint32_t cp, char *out) +#define TERM_UTF8_MAX_BYTES 4 +static inline int term_utf8_encode(uint32_t cp, char *out) { if (cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) cp = 0xfffd; diff --git a/include/term/unicode/width.h b/include/term/unicode/width.h index a7dfe66..a85ec77 100644 --- a/include/term/unicode/width.h +++ b/include/term/unicode/width.h @@ -5,15 +5,15 @@ #include <stddef.h> #include <stdint.h> -struct unicode_range { uint32_t lo, hi; }; +struct term_unicode_range { uint32_t lo, hi; }; #include "unicode_data.inc" -static inline bool in_ranges(const struct unicode_range *ranges, size_t count, uint32_t cp) +static inline bool term_in_ranges(const struct term_unicode_range *ranges, size_t count, uint32_t cp) { if (count == 0 || cp < ranges[0].lo || cp > ranges[count - 1].hi) return false; - const struct unicode_range *base = ranges; + const struct term_unicode_range *base = ranges; size_t n = count; while (n > 1) { size_t half = n / 2; @@ -23,34 +23,34 @@ static inline bool in_ranges(const struct unicode_range *ranges, size_t count, u return cp <= base->hi; } -#define IN_TABLE(table, cp) in_ranges(table, sizeof(table) / sizeof(table[0]), cp) +#define TERM_IN_TABLE(table, cp) term_in_ranges(table, sizeof(table) / sizeof(table[0]), cp) -static inline int unicode_width(uint32_t cp) +static inline int term_unicode_width(uint32_t cp) { - if (cp > 0x10FFFF || IN_TABLE(nonprintable, cp)) + if (cp > 0x10FFFF || TERM_IN_TABLE(term_nonprintable, cp)) return -1; - if (IN_TABLE(zero_width, cp)) + if (TERM_IN_TABLE(term_zero_width, cp)) return 0; - return IN_TABLE(unicode_wide, cp) ? 2 : 1; + return TERM_IN_TABLE(term_unicode_wide, cp) ? 2 : 1; } -static inline bool unicode_extended_pictographic(uint32_t cp) +static inline bool term_unicode_extended_pictographic(uint32_t cp) { - return IN_TABLE(extended_pictographic, cp); + return TERM_IN_TABLE(term_extended_pictographic, cp); } -static inline bool unicode_emoji_presentation(uint32_t cp) +static inline bool term_unicode_emoji_presentation(uint32_t cp) { - return IN_TABLE(emoji_presentation, cp); + return TERM_IN_TABLE(term_emoji_presentation, cp); } -static inline bool unicode_spacing_mark(uint32_t cp) +static inline bool term_unicode_spacing_mark(uint32_t cp) { if (cp < 0x903 || ((cp - 0xABEDu) < (0x11000u - 0xABEDu)) || cp > 0x1D281) return false; - return IN_TABLE(spacing_mark, cp); + return TERM_IN_TABLE(term_spacing_mark, cp); } -#undef IN_TABLE +#undef TERM_IN_TABLE #endif diff --git a/meson.build b/meson.build index d23c0b4..e25bf13 100644 --- a/meson.build +++ b/meson.build @@ -15,6 +15,8 @@ project('term', 'c', cc = meson.get_compiler('c') m_dep = cc.find_library('m', required: false) +# The writer lock is a pthread mutex on POSIX and an SRWLOCK on Windows. +thread_dep = dependency('threads') if get_option('arch_flag') != '' add_project_arguments(get_option('arch_flag'), language: 'c') endif @@ -29,7 +31,7 @@ subdir('src') term_dep = declare_dependency( include_directories: term_inc, link_with: term_lib.get_static_lib(), - dependencies: m_dep, + dependencies: [m_dep, thread_dep], ) meson.override_dependency('term', term_dep) @@ -37,7 +39,7 @@ meson.override_dependency('term', term_dep) term_internal_dep = declare_dependency( include_directories: [term_inc, term_private_inc], link_with: term_lib.get_static_lib(), - dependencies: m_dep, + dependencies: [m_dep, thread_dep], ) # The short forms of term.h that the term tests use. A host project can use @@ -70,7 +72,7 @@ pkg.generate( name: 'term', description: 'Headless terminal emulator core library', version: meson.project_version(), - subdirs: ['.', 'term'], + subdirs: ['.'], ) if get_option('tests') diff --git a/src/compat.h b/src/compat.h index cb62679..6c3302f 100644 --- a/src/compat.h +++ b/src/compat.h @@ -14,6 +14,19 @@ #include <stdlib.h> #include <string.h> +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#else +#include <pthread.h> +#include <unistd.h> +#endif + #if defined(__GNUC__) || defined(__clang__) #define TERM_COMPILER_GNU 1 #elif defined(_MSC_VER) @@ -184,6 +197,68 @@ static inline void term_cpu_relax(void) #endif } +/* The writer lock. POSIX uses a mutex with priority inheritance where the + * platform has it, so a waiter lifts the thread that holds the lock. Windows + * uses an SRWLOCK. Neither nests, so term_write_begin() counts re-entry. */ +#if defined(_WIN32) +typedef SRWLOCK term_mutex; +#else +typedef pthread_mutex_t term_mutex; +/* Strict C11 can hide PTHREAD_PRIO_INHERIT in pthread.h, as zig's aarch64 + * glibc headers do. Every POSIX system that has the option uses 1 for it. */ +#if !defined(PTHREAD_PRIO_INHERIT) +#define PTHREAD_PRIO_INHERIT 1 +#endif +#endif + +/* Returns false when the lock cannot be made. */ +static inline bool term_mutex_init(term_mutex *m) +{ +#if defined(_WIN32) + InitializeSRWLock(m); + return true; +#elif defined(_POSIX_THREAD_PRIO_INHERIT) && _POSIX_THREAD_PRIO_INHERIT > 0 + pthread_mutexattr_t attr; + if (pthread_mutexattr_init(&attr) != 0) + return false; + bool ok = pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT) == 0 && + pthread_mutex_init(m, &attr) == 0; + pthread_mutexattr_destroy(&attr); + return ok; +#else + return pthread_mutex_init(m, NULL) == 0; +#endif +} + +/* A normal mutex fails only when it is broken, so the result is ignored. */ +static inline void term_mutex_lock(term_mutex *m) +{ +#if defined(_WIN32) + AcquireSRWLockExclusive(m); +#else + (void)pthread_mutex_lock(m); +#endif +} + +static inline void term_mutex_unlock(term_mutex *m) +{ +#if defined(_WIN32) + ReleaseSRWLockExclusive(m); +#else + (void)pthread_mutex_unlock(m); +#endif +} + +/* An SRWLOCK has no destroy. */ +static inline void term_mutex_destroy(term_mutex *m) +{ +#if defined(_WIN32) + (void)m; +#else + (void)pthread_mutex_destroy(m); +#endif +} + /* strdup is POSIX, so the core copies strings itself. */ static inline char *term_strdup(const char *s) { @@ -16,13 +16,13 @@ static uint16_t cluster_presentation(const uint32_t *cps, int len) { if (len <= 1) return 0; - bool pictographic = unicode_extended_pictographic(cps[0]); + bool pictographic = term_unicode_extended_pictographic(cps[0]); if (len == 2) { uint32_t c1 = cps[1]; if (c1 == 0xFE0F) return CELL_EMOJI; if (c1 == 0xFE0E) return CELL_TEXT; - if (c1 == 0x20E3 || grapheme_regional_indicator(c1) || - (pictographic && (c1 == 0x200D || grapheme_emoji_modifier(c1) || + if (c1 == 0x20E3 || term_grapheme_regional_indicator(c1) || + (pictographic && (c1 == 0x200D || term_grapheme_emoji_modifier(c1) || (c1 >= 0xE0020 && c1 <= 0xE007F)))) return CELL_EMOJI; return 0; @@ -33,8 +33,8 @@ static uint16_t cluster_presentation(const uint32_t *cps, int len) return CELL_EMOJI; if (cps[i] == 0xFE0E) return CELL_TEXT; - if (cps[i] == 0x20E3 || grapheme_regional_indicator(cps[i]) || - (pictographic && (cps[i] == 0x200D || grapheme_emoji_modifier(cps[i]) || + if (cps[i] == 0x20E3 || term_grapheme_regional_indicator(cps[i]) || + (pictographic && (cps[i] == 0x200D || term_grapheme_emoji_modifier(cps[i]) || (cps[i] >= 0xE0020 && cps[i] <= 0xE007F)))) emoji = true; } @@ -44,9 +44,9 @@ static uint16_t cluster_presentation(const uint32_t *cps, int len) void term_sc_print_glyph(struct term *t, uint32_t cp, int width, uint16_t flags) { uint32_t base = cp; - if (TERM_EXPECT(grapheme_is_handle(cp), 0)) { - uint32_t cps[GRAPHEME_MAX_LEN]; - int len = term_gstore_expand(t->gstore, cp, cps, GRAPHEME_MAX_LEN); + if (TERM_EXPECT(term_grapheme_is_handle(cp), 0)) { + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; + int len = term_gstore_expand(t->gstore, cp, cps, TERM_GRAPHEME_MAX_LEN); base = cps[0]; flags = (flags & ~(CELL_EMOJI | CELL_TEXT)) | cluster_presentation(cps, len); } @@ -179,13 +179,13 @@ bool term_sc_extend_cluster(struct term *t, uint32_t cp, int klass) return false; struct cell *prev = last_written_cell(t); - uint32_t cps[GRAPHEME_MAX_LEN]; - int len = prev && prev->c > ' ' ? term_gstore_expand(t->gstore, prev->c, cps, GRAPHEME_MAX_LEN) : 0; + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; + int len = prev && prev->c > ' ' ? term_gstore_expand(t->gstore, prev->c, cps, TERM_GRAPHEME_MAX_LEN) : 0; if (!len) return joins_cluster(cp, klass); if (!cluster_joins(cps, len, cp, klass)) return false; - if (len >= GRAPHEME_MAX_LEN) + if (len >= TERM_GRAPHEME_MAX_LEN) return true; cps[len++] = cp; @@ -517,7 +517,7 @@ static inline bool next_byte_wraps(const char *buf, size_t len, size_t pos, uint { if (pos >= len) return false; uint32_t cp; - if (!utf8_decode_one(buf + pos, len - pos, &cp)) return false; + if (!term_utf8_decode_one(buf + pos, len - pos, &cp)) return false; int w = term_text_class(cp); return w > 0 && !can_extend_cluster(last, cp, w); } diff --git a/src/escape.c b/src/escape.c index da0449a..dceb7a6 100644 --- a/src/escape.c +++ b/src/escape.c @@ -834,7 +834,9 @@ static inline TERM_ALWAYS_INLINE void dispatch_csi(struct term *t, const struct case 'q': { if (csi->prefix == '>' && !csi->intermediate && (!csi->nparams || !csi->params[0])) { - send_response(t, "\033P>|wsap_term\033\\"); + char resp[96]; + snprintf(resp, sizeof(resp), "\033P>|%s\033\\", t->name); + send_response(t, resp); } else if (!csi->prefix && csi->intermediate == ' ') { term_sc_set_cursor_style(t, csi->nparams > 0 ? csi->params[0] : 0); } diff --git a/src/gstore.c b/src/gstore.c index d2bc57f..813ea7d 100644 --- a/src/gstore.c +++ b/src/gstore.c @@ -72,11 +72,11 @@ int term_gstore_expand(const struct gstore *g, uint32_t handle, uint32_t *cps, i { if (cap <= 0) return 0; - if (!g || !grapheme_is_handle(handle)) { + if (!g || !term_grapheme_is_handle(handle)) { cps[0] = handle; return 1; } - uint32_t idx = handle - GRAPHEME_HANDLE_BASE; + uint32_t idx = handle - TERM_GRAPHEME_HANDLE_BASE; if (idx >= g->entries_count || !g->entries[idx].allocated) { cps[0] = 0xFFFD; return 1; @@ -124,8 +124,8 @@ static bool grow_slots(struct gstore *g) struct gentry *e = &g->entries[i]; if (!e->allocated) continue; - uint32_t cps_buf[GRAPHEME_MAX_LEN]; - int n = term_gstore_expand(g, GRAPHEME_HANDLE_BASE + i, cps_buf, GRAPHEME_MAX_LEN); + uint32_t cps_buf[TERM_GRAPHEME_MAX_LEN]; + int n = term_gstore_expand(g, TERM_GRAPHEME_HANDLE_BASE + i, cps_buf, TERM_GRAPHEME_MAX_LEN); uint64_t h = hash_cps(cps_buf, n); uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1; gstore_insert_slot(g, h32, i); @@ -147,7 +147,7 @@ static uint32_t gstore_find(const struct gstore *g, const uint32_t *cps, int n, uint32_t e_idx = slot->entry_idx - 1; const struct gentry *e = &g->entries[e_idx]; if (e->allocated && gentry_matches(g, e, cps, n)) - return GRAPHEME_HANDLE_BASE + e_idx; + return TERM_GRAPHEME_HANDLE_BASE + e_idx; } idx = (idx + 1) & mask; } @@ -193,9 +193,9 @@ void term_gstore_unpin(struct gstore *g) void term_gstore_mark(struct gstore *g, uint32_t handle) { - if (!g || !grapheme_is_handle(handle)) + if (!g || !term_grapheme_is_handle(handle)) return; - uint32_t idx = handle - GRAPHEME_HANDLE_BASE; + uint32_t idx = handle - TERM_GRAPHEME_HANDLE_BASE; if (idx < g->entries_count && g->entries[idx].allocated) g->entries[idx].mark = true; } @@ -204,7 +204,7 @@ static void sweep_mark_row(struct gstore *g, const struct cell *cells, int len) { for (int x = 0; x < len; x++) { uint32_t c = cells[x].c; - if (grapheme_is_handle(c)) + if (term_grapheme_is_handle(c)) term_gstore_mark(g, c); } } @@ -215,7 +215,7 @@ void term_gstore_sweep(struct gstore *g, struct term *t) return; if (t) { - if (grapheme_is_handle(t->last_char)) + if (term_grapheme_is_handle(t->last_char)) term_gstore_mark(g, t->last_char); struct screen *s = &t->scr; @@ -266,8 +266,8 @@ void term_gstore_sweep(struct gstore *g, struct term *t) live++; e->mark = false; if (g->slots) { - uint32_t cps_buf[GRAPHEME_MAX_LEN]; - int n = term_gstore_expand(g, GRAPHEME_HANDLE_BASE + i, cps_buf, GRAPHEME_MAX_LEN); + uint32_t cps_buf[TERM_GRAPHEME_MAX_LEN]; + int n = term_gstore_expand(g, TERM_GRAPHEME_HANDLE_BASE + i, cps_buf, TERM_GRAPHEME_MAX_LEN); uint64_t h = hash_cps(cps_buf, n); uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1; gstore_insert_slot(g, h32, i); @@ -300,8 +300,8 @@ uint32_t term_gstore_intern(struct gstore *g, const uint32_t *cps, int n) return 0; if (n == 1) return cps[0]; - if (n > GRAPHEME_MAX_LEN) - n = GRAPHEME_MAX_LEN; + if (n > TERM_GRAPHEME_MAX_LEN) + n = TERM_GRAPHEME_MAX_LEN; uint64_t h = hash_cps(cps, n); uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1; @@ -375,5 +375,5 @@ uint32_t term_gstore_intern(struct gstore *g, const uint32_t *cps, int n) gstore_insert_slot(g, h32, entry_idx); g->live_count++; - return GRAPHEME_HANDLE_BASE + entry_idx; + return TERM_GRAPHEME_HANDLE_BASE + entry_idx; } @@ -42,7 +42,7 @@ #define ROWS_UNKNOWN 255 /* Bytes one cell can need, except a cluster. */ #define CELL_BYTES 5 -#define CLUSTER_BYTES (2 + GRAPHEME_MAX_LEN * 4) +#define CLUSTER_BYTES (2 + TERM_GRAPHEME_MAX_LEN * 4) /* ---- Parts and pages ---- */ @@ -1039,8 +1039,8 @@ static int uniform_utf8_prefix(const struct cell *cells, int len, uint8_t *dst, static TERM_NOINLINE size_t encode_cluster(uint8_t *dst, const struct gstore *g, uint32_t handle, uint16_t f) { - uint32_t cps[GRAPHEME_MAX_LEN]; - int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; + int n = term_gstore_expand(g, handle, cps, TERM_GRAPHEME_MAX_LEN); size_t b = 0; dst[b++] = PREFIX_CLUSTER; dst[b++] = (uint8_t)((n & 0x1f) | ((f & CELL_WIDE) ? 0x80 : 0) | @@ -1053,7 +1053,7 @@ static TERM_NOINLINE size_t encode_cluster(uint8_t *dst, const struct gstore *g, static size_t encode_shaped(uint8_t *dst, uint32_t cp, uint16_t f, const struct gstore *g, uint8_t *flags) { - if (grapheme_is_handle(cp)) { + if (term_grapheme_is_handle(cp)) { *flags |= HLINE_HAS_CLUSTER | ((f & CELL_WIDE) ? HLINE_HAS_WIDE : 0); return encode_cluster(dst, g, cp, f); } @@ -1155,7 +1155,7 @@ static int encode_cells(struct hist_runs *r, const struct cell *cells, int n, ui continue; uint32_t cp = c->c; bool simple = !(f & (CELL_WIDE | CELL_EMOJI | CELL_TEXT)) && cp - 0x20u < 0x60u; - if (!simple && grapheme_is_handle(cp) && + if (!simple && term_grapheme_is_handle(cp) && bytes + CLUSTER_BYTES + (size_t)(n - i) * CELL_BYTES > cap) break; uint32_t sl = (uint32_t)(f & STYLE_FLAGS) | ((uint32_t)c->pad << 16); @@ -2516,12 +2516,12 @@ static const uint8_t *text_cell(const uint8_t *s, const uint8_t *end, struct gst } uint8_t k = s[1]; s += 2; - uint32_t cps[GRAPHEME_MAX_LEN]; + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; int n = 0; for (int i = 0; i < (k & 0x1f) && s < end; i++) { uint32_t c; s += get_utf8(s, end, &c); - if (n < GRAPHEME_MAX_LEN) + if (n < TERM_GRAPHEME_MAX_LEN) cps[n++] = c; } *cp = n == 0 ? ' ' : (g && n > 1) ? term_gstore_intern(g, cps, n) : cps[0]; diff --git a/src/input.c b/src/input.c index 9fee179..2f0cd5c 100644 --- a/src/input.c +++ b/src/input.c @@ -53,7 +53,7 @@ static inline void sb_u32(struct seqbuf *sb, uint32_t val) static inline void sb_utf8(struct seqbuf *sb, uint32_t cp) { char tmp[8]; - size_t n = (size_t)utf8_encode(cp, tmp); + size_t n = (size_t)term_utf8_encode(cp, tmp); sb_bytes(sb, tmp, n); } @@ -695,7 +695,7 @@ struct term_input_result term_input_encode_paste(const struct term_modes *m, continue; } uint32_t cp; - int n = utf8_decode_one(text + i, len - i, &cp); + int n = term_utf8_decode_one(text + i, len - i, &cp); if (!n) { i++; continue; } if (cp >= 0xa0 && !is_direction_format(cp)) { sb_bytes(&sb, text + i, (size_t)n); diff --git a/src/internal.h b/src/internal.h index 8c02c91..676413c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -108,7 +108,8 @@ struct resize_scratch { struct term { - _Atomic uint64_t seq; + /* The writer section. A waiting thread sleeps on it. */ + term_mutex write_lock; _Atomic uint64_t writer_tid; uint32_t write_depth; int cols; @@ -208,6 +209,8 @@ struct term { bool custom_tabs; char osc_buf[256]; + /* The host's name. A DCS sequence can change tcap_name, not this. */ + char name[64]; char tcap_name[64]; char *osc_heap; @@ -375,18 +378,7 @@ static inline void term_write_begin(struct term *t) t->write_depth++; return; } - uint64_t s; - for (;;) { - s = atomic_load_explicit(&t->seq, memory_order_relaxed); - if (s & 1) { - term_cpu_relax(); - continue; - } - if (atomic_compare_exchange_weak_explicit(&t->seq, &s, s + 1, - memory_order_acquire, - memory_order_relaxed)) - break; - } + term_mutex_lock(&t->write_lock); atomic_store_explicit(&t->writer_tid, self, memory_order_relaxed); t->write_depth = 1; } @@ -396,7 +388,7 @@ static inline void term_write_end(struct term *t) if (!t) return; if (--t->write_depth == 0) { atomic_store_explicit(&t->writer_tid, 0, memory_order_relaxed); - atomic_fetch_add_explicit(&t->seq, 1, memory_order_release); + term_mutex_unlock(&t->write_lock); } } diff --git a/src/meson.build b/src/meson.build index 694ee0e..7684275 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,9 @@ subdir('osc') +# The SONAME carries the major version. An ABI break bumps it, so an old +# binary keeps the old library. +term_soversion = meson.project_version().split('.')[0] + # The x86-64 SIMD kernels pick their level at run time (see simd.h), so the # library needs no ISA flags. -march in c_args sets the lowest level. term_lib = both_libraries('term', @@ -21,8 +25,10 @@ term_lib = both_libraries('term', 'text_run.c', term_osc_src, include_directories: term_private_inc, - dependencies: [m_dep], + dependencies: [m_dep, thread_dep], gnu_symbol_visibility: 'hidden', + version: meson.project_version(), + soversion: term_soversion, install: true ) diff --git a/src/osc/misc.c b/src/osc/misc.c index 5d6b506..74b35cf 100644 --- a/src/osc/misc.c +++ b/src/osc/misc.c @@ -53,7 +53,7 @@ void term_osc_misc_dispatch(struct term *t, long code, const char *params) break; case 9: if (t->cb.notify && strncmp(params, "4;", 2)) - t->cb.notify(t->cb.ud, "wsap_term", params); + t->cb.notify(t->cb.ud, t->name, params); break; case 22: free(t->pointer_shape); diff --git a/src/screen.c b/src/screen.c index 87d6f2c..b99948d 100644 --- a/src/screen.c +++ b/src/screen.c @@ -808,12 +808,11 @@ void term_screen_set_alternate(struct term *t, bool enabled) } } +/* The caller has checked that requested is not negative. */ static int scrollback_lines(int requested) { if (requested == 0) return TERM_SCROLLBACK_DEFAULT; - if (requested < 0) - return 0; return requested > 1000000 ? 1000000 : requested; } @@ -841,22 +840,45 @@ bool term_tables_ready(void) return atomic_load_explicit(&tables_state, memory_order_relaxed) == 2; } +/* A name is 1 to 63 printable ASCII bytes. DCS replies echo it. */ +static bool name_valid(const char *s) +{ + for (size_t i = 0; s[i]; i++) { + unsigned char c = (unsigned char)s[i]; + if (i >= 63 || c < 0x20 || c > 0x7e) + return false; + } + return true; +} + struct term *term_new(const struct term_options *opt) { tables_init(); static const struct term_options defaults; if (!opt) opt = &defaults; - int cols = opt->size.x < 1 ? 80 : opt->size.x; - int rows = opt->size.y < 1 ? 24 : opt->size.y; - if (cols > TERM_MAX_COLS) - cols = TERM_MAX_COLS; + for (size_t i = 0; i < sizeof(opt->reserved) / sizeof(opt->reserved[0]); i++) + if (opt->reserved[i]) + return NULL; + if (opt->size.x < 0 || opt->size.y < 0 || opt->size.x > TERM_MAX_COLS) + return NULL; + if (opt->scrollback < 0 || (unsigned)opt->cursor_style > TERM_CURSOR_BAR) + return NULL; + const char *name = opt->name && *opt->name ? opt->name : "term"; + if (!name_valid(name)) + return NULL; + int cols = opt->size.x == 0 ? 80 : opt->size.x; + int rows = opt->size.y == 0 ? 24 : opt->size.y; static _Atomic uint64_t next_serial = 1; struct term *t = calloc(1, sizeof(*t)); if (!t) return NULL; - atomic_init(&t->seq, 0); + if (!term_mutex_init(&t->write_lock)) { + free(t); + return NULL; + } + memcpy(t->name, name, strlen(name) + 1); atomic_init(&t->writer_tid, 0); t->write_depth = 0; t->serial = atomic_fetch_add_explicit(&next_serial, 1, memory_order_relaxed); @@ -871,11 +893,13 @@ struct term *term_new(const struct term_options *opt) t->next_row_uid = 1; if (!screen_setup(&t->scr, cols, rows, &t->next_row_uid)) { + term_mutex_destroy(&t->write_lock); free(t); return NULL; } if (!term_hist_init(&t->hist, t->sb_capacity, 0)) { screen_free(&t->scr); + term_mutex_destroy(&t->write_lock); free(t); return NULL; } @@ -895,14 +919,14 @@ struct term *term_new(const struct term_options *opt) t->cur_fg = TERM_COLOR_FG; t->cur_bg = TERM_COLOR_BG; t->cursor_visible = true; - t->default_cursor_style = opt->cursor_style > TERM_CURSOR_BAR ? TERM_CURSOR_BLOCK : opt->cursor_style; + t->default_cursor_style = opt->cursor_style; t->default_cursor_blink = !opt->cursor_steady; t->cursor_style = t->default_cursor_style; t->cursor_blink = t->default_cursor_blink; t->autowrap = true; t->charsets[0] = t->charsets[1] = 'B'; t->saved = term_saved_cursor_initial(); - memcpy(t->tcap_name, "wsap_term", sizeof("wsap_term")); + memcpy(t->tcap_name, t->name, sizeof(t->name)); term_tabs_default(t); t->full_gen = 1; memset(t->text_width.lo, 0xFF, sizeof(t->text_width.lo)); @@ -932,6 +956,7 @@ void term_free(struct term *t) free(t->tabs); term_osc_cleanup(t); term_gstore_free(t->gstore); + term_mutex_destroy(&t->write_lock); free(t); } @@ -1523,10 +1548,8 @@ bool term_resize(struct term *t, struct term_vector2i size) return false; int cols = size.x; int rows = size.y; - if (cols < 1 || rows < 1) + if (cols < 1 || rows < 1 || cols > TERM_MAX_COLS) return false; - if (cols > TERM_MAX_COLS) - cols = TERM_MAX_COLS; term_write_begin(t); if (cols == t->cols && rows == t->rows) { term_write_end(t); diff --git a/src/selection.c b/src/selection.c index 3ac2ee3..40fb1f1 100644 --- a/src/selection.c +++ b/src/selection.c @@ -742,17 +742,17 @@ static char *range_text_inner(const struct term *t, uint32_t cp = cells[c].c; if (cells[c].flags & (CELL_TRAIL | CELL_WRAP_PAD)) continue; - /* One cell expands to GRAPHEME_MAX_LEN code points of up + /* One cell expands to TERM_GRAPHEME_MAX_LEN code points of up * to 4 bytes each. Leave room for all of them. */ - if (!text_reserve(&text, &cap, n + (size_t)GRAPHEME_MAX_LEN * 4 + 1)) + if (!text_reserve(&text, &cap, n + (size_t)TERM_GRAPHEME_MAX_LEN * 4 + 1)) break; if (cp > 0 && cp < 128) { text[n++] = (char)cp; } else { - uint32_t cps[GRAPHEME_MAX_LEN]; - int len = term_gstore_expand(t->gstore, cp ? cp : ' ', cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; + int len = term_gstore_expand(t->gstore, cp ? cp : ' ', cps, TERM_GRAPHEME_MAX_LEN); for (int k = 0; k < len; k++) - n += (size_t)utf8_encode(cps[k], text + n); + n += (size_t)term_utf8_encode(cps[k], text + n); } } if (text && !is_last && !wrapped && text_reserve(&text, &cap, n + 2)) diff --git a/src/snapshot.c b/src/snapshot.c index 82a2805..8726265 100644 --- a/src/snapshot.c +++ b/src/snapshot.c @@ -78,8 +78,8 @@ static bool reserve(struct term_snapshot *s, int cols, int rows) /* Append a cluster record. The caller reserved room for it. */ static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle) { - uint32_t cps[GRAPHEME_MAX_LEN]; - int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; + int n = term_gstore_expand(g, handle, cps, TERM_GRAPHEME_MAX_LEN); if (n <= 0) return ' '; uint32_t off = s->arena_len; @@ -92,13 +92,13 @@ static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint3 /* Arena words that converting src adds. Counts what arena_add() writes. */ static uint64_t row_words(const struct gstore *g, const struct cell *src, int cols) { - uint32_t cps[GRAPHEME_MAX_LEN]; + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; uint64_t words = 0; for (int x = 0; x < cols; x++) { uint32_t text = src[x].c; - if (!grapheme_is_handle(text)) + if (!term_grapheme_is_handle(text)) continue; - int n = term_gstore_expand(g, text, cps, GRAPHEME_MAX_LEN); + int n = term_gstore_expand(g, text, cps, TERM_GRAPHEME_MAX_LEN); if (n > 0) words += (uint64_t)n + 1; } @@ -117,7 +117,7 @@ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g, uint32_t text = c->c; if (TERM_EXPECT(text < 0x20, 0)) text = text ? ' ' : 0; - else if (TERM_EXPECT(grapheme_is_handle(text), 0)) + else if (TERM_EXPECT(term_grapheme_is_handle(text), 0)) text = arena_add(s, g, text); dst[x] = (struct term_cell){ .text = text, @@ -341,34 +341,36 @@ bool term_snapshot_update(struct term_snapshot *s, struct term *t) const struct term_view *term_snapshot_view(const struct term_snapshot *s) { - return &s->view; + return s ? &s->view : NULL; } const struct term_cell *term_snapshot_row(const struct term_snapshot *s, int y) { - if (y < 0 || y >= s->view.size.y) + if (!s || y < 0 || y >= s->view.size.y) return NULL; return s->cells + (size_t)y * s->view.size.x; } uint64_t term_snapshot_row_key(const struct term_snapshot *s, int y) { - if (y < 0 || y >= s->view.size.y) + if (!s || y < 0 || y >= s->view.size.y) return 0; return s->keys[y]; } struct term_span term_snapshot_row_selection(const struct term_snapshot *s, int y) { - if (y < 0 || y >= s->view.size.y) + if (!s || y < 0 || y >= s->view.size.y) return (struct term_span){ 0 }; return s->selection[y]; } int term_snapshot_cluster(const struct term_snapshot *s, uint32_t text, uint32_t *cps, int cap) { + if (!s) + return 0; if (!(text & TERM_TEXT_CLUSTER)) { - if (cap > 0) + if (cps && cap > 0) cps[0] = text; return 1; } @@ -377,7 +379,7 @@ int term_snapshot_cluster(const struct term_snapshot *s, uint32_t text, uint32_t return 0; int n = (int)s->arena[off]; int copy = n < cap ? n : cap; - if (copy > 0) + if (cps && copy > 0) memcpy(cps, &s->arena[off + 1], (size_t)copy * sizeof(*cps)); return n; } @@ -5,7 +5,12 @@ _Thread_local char term_thread_anchor; const char *term_unicode_version(void) { - return UNICODE_DATA_VERSION; + return TERM_UNICODE_DATA_VERSION; +} + +unsigned term_version(void) +{ + return TERM_VERSION_CODE; } void term_lock(struct term *t) @@ -50,15 +50,15 @@ static inline void emit_decoded(struct term *t, uint32_t cp, uint16_t flags) * sequence prints U+FFFD (Unicode 3.9, maximal subparts). */ void term_text_utf8_interrupt(struct term *t) { - if (TERM_EXPECT(t->utf8_state == UTF8_ACCEPT, 1)) + if (TERM_EXPECT(t->utf8_state == TERM_UTF8_ACCEPT, 1)) return; - t->utf8_state = UTF8_ACCEPT; + t->utf8_state = TERM_UTF8_ACCEPT; emit_decoded(t, 0xFFFD, term_current_flags(t)); } void term_text_ground(struct term *t, unsigned char ch) { - if (ch >= 0x20 && ch < 0x7f && t->utf8_state == UTF8_ACCEPT) { + if (ch >= 0x20 && ch < 0x7f && t->utf8_state == TERM_UTF8_ACCEPT) { emit_codepoint(t, ch, 1, term_current_flags(t)); return; } @@ -92,14 +92,14 @@ void term_text_ground(struct term *t, unsigned char ch) t->graphics = t->charsets[0] == '0'; } else if (ch >= 0x20 && ch != 0x7f) { uint32_t before = t->utf8_state; - uint32_t st = utf8_decode(&t->utf8_state, &t->utf8_codep, ch); - if (st == UTF8_ACCEPT) { + uint32_t st = term_utf8_decode(&t->utf8_state, &t->utf8_codep, ch); + if (st == TERM_UTF8_ACCEPT) { emit_decoded(t, t->utf8_codep, term_current_flags(t)); - } else if (st == UTF8_REJECT) { - t->utf8_state = UTF8_ACCEPT; + } else if (st == TERM_UTF8_REJECT) { + t->utf8_state = TERM_UTF8_ACCEPT; emit_decoded(t, 0xFFFD, term_current_flags(t)); /* The byte that ended an unfinished sequence starts anew. */ - if (before != UTF8_ACCEPT) + if (before != TERM_UTF8_ACCEPT) term_text_ground(t, ch); } } @@ -109,7 +109,7 @@ static inline bool can_stream_bottom(const struct term *t) { return t->state == ST_GROUND && !t->graphics && !t->insert_mode && t->autowrap && !t->lr_margin_mode && - t->utf8_state == UTF8_ACCEPT && !t->inverse && + t->utf8_state == TERM_UTF8_ACCEPT && !t->inverse && t->scroll_top == 0 && t->scroll_bottom == t->rows - 1 && t->cursor_y == t->rows - 1 && !t->alternate && t->sb_capacity > 0 && @@ -123,14 +123,14 @@ static inline bool try_text_run(struct term *t, const char *buf, size_t len, if (t->pending_wrap || i + 4 > len || ((unsigned char)buf[i] >= 0x80 && ((unsigned char)buf[i] < 0xc2 || (unsigned char)buf[i] > 0xf4)) || - t->last_char == 0x200D || grapheme_is_handle(t->last_char)) + t->last_char == 0x200D || term_grapheme_is_handle(t->last_char)) return false; if (t->cursor_x != 0 && t->cursor_x != term_row(t, t->cursor_y)->len) return false; if ((unsigned char)buf[i] >= 0x80) { uint32_t cp; - int bytes = utf8_decode_one(buf + i, len - i, &cp); + int bytes = term_utf8_decode_one(buf + i, len - i, &cp); if (!bytes || term_text_class(cp) <= 0) return false; } @@ -181,7 +181,7 @@ static inline bool try_text_run(struct term *t, const char *buf, size_t len, newline = true; } if (!newline || i + 4 > len || t->pending_wrap || - t->last_char == 0x200D || grapheme_is_handle(t->last_char) || + t->last_char == 0x200D || term_grapheme_is_handle(t->last_char) || (t->cursor_x != 0 && t->cursor_x != term_row(t, t->cursor_y)->len)) break; @@ -190,10 +190,11 @@ static inline bool try_text_run(struct term *t, const char *buf, size_t len, return wrote; } +/* The feed flags this build knows. term_feed() rejects any other bit. */ +#define TERM_FEED_KNOWN TERM_FEED_ONLCR + static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigned flags) { - if (!t || !buf || len == 0) - return; t->transport_newline = (flags & TERM_FEED_ONLCR) != 0; uint16_t cur_flags = term_current_flags(t); @@ -212,7 +213,7 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne unsigned char ch = (unsigned char)buf[i]; if (TERM_EXPECT(ch >= 0x20 && ch < 0x7f && !t->insert_mode && !t->lr_margin_mode && t->state == ST_GROUND && !t->graphics && - t->utf8_state == UTF8_ACCEPT, 1)) { + t->utf8_state == TERM_UTF8_ACCEPT, 1)) { int avail = t->pending_wrap ? t->cols : t->cols - t->cursor_x; int rem = (int)(len - i); if (rem > avail) rem = avail; @@ -239,7 +240,7 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne (unsigned char)buf[i + 1] <= 0xf4 && (i + 2 == len || ((unsigned char)buf[i + 2] & 0xc0) == 0x80)) { uint32_t next_cp; - if (utf8_decode_one(buf + i + 1, len - i - 1, &next_cp) && + if (term_utf8_decode_one(buf + i + 1, len - i - 1, &next_cp) && term_text_class(next_cp) > 0 && try_text_run(t, buf, len, &i)) continue; @@ -249,7 +250,7 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne } if (i + (size_t)run < len && (unsigned char)buf[i + run] >= 0x80) { uint32_t next_cp; - if (utf8_decode_one(buf + i + run, len - i - (size_t)run, &next_cp) && + if (term_utf8_decode_one(buf + i + run, len - i - (size_t)run, &next_cp) && term_text_class(next_cp) > 0 && try_text_run(t, buf, len, &i)) continue; @@ -274,21 +275,21 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne if (TERM_EXPECT(ch >= 0x80, 1)) { if (TERM_EXPECT(t->state == ST_GROUND && - t->utf8_state == UTF8_ACCEPT && !t->graphics, 1)) { + t->utf8_state == TERM_UTF8_ACCEPT && !t->graphics, 1)) { if (ch >= 0xc2 && ch <= 0xf4 && (i + 1 == len || ((unsigned char)buf[i + 1] & 0xc0) == 0x80)) { if (!t->insert_mode && !t->lr_margin_mode && try_text_run(t, buf, len, &i)) continue; } else { - if (TERM_EXPECT(t->last_char != 0x200D && !grapheme_is_handle(t->last_char), 1)) + if (TERM_EXPECT(t->last_char != 0x200D && !term_grapheme_is_handle(t->last_char), 1)) sc_print_cell(t, 0xFFFD, 1, cur_flags); else emit_decoded(t, 0xFFFD, cur_flags); continue; } uint32_t cp; - int cp_len = utf8_decode_ground(&t->utf8_state, &t->utf8_codep, buf + i, len - i, &cp); + int cp_len = term_utf8_decode_ground(&t->utf8_state, &t->utf8_codep, buf + i, len - i, &cp); if (cp) emit_decoded(t, cp, cur_flags); i += (size_t)(cp_len - 1); @@ -328,7 +329,7 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne } continue; case 0x1b: - if (t->utf8_state == UTF8_ACCEPT && i + 1 < len && + if (t->utf8_state == TERM_UTF8_ACCEPT && i + 1 < len && buf[i + 1] == '[' && !t->insert_mode && !t->lr_margin_mode && !t->graphics) { int fast = term_try_fast_csi(t, buf + i + 2, len - i - 2); @@ -366,13 +367,16 @@ static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigne } } -void term_feed(struct term *t, const char *buf, size_t len, unsigned flags) +bool term_feed(struct term *t, const char *buf, size_t len, unsigned flags) { - if (!t || !buf || len == 0) - return; + if (!t || (!buf && len > 0) || (flags & ~TERM_FEED_KNOWN)) + return false; + if (len == 0) + return true; term_write_begin(t); term_feed_inner(t, buf, len, flags); term_write_end(t); + return true; } void term_text_repeat(struct term *t, int count) diff --git a/src/text_run.c b/src/text_run.c index 028b160..c48b73e 100644 --- a/src/text_run.c +++ b/src/text_run.c @@ -19,7 +19,7 @@ static int width_run_count; void term_text_tables_init(void) { for (uint32_t cp = 0; cp < 65536; cp++) { - int w = unicode_width(cp); + int w = term_unicode_width(cp); unicode_fast_class[cp] = (w == 1 || w == 2) && !cluster_contextual(cp) ? (uint8_t)w : 0; unicode_width_bmp[cp] = (int8_t)w; @@ -92,7 +92,7 @@ static TERM_MAYBE_UNUSED bool width_cache_extend(struct width_cache *wc, const u int term_text_class(uint32_t cp) { assert(term_tables_ready()); - return TERM_EXPECT(cp < 65536, 1) ? (int)unicode_width_bmp[cp] : unicode_width(cp); + return TERM_EXPECT(cp < 65536, 1) ? (int)unicode_width_bmp[cp] : term_unicode_width(cp); } #if defined(TERM_KERNEL_AVX512) @@ -189,10 +189,10 @@ static inline bool classify_3byte_block(__m512i in, struct width_ranges *ranges, _mm512_loadu_si512(utf8_3byte_bits)) & UTF8_3BYTE_SPAN, 0)) return false; *cps = utf8_3byte_decode(_mm512_permutexvar_epi8(_mm512_loadu_si512(utf8_3byte_shuffle), in)); - if (_mm512_cmplt_epu32_mask(*cps, _mm512_set1_epi32((int)UTF8_3BYTE_MIN)) || + if (_mm512_cmplt_epu32_mask(*cps, _mm512_set1_epi32((int)TERM_UTF8_3BYTE_MIN)) || _mm512_cmple_epu32_mask( - _mm512_sub_epi32(*cps, _mm512_set1_epi32((int)UTF8_SURROGATE_LO)), - _mm512_set1_epi32((int)(UTF8_SURROGATE_HI - UTF8_SURROGATE_LO)))) + _mm512_sub_epi32(*cps, _mm512_set1_epi32((int)TERM_UTF8_SURROGATE_LO)), + _mm512_set1_epi32((int)(TERM_UTF8_SURROGATE_HI - TERM_UTF8_SURROGATE_LO)))) return false; __mmask16 covered = width_ranges_cover(ranges, *cps); if (TERM_EXPECT(covered != 0xFFFF || wc->klass == 0, 0)) { @@ -289,7 +289,7 @@ text_3byte_blocks(uint8_t * restrict dst, const char * restrict buf, int len, in struct text_run_bytes term_text_run_bytes(uint8_t *restrict dst, const char *restrict buf, int len, int max_cols, uint32_t last, struct width_cache *restrict wc) { - if (TERM_EXPECT(last == 0x200D || grapheme_is_handle(last), 0)) + if (TERM_EXPECT(last == 0x200D || term_grapheme_is_handle(last), 0)) return (struct text_run_bytes){ .last = last, .narrow = true }; /* Unlike term_text_run(), this fills up to the edge and up to the end of * buf: the cells path writes those characters one by one with the same @@ -335,7 +335,7 @@ struct text_run_bytes term_text_run_bytes(uint8_t *restrict dst, const char *res } #endif uint32_t cp; - int bytes = utf8_decode_one(buf + i, (size_t)(len - i), &cp); + int bytes = term_utf8_decode_one(buf + i, (size_t)(len - i), &cp); if (!bytes) break; int w = bytes < 4 ? unicode_fast_class[cp] : term_text_class(cp); @@ -493,9 +493,9 @@ static inline int decode_3byte_quads(uint64_t **dst_io, const char *buf, int len _mm_and_si128(_mm_srli_epi32(packed, 2), _mm_set1_epi32(0x0FC0))), _mm_and_si128(_mm_srli_epi32(packed, 4), _mm_set1_epi32(0xF000))); __m128i bad = _mm_or_si128( - _mm_cmplt_epi32(cps, _mm_set1_epi32((int)UTF8_3BYTE_MIN)), + _mm_cmplt_epi32(cps, _mm_set1_epi32((int)TERM_UTF8_3BYTE_MIN)), _mm_cmpeq_epi32(_mm_and_si128(cps, _mm_set1_epi32(0xF800)), - _mm_set1_epi32((int)UTF8_SURROGATE_LO))); + _mm_set1_epi32((int)TERM_UTF8_SURROGATE_LO))); if (_mm_movemask_epi8(bad)) break; alignas(16) uint32_t cp[4]; @@ -530,7 +530,7 @@ text_run_body(struct cell *restrict row, const char *restrict buf, int len, int { int i = 0; int cx = 0; - if (TERM_EXPECT(last == 0x200D || grapheme_is_handle(last), 0)) + if (TERM_EXPECT(last == 0x200D || term_grapheme_is_handle(last), 0)) return (struct text_run_result){0, 0, last}; const struct cell_bases bases = cell_bases_make(fg, bg, flags); @@ -593,14 +593,14 @@ text_run_body(struct cell *restrict row, const char *restrict buf, int len, int } #endif uint32_t cp; - int bytes = utf8_decode_one(buf + i, (size_t)(len - i), &cp); + int bytes = term_utf8_decode_one(buf + i, (size_t)(len - i), &cp); if (!bytes) break; int w = bytes < 4 ? unicode_fast_class[cp] : term_text_class(cp); if (w <= 0 || (bytes == 4 && can_extend_cluster(last, cp, w))) break; #if defined(TERM_KERNEL_AVX512) if (isa == TERM_ISA_AVX512 && bytes == 3 && w == 2 && i + 6 <= len && cx + 4 <= max_cols) { uint32_t next_cp; - if (utf8_decode_one(buf + i + 3, (size_t)(len - i - 3), &next_cp) == 3 && + if (term_utf8_decode_one(buf + i + 3, (size_t)(len - i - 3), &next_cp) == 3 && unicode_fast_class[next_cp] == 2) { store_wide_pair(dst, fg, bg, flags, cp, next_cp); dst += 8; diff --git a/src/text_run.h b/src/text_run.h index fac638f..3ff432b 100644 --- a/src/text_run.h +++ b/src/text_run.h @@ -53,25 +53,25 @@ static inline bool joins_cluster(uint32_t cp, int klass) { if (cp < 0x0300) return false; - if (klass == 0 || grapheme_emoji_modifier(cp)) + if (klass == 0 || term_grapheme_emoji_modifier(cp)) return true; - return klass == 1 && unicode_spacing_mark(cp); + return klass == 1 && term_unicode_spacing_mark(cp); } static inline bool can_extend_cluster(uint32_t last_char, uint32_t cp, int klass) { if (klass == 0) return true; - if (last_char == 0x200D || grapheme_is_handle(last_char)) return true; - if (cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp)) + if (last_char == 0x200D || term_grapheme_is_handle(last_char)) return true; + if (cp - 0x1F1E6u <= 0x19u && term_grapheme_regional_indicator(cp)) return true; return joins_cluster(cp, klass); } static inline bool cluster_contextual(uint32_t cp) { - return unicode_spacing_mark(cp) || grapheme_emoji_modifier(cp) || - (cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp)); + return term_unicode_spacing_mark(cp) || term_grapheme_emoji_modifier(cp) || + (cp - 0x1F1E6u <= 0x19u && term_grapheme_regional_indicator(cp)); } static inline bool cluster_joins(const uint32_t *cps, int len, @@ -80,10 +80,10 @@ static inline bool cluster_joins(const uint32_t *cps, int len, if (joins_cluster(cp, klass)) return true; if (cps[len - 1] == 0x200D) - return unicode_extended_pictographic(cp); + return term_unicode_extended_pictographic(cp); return len == 1 && cp - 0x1F1E6u <= 0x19u && - grapheme_regional_indicator(cp) && - grapheme_regional_indicator(cps[0]); + term_grapheme_regional_indicator(cp) && + term_grapheme_regional_indicator(cps[0]); } #endif diff --git a/tests/api_args_test.c b/tests/api_args_test.c new file mode 100644 index 0000000..18af728 --- /dev/null +++ b/tests/api_args_test.c @@ -0,0 +1,140 @@ +#include "term_api.h" +#include <assert.h> +#include <stdbool.h> +#include <string.h> + +/* A feed that returns false must not move the cursor or change a cell. */ +static void expect_unchanged(struct term *t, int x, int y, uint32_t cp) +{ + assert(tt_cursor_x(t) == x); + assert(tt_cursor_y(t) == y); + assert(tt_cell(t, 0, 0)->text == cp); +} + +static void test_feed_unknown_flag(void) +{ + struct term *t = tt_new(10, 3); + assert(t); + assert(term_feed(t, "abc", 3, 0)); + expect_unchanged(t, 3, 0, 'a'); + + assert(!term_feed(t, "xy", 2, 1u << 1)); + expect_unchanged(t, 3, 0, 'a'); + assert(!term_feed(t, "xy", 2, TERM_FEED_ONLCR | (1u << 4))); + expect_unchanged(t, 3, 0, 'a'); + assert(!term_feed(t, "xy", 2, 1u << 31)); + expect_unchanged(t, 3, 0, 'a'); + /* Unknown bits are rejected even when there are no bytes. */ + assert(!term_feed(t, "", 0, 1u << 1)); + + assert(term_feed(t, "\nz", 2, TERM_FEED_ONLCR)); + assert(tt_cursor_x(t) == 1); + assert(tt_cursor_y(t) == 1); + term_free(t); +} + +static void test_feed_null_buffer(void) +{ + struct term *t = tt_new(10, 3); + assert(t); + assert(term_feed(t, "abc", 3, 0)); + + assert(!term_feed(t, NULL, 1, 0)); + expect_unchanged(t, 3, 0, 'a'); + assert(!term_feed(t, NULL, 4096, TERM_FEED_ONLCR)); + expect_unchanged(t, 3, 0, 'a'); + + /* A zero length is a no-op, so a NULL buffer is fine. */ + assert(term_feed(t, NULL, 0, 0)); + assert(term_feed(t, "", 0, TERM_FEED_ONLCR)); + expect_unchanged(t, 3, 0, 'a'); + + assert(!term_feed(NULL, "a", 1, 0)); + assert(!term_feed(NULL, NULL, 0, 0)); + term_free(t); +} + +static void test_new_rejects_bad_options(void) +{ + struct term_options opt; + + memset(&opt, 0, sizeof(opt)); + opt.size.x = -1; + assert(!term_new(&opt)); + + memset(&opt, 0, sizeof(opt)); + opt.size.y = -1; + assert(!term_new(&opt)); + + memset(&opt, 0, sizeof(opt)); + opt.size.x = TERM_MAX_COLS + 1; + assert(!term_new(&opt)); + + memset(&opt, 0, sizeof(opt)); + opt.scrollback = -1; + assert(!term_new(&opt)); + + memset(&opt, 0, sizeof(opt)); + opt.cursor_style = (enum term_cursor_style)3; + assert(!term_new(&opt)); + + memset(&opt, 0, sizeof(opt)); + opt.cursor_style = (enum term_cursor_style)-1; + assert(!term_new(&opt)); +} + +static void test_new_accepts_defaults(void) +{ + struct term_options opt; + + memset(&opt, 0, sizeof(opt)); + struct term *t = term_new(&opt); + assert(t); + assert(tt_cols(t) == 80); + assert(tt_rows(t) == 24); + assert(term_cursor(t).style == TERM_CURSOR_BLOCK); + term_free(t); + + memset(&opt, 0, sizeof(opt)); + opt.size.x = TERM_MAX_COLS; + opt.size.y = 1; + opt.cursor_style = TERM_CURSOR_BAR; + t = term_new(&opt); + assert(t); + assert(tt_cols(t) == TERM_MAX_COLS); + assert(tt_rows(t) == 1); + assert(term_cursor(t).style == TERM_CURSOR_BAR); + term_free(t); +} + +static void test_resize_rejects_too_wide(void) +{ + struct term *t = tt_new(10, 3); + assert(t); + assert(term_feed(t, "abc", 3, 0)); + + assert(!tt_resize(t, TERM_MAX_COLS + 1, 3)); + assert(tt_cols(t) == 10); + assert(tt_rows(t) == 3); + expect_unchanged(t, 3, 0, 'a'); + + assert(!tt_resize(t, 10, 0)); + assert(!tt_resize(t, 0, 3)); + assert(tt_cols(t) == 10); + assert(tt_rows(t) == 3); + + assert(tt_resize(t, TERM_MAX_COLS, 3)); + assert(tt_cols(t) == TERM_MAX_COLS); + assert(tt_rows(t) == 3); + term_free(t); +} + +int main(void) +{ + test_feed_unknown_flag(); + test_feed_null_buffer(); + test_new_rejects_bad_options(); + test_new_accepts_defaults(); + test_resize_rejects_too_wide(); + return 0; +} diff --git a/tests/dcs_test.c b/tests/dcs_test.c index 0e67f78..d9455be 100644 --- a/tests/dcs_test.c +++ b/tests/dcs_test.c @@ -176,8 +176,8 @@ static void test_xtgettcap(void) { struct term *t = new_term(80, 24); assert_tcap(t, "kind", "\033[1;2B"); - assert_tcap(t, "TN", "wsap_term"); - assert_tcap(t, "name", "wsap_term"); + assert_tcap(t, "TN", "term"); + assert_tcap(t, "name", "term"); assert_tcap(t, "Co", "16777216"); assert_tcap(t, "colors", "16777216"); assert_tcap(t, "RGB", "8/8/8"); @@ -230,7 +230,7 @@ static void test_xtgettcap_multi(void) snprintf(req, sizeof(req), "\033P+q%s;%s\033\\", hk, htn); snprintf(want, sizeof(want), "\033P1+r%s=%s;%s=%s\033\\", - hk, hv, htn, "777361705f7465726d"); + hk, hv, htn, "7465726d"); response_len = 0; write_seq(t, req); expect_response(want); @@ -383,7 +383,51 @@ static void test_xtversion(void) struct term *t = new_term(80, 24); response_len = 0; write_seq(t, "\033[>0q"); - expect_response("\033P>|wsap_term\033\\"); + expect_response("\033P>|term\033\\"); + term_free(t); +} + +/* term_options.name feeds XTVERSION and the TN and name capabilities. */ +static void test_host_name(void) +{ + struct term_options opt = { + .size = { .x = 80, .y = 24 }, + .scrollback = TEST_SCROLLBACK, + .name = "myterm", + .callbacks = { .reply = respond }, + }; + struct term *t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|myterm\033\\"); + assert_tcap(t, "TN", "myterm"); + assert_tcap(t, "name", "myterm"); + + /* A DCS +p sets the capability value. The XTVERSION name does not change. */ + response_len = 0; + write_seq(t, "\033P+p6f74686572\033\\"); + expect_no_response(); + assert_tcap(t, "TN", "other"); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|myterm\033\\"); + term_free(t); + + /* NULL and "" both select the default. */ + opt.name = ""; + t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|term\033\\"); + term_free(t); + opt.name = NULL; + t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|term\033\\"); term_free(t); } @@ -467,6 +511,7 @@ int main(void) test_synchronized_output(); test_ignored_dcs(); test_xtversion(); + test_host_name(); test_bel_terminator(); test_dcs_long_query(); test_dcs_abort_c0(); diff --git a/tests/gstore_test.c b/tests/gstore_test.c index 637b136..ea582f3 100644 --- a/tests/gstore_test.c +++ b/tests/gstore_test.c @@ -32,7 +32,7 @@ static void test_direct_unit(void) /* Cluster returns a handle. */ uint32_t c1[2] = { 'e', 0x0301 }; uint32_t h1 = term_gstore_intern(g, c1, 2); - assert(grapheme_is_handle(h1)); + assert(term_grapheme_is_handle(h1)); assert(term_gstore_count(g) == 1); /* Identical cluster deduplicates to the same handle. */ @@ -43,17 +43,17 @@ static void test_direct_unit(void) /* Different cluster gets a distinct handle. */ uint32_t c2[2] = { 'a', 0x0300 }; uint32_t h2 = term_gstore_intern(g, c2, 2); - assert(grapheme_is_handle(h2)); + assert(term_grapheme_is_handle(h2)); assert(h2 != h1); assert(term_gstore_count(g) == 2); /* Expand both handles. */ - uint32_t out[GRAPHEME_MAX_LEN] = {0}; - int n = term_gstore_expand(g, h1, out, GRAPHEME_MAX_LEN); + uint32_t out[TERM_GRAPHEME_MAX_LEN] = {0}; + int n = term_gstore_expand(g, h1, out, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(out[0] == 'e' && out[1] == 0x0301); - n = term_gstore_expand(g, h2, out, GRAPHEME_MAX_LEN); + n = term_gstore_expand(g, h2, out, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(out[0] == 'a' && out[1] == 0x0300); @@ -63,20 +63,20 @@ static void test_direct_unit(void) assert(term_gstore_count(g) == 1); /* h1 is still valid. */ - n = term_gstore_expand(g, h1, out, GRAPHEME_MAX_LEN); + n = term_gstore_expand(g, h1, out, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(out[0] == 'e' && out[1] == 0x0301); /* h2 was reclaimed. */ - n = term_gstore_expand(g, h2, out, GRAPHEME_MAX_LEN); + n = term_gstore_expand(g, h2, out, TERM_GRAPHEME_MAX_LEN); assert(n == 1 && out[0] == 0xFFFD); /* New cluster can reuse the reclaimed slot. */ uint32_t c3[3] = { 'o', 0x0308, 0x0301 }; uint32_t h3 = term_gstore_intern(g, c3, 3); - assert(grapheme_is_handle(h3)); + assert(term_grapheme_is_handle(h3)); assert(term_gstore_count(g) == 2); - n = term_gstore_expand(g, h3, out, GRAPHEME_MAX_LEN); + n = term_gstore_expand(g, h3, out, TERM_GRAPHEME_MAX_LEN); assert(n == 3); assert(out[0] == 'o' && out[1] == 0x0308 && out[2] == 0x0301); @@ -105,8 +105,8 @@ static void test_two_terminals_isolation(void) const struct term_cell *row = term_snapshot_row(snap, 0); assert(row[0].text & TERM_TEXT_CLUSTER); - uint32_t cps[GRAPHEME_MAX_LEN] = {0}; - int n = term_snapshot_cluster(snap, row[0].text, cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN] = {0}; + int n = term_snapshot_cluster(snap, row[0].text, cps, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(cps[0] == 'e' && cps[1] == 0x0301); @@ -143,16 +143,16 @@ static void test_screen_clusters_survive_sweep(void) const struct term_cell *row = term_snapshot_row(snap, 0); assert(row[0].text & TERM_TEXT_CLUSTER); - uint32_t cps[GRAPHEME_MAX_LEN] = {0}; - int n = term_snapshot_cluster(snap, row[0].text, cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN] = {0}; + int n = term_snapshot_cluster(snap, row[0].text, cps, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(cps[0] == 'a' && cps[1] == 0x0300); - n = term_snapshot_cluster(snap, row[1].text, cps, GRAPHEME_MAX_LEN); + n = term_snapshot_cluster(snap, row[1].text, cps, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(cps[0] == 'b' && cps[1] == 0x0301); - n = term_snapshot_cluster(snap, row[2].text, cps, GRAPHEME_MAX_LEN); + n = term_snapshot_cluster(snap, row[2].text, cps, TERM_GRAPHEME_MAX_LEN); assert(n == 2); assert(cps[0] == 'c' && cps[1] == 0x0302); @@ -183,8 +183,8 @@ static void test_more_than_65536_clusters(void) size_t len = 0; buf[len++] = base; - len += (size_t)utf8_encode(cp1, buf + len); - len += (size_t)utf8_encode(cp2, buf + len); + len += (size_t)term_utf8_encode(cp1, buf + len); + len += (size_t)term_utf8_encode(cp2, buf + len); buf[len++] = '\r'; buf[len++] = '\n'; @@ -204,8 +204,8 @@ static void test_more_than_65536_clusters(void) const struct term_cell *row = term_snapshot_row(snap, y); assert(row[0].text & TERM_TEXT_CLUSTER); - uint32_t cps[GRAPHEME_MAX_LEN] = {0}; - int n = term_snapshot_cluster(snap, row[0].text, cps, GRAPHEME_MAX_LEN); + uint32_t cps[TERM_GRAPHEME_MAX_LEN] = {0}; + int n = term_snapshot_cluster(snap, row[0].text, cps, TERM_GRAPHEME_MAX_LEN); assert(n == 3); /* Live cluster count must be small (only bounded screen and history). */ diff --git a/tests/header_test.sh b/tests/header_test.sh index 4ba091b..9fd1041 100644 --- a/tests/header_test.sh +++ b/tests/header_test.sh @@ -1,25 +1,70 @@ #!/usr/bin/env bash -# A host includes term.h and nothing else. The header compiles alone as -# strict C11 and as C++, with only include/term on the include path. +# A host includes <term/term.h> and nothing else. The package puts only the +# parent of term/ on the include path. The headers compile alone as strict +# C11 and as C++. set -euo pipefail cd "${1:-$(dirname "$0")/..}" cc=${CC:-cc} cxx=${CXX:-c++} status=0 -echo '#include "term.h"' | "$cc" -std=c11 -Wall -Wextra -Wpedantic -Werror \ - -Iinclude/term -x c -fsyntax-only - || { echo "term.h: not valid C11" >&2; status=1; } -if command -v "$cxx" >/dev/null; then - echo '#include "term.h"' | "$cxx" -std=c++17 -Wall -Wextra -Wpedantic -Werror \ - -Iinclude/term -x c++ -fsyntax-only - || { echo "term.h: not valid C++" >&2; status=1; } -fi + +# check LABEL: compile the code on stdin as C11 and as C++, with include/ +# on the path. +check() { + local label=$1 src + src=$(cat) + printf '%s\n' "$src" | "$cc" -std=c11 -Wall -Wextra -Wpedantic -Werror \ + -Iinclude -x c -fsyntax-only - || { echo "$label: not valid C11" >&2; status=1; } + if command -v "$cxx" >/dev/null; then + printf '%s\n' "$src" | "$cxx" -std=c++17 -Wall -Wextra -Wpedantic -Werror \ + -Iinclude -x c++ -fsyntax-only - || { echo "$label: not valid C++" >&2; status=1; } + fi +} + +check term.h <<'EOF' +#include <term/term.h> +EOF # Apple headers define wide before a host includes the Unicode API. -unicode_header=$'typedef struct wide { int value; } wide;\n#include "unicode/width.h"' -echo "$unicode_header" | "$cc" -std=c11 -Wall -Wextra -Wpedantic -Werror \ - -Iinclude/term -x c -fsyntax-only - || { echo "unicode/width.h: conflicts with host types" >&2; status=1; } -if command -v "$cxx" >/dev/null; then - echo "$unicode_header" | "$cxx" -std=c++17 -Wall -Wextra -Wpedantic -Werror \ - -Iinclude/term -x c++ -fsyntax-only - || { echo "unicode/width.h: conflicts with C++ host types" >&2; status=1; } -fi +check "unicode/width.h: conflicts with host types" <<'EOF' +typedef struct wide { int value; } wide; +#include <term/unicode/width.h> +EOF + +# A host that uses the names the Unicode API had before the term_ and +# TERM_ prefixes. The API must not clash with them in either order. +check "unicode API: host names declared first" <<'EOF' +int unicode_width(int); +static int in_ranges; +static int zero_width; +static int utf8d; +struct unicode_range { int lo; }; +#define UTF8_ACCEPT 7 +#define GRAPHEME_MAX_LEN 3 +#define IN_TABLE 1 +#define UNICODE_DATA_VERSION "host" +#define UTF_8_H +#include <term/unicode/utf8.h> +#include <term/unicode/width.h> +#include <term/unicode/grapheme.h> +int host_use(void) +{ + return in_ranges + zero_width + utf8d + unicode_width(1) + UTF8_ACCEPT + GRAPHEME_MAX_LEN; +} +EOF + +check "unicode API: host names declared after" <<'EOF' +#include <term/unicode/utf8.h> +#include <term/unicode/width.h> +#include <term/unicode/grapheme.h> +int unicode_width(int); +static int in_ranges; +#define UTF8_ACCEPT 7 +#define GRAPHEME_MAX_LEN 3 +int host_use(void) +{ + return in_ranges + unicode_width(1) + UTF8_ACCEPT + GRAPHEME_MAX_LEN; +} +EOF exit $status diff --git a/tests/hist_test.c b/tests/hist_test.c index d772c5a..691ae3c 100644 --- a/tests/hist_test.c +++ b/tests/hist_test.c @@ -129,9 +129,9 @@ static void test_clusters_roundtrip(void) struct cell out[2]; assert(term_hist_decode_segment(&h, 0, 0, 2, out, g) == 2); - assert(grapheme_is_handle(out[0].c)); - uint32_t got[GRAPHEME_MAX_LEN]; - assert(term_gstore_expand(g, out[0].c, got, GRAPHEME_MAX_LEN) == 3); + assert(term_grapheme_is_handle(out[0].c)); + uint32_t got[TERM_GRAPHEME_MAX_LEN]; + assert(term_gstore_expand(g, out[0].c, got, TERM_GRAPHEME_MAX_LEN) == 3); assert(got[0] == cps[0] && got[1] == cps[1] && got[2] == cps[2]); assert(out[1].c == 'X'); @@ -623,9 +623,9 @@ static void test_fuzz_rows(void) continue; } assert(a->fg == b->fg && a->flags == b->flags); - if (grapheme_is_handle(a->c)) { - uint32_t got[GRAPHEME_MAX_LEN]; - assert(term_gstore_expand(g, b->c, got, GRAPHEME_MAX_LEN) == 2 && got[1] == 0x0301); + if (term_grapheme_is_handle(a->c)) { + uint32_t got[TERM_GRAPHEME_MAX_LEN]; + assert(term_gstore_expand(g, b->c, got, TERM_GRAPHEME_MAX_LEN) == 2 && got[1] == 0x0301); } else { assert(a->c == b->c); } diff --git a/tests/meson.build b/tests/meson.build index 69f3c49..4fda075 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -30,6 +30,7 @@ term_tests = [ ['term-resize-failure-test', files('resize_failure_test.c'), [dl_dep], ['extlib']], ['dcs-test', files('dcs_test.c'), [], []], ['osc-test', files('osc_test.c'), [], []], + ['api-args-test', files('api_args_test.c'), [], []], ] test_helpers_inc = include_directories('helpers') @@ -76,20 +77,20 @@ term_amalgamation = custom_target( term_amalgamation_lib = static_library( 'term-amalgamation', term_amalgamation[0], - dependencies: [m_dep], + dependencies: [m_dep, thread_dep], override_options: ['c_std=c11', 'warning_level=3', 'werror=true'], build_by_default: false, ) foreach entry : term_tests if entry[0] in ['term-test', 'spec-test', 'view-test', 'dcs-test', 'osc-test', 'history-text-test', 'term-input-test', 'model-test', 'model-row-test', - 'resize-property-test'] + 'resize-property-test', 'api-args-test'] test(entry[0] + '-amalgamation', executable(entry[0] + '-amalgamation', entry[1], c_args: test_c_args, include_directories: [term_inc, test_helpers_inc], link_with: term_amalgamation_lib, - dependencies: [m_dep] + entry[2], + dependencies: [m_dep, thread_dep] + entry[2], build_by_default: false), env: test_env, suite: ['unit', 'amalgamation'], timeout: 180) endif @@ -117,6 +118,17 @@ if cc.has_link_argument('-Wl,--wrap=realloc') and cc.has_link_argument('-Wl,--wr suite: 'unit', ) endif + +# The project version comes in as a define, so the header macros cannot +# drift from meson.build. +version_test = executable('version-test', 'version_test.c', + c_args: test_c_args + ['-DTERM_TEST_PROJECT_VERSION="@0@"'.format(meson.project_version())], + dependencies: [term_internal_dep], +) +test('version-test', version_test, + env: test_env, + suite: 'unit', +) test('term-cross-test', bash, args: [files('cross_test.sh'), meson.project_source_root()], suite: 'portability', diff --git a/tests/model.c b/tests/model.c index 8dead6c..6c9f288 100644 --- a/tests/model.c +++ b/tests/model.c @@ -31,7 +31,7 @@ #define STR_KEEP 1024 struct cell { - uint32_t cp[GRAPHEME_MAX_LEN]; + uint32_t cp[TERM_GRAPHEME_MAX_LEN]; uint8_t n, width; uint16_t attrs; term_color fg, bg; @@ -72,6 +72,7 @@ enum { GROUND, ESC, CSI, STR, STR_ESC, CHARSET, HASH, DOCS }; enum { OSC, DCS, IGNORE }; struct model { + char name[64]; /* XTVERSION reply */ int cols, rows, limit; struct row **scr; struct row **hist; @@ -98,7 +99,7 @@ struct model { bool tabs[MAX_TABS]; struct keyboard keyboard[2]; /* The last printed glyph, for REP and cluster joins. */ - uint32_t last[GRAPHEME_MAX_LEN]; + uint32_t last[TERM_GRAPHEME_MAX_LEN]; int last_n; int prompt; /* screen row of the last OSC 133;A, or -1 */ unsigned flags; @@ -158,7 +159,7 @@ static bool pristine(const struct model *m) static bool is_ri(uint32_t cp) { - return cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp); + return cp - 0x1F1E6u <= 0x19u && term_grapheme_regional_indicator(cp); } /* ---- replies ---- */ @@ -417,13 +418,13 @@ static void fill_gap(struct row *r, int x) static uint16_t presentation(const uint32_t *cps, int n) { - bool pictographic = unicode_extended_pictographic(cps[0]); + bool pictographic = term_unicode_extended_pictographic(cps[0]); bool emoji = false; for (int i = 1; i < n; i++) { if (cps[i] == 0xFE0F) return TERM_ATTR_EMOJI; if (cps[i] == 0xFE0E) return TERM_ATTR_TEXT; - if (cps[i] == 0x20E3 || grapheme_regional_indicator(cps[i]) || - (pictographic && (cps[i] == 0x200D || grapheme_emoji_modifier(cps[i]) || + if (cps[i] == 0x20E3 || term_grapheme_regional_indicator(cps[i]) || + (pictographic && (cps[i] == 0x200D || term_grapheme_emoji_modifier(cps[i]) || (cps[i] >= 0xE0020 && cps[i] <= 0xE007F)))) emoji = true; } @@ -443,7 +444,7 @@ static void put_glyph(struct model *m, const uint32_t *cps, int n, uint16_t attr { if (n > 1) attrs = (uint16_t)((attrs & ~(TERM_ATTR_EMOJI | TERM_ATTR_TEXT)) | presentation(cps, n)); - int width = unicode_width(cps[0]) == 2 ? 2 : 1; + int width = term_unicode_width(cps[0]) == 2 ? 2 : 1; if (attrs & TERM_ATTR_EMOJI) width = 2; if (attrs & TERM_ATTR_TEXT) width = 1; if (m->pending) wrap_to_next_row(m); @@ -495,14 +496,14 @@ static void put_glyph(struct model *m, const uint32_t *cps, int n, uint16_t attr static bool joins(uint32_t cp, int width) { if (cp < 0x300) return false; - return width == 0 || grapheme_emoji_modifier(cp) || - (width == 1 && unicode_spacing_mark(cp)); + return width == 0 || term_grapheme_emoji_modifier(cp) || + (width == 1 && term_unicode_spacing_mark(cp)); } static bool cluster_joins(const uint32_t *cps, int n, uint32_t cp, int width) { if (joins(cp, width)) return true; - if (cps[n - 1] == 0x200D) return unicode_extended_pictographic(cp); + if (cps[n - 1] == 0x200D) return term_unicode_extended_pictographic(cp); return n == 1 && is_ri(cp) && is_ri(cps[0]); } @@ -527,10 +528,10 @@ static bool extend_cluster(struct model *m, uint32_t cp, int width) return joins(cp, width); if (!cluster_joins(prev->cp, prev->n, cp, width)) return false; - if (prev->n >= GRAPHEME_MAX_LEN) + if (prev->n >= TERM_GRAPHEME_MAX_LEN) return true; struct cell old = *prev; - uint32_t cps[GRAPHEME_MAX_LEN]; + uint32_t cps[TERM_GRAPHEME_MAX_LEN]; memcpy(cps, old.cp, (size_t)old.n * sizeof(*cps)); cps[old.n] = cp; int n = old.n + 1; @@ -590,7 +591,7 @@ static void print_cp(struct model *m, uint32_t cp) emit(m, cp); return; } - int width = unicode_width(cp); + int width = term_unicode_width(cp); if (!extend_cluster(m, cp, width)) emit(m, cp); } @@ -1290,7 +1291,7 @@ static void resize_plain(struct model *m, int cols, int rows) static bool resize(struct model *m, int cols, int rows) { - if (cols > TERM_MAX_COLS) cols = TERM_MAX_COLS; + if (cols > TERM_MAX_COLS) return false; if (cols == m->cols && rows == m->rows) return true; if (cols > MAX_TABS) return false; m->top = 0; @@ -1625,7 +1626,7 @@ static void csi_dispatch(struct model *m, unsigned char final) break; case 'q': if (m->prefix == '>' && !m->inter && (!m->nparam || !m->param[0])) { - reply(m, "\033P>|wsap_term\033\\"); + reply(m, "\033P>|%s\033\\", m->name); } else if (!m->prefix && m->inter == ' ') { int ps = m->nparam ? m->param[0] : 0; if (ps == 0) { @@ -1971,10 +1972,11 @@ static void feed_byte(struct model *m, unsigned char c) /* ---- public ---- */ -struct model *model_new(struct term_vector2i size, int history) +struct model *model_new(struct term_vector2i size, int history, const char *name) { struct model *m = calloc(1, sizeof(*m)); if (!m) abort(); + snprintf(m->name, sizeof(m->name), "%s", name && *name ? name : "term"); m->cols = size.x < 1 ? 80 : size.x; m->rows = size.y < 1 ? 24 : size.y; m->limit = history_limit(history); diff --git a/tests/model.h b/tests/model.h index 9a4316f..ca2bf96 100644 --- a/tests/model.h +++ b/tests/model.h @@ -19,7 +19,7 @@ #include <stdint.h> struct model_cell { - uint32_t cp[GRAPHEME_MAX_LEN]; + uint32_t cp[TERM_GRAPHEME_MAX_LEN]; uint8_t n; /* code points; 0 for the right half of a wide cell */ uint8_t width; /* 1, 2 for a wide cell, 0 for its right half */ uint16_t attrs; /* TERM_ATTR_* */ @@ -28,8 +28,9 @@ struct model_cell { struct model; -/* history is the history limit, as in term_options.scrollback. */ -struct model *model_new(struct term_vector2i size, int history); +/* history is the history limit, as in term_options.scrollback. name is + * as in term_options.name. */ +struct model *model_new(struct term_vector2i size, int history, const char *name); void model_free(struct model *m); void model_feed(struct model *m, const char *s, size_t len, unsigned flags); bool model_resize(struct model *m, struct term_vector2i size); diff --git a/tests/model_row_test.c b/tests/model_row_test.c index 7afca05..ad4ac2c 100644 --- a/tests/model_row_test.c +++ b/tests/model_row_test.c @@ -5,7 +5,7 @@ int main(void) { - struct model *m = model_new((struct term_vector2i){ .x = 10, .y = 3 }, 10); + struct model *m = model_new((struct term_vector2i){ .x = 10, .y = 3 }, 10, NULL); model_feed(m, "A\r\nB\r\nC\r\nD", strlen("A\r\nB\r\nC\r\nD"), 0); assert(model_history(m) == 1); assert(model_row(m, -1) == NULL); diff --git a/tests/model_test.c b/tests/model_test.c index 395097e..d3988c6 100644 --- a/tests/model_test.c +++ b/tests/model_test.c @@ -122,8 +122,8 @@ static void dump_all(struct model *m, struct term *t, struct term_snapshot *snap term_snapshot_update(snap, t); const struct term_cell *row = term_snapshot_row(snap, r < total ? 0 : r - total); for (int x = 0; x < size.x; x++) { - uint32_t cp[GRAPHEME_MAX_LEN] = {0}; - if (row[x].text) term_snapshot_cluster(snap, row[x].text, cp, GRAPHEME_MAX_LEN); + uint32_t cp[TERM_GRAPHEME_MAX_LEN] = {0}; + if (row[x].text) term_snapshot_cluster(snap, row[x].text, cp, TERM_GRAPHEME_MAX_LEN); fputc(glyph(cp[0]), log); } } else { @@ -201,9 +201,9 @@ static const char *compare(struct model *m, struct term *t, struct term_snapshot const struct model_cell *want_row = model_row(m, retained); for (int x = 0; x < size.x; x++) { const struct model_cell *e = &want_row[x]; - uint32_t cps[GRAPHEME_MAX_LEN] = {0}; + uint32_t cps[TERM_GRAPHEME_MAX_LEN] = {0}; int n = row[x].text ? term_snapshot_cluster(snap, row[x].text, cps, - GRAPHEME_MAX_LEN) : 0; + TERM_GRAPHEME_MAX_LEN) : 0; if (n == e->n && row[x].width == e->width && row[x].attrs == e->attrs && row[x].fg == e->fg && row[x].bg == e->bg && !memcmp(cps, e->cp, (size_t)n * sizeof(*cps))) @@ -211,9 +211,9 @@ static const char *compare(struct model *m, struct term *t, struct term_snapshot if (log) { fprintf(log, "retained row %d (offset %d):\n term: ", retained, offset); for (int k = 0; k < size.x; k++) { - uint32_t cp[GRAPHEME_MAX_LEN] = {0}; + uint32_t cp[TERM_GRAPHEME_MAX_LEN] = {0}; if (row[k].text) - term_snapshot_cluster(snap, row[k].text, cp, GRAPHEME_MAX_LEN); + term_snapshot_cluster(snap, row[k].text, cp, TERM_GRAPHEME_MAX_LEN); fputc(glyph(cp[0]), log); } fprintf(log, "\n model: "); @@ -256,7 +256,7 @@ static int run(const struct script *s, enum chunking chunks, uint64_t chunk_seed .callbacks = { .ud = &got, .reply = on_reply }, }; struct term *t = term_new(&opt); - struct model *m = model_new(s->size, s->history); + struct model *m = model_new(s->size, s->history, NULL); struct term_snapshot *snap = term_snapshot_new(); if (!t || !snap) abort(); uint64_t saved_rng = rng; diff --git a/tests/osc_test.c b/tests/osc_test.c index 41a9247..9d08157 100644 --- a/tests/osc_test.c +++ b/tests/osc_test.c @@ -104,6 +104,35 @@ static void reset_captures(void) notify_count = 0; } +/* The notification default is term_options.name. A DCS +p does not change it. */ +static void test_host_name_notify(void) +{ + struct term_options opt = { + .size = { .x = 80, .y = 24 }, + .scrollback = TEST_SCROLLBACK, + .name = "myterm", + .callbacks = { .notify = capture_notify }, + }; + struct term *t = term_new(&opt); + assert(t); + reset_captures(); + write_seq(t, "\033]9;hello\007"); + assert(notify_count == 1); + assert(!strcmp(notify_title, "myterm")); + assert(!strcmp(notify_body, "hello")); + + write_seq(t, "\033P+p6f74686572\033\\"); + reset_captures(); + write_seq(t, "\033]9;again\007"); + assert(notify_count == 1); + assert(!strcmp(notify_title, "myterm")); + + write_seq(t, "\033]777;notify;Title;Body\007"); + assert(notify_count == 2); + assert(!strcmp(notify_title, "Title")); + term_free(t); +} + static void test_osc52(void) { struct term *t = osc_test_term(); @@ -289,7 +318,7 @@ static void test_osc_misc(void) write_seq(t, "\033]9;hello there\033\\"); assert(notify_count == 1); - assert(!strcmp(notify_title, "wsap_term")); + assert(!strcmp(notify_title, "term")); assert(!strcmp(notify_body, "hello there")); write_seq(t, "\033]9;4;3;50\007"); assert(notify_count == 1); @@ -450,6 +479,7 @@ int main(void) test_osc8(); test_sgr_reset_keeps_link(); test_osc_misc(); + test_host_name_notify(); test_osc_colors(); test_osc_legacy_titles(); test_palette_and_theme(); diff --git a/tests/term_test.c b/tests/term_test.c index 66dbd96..235e9df 100644 --- a/tests/term_test.c +++ b/tests/term_test.c @@ -4,6 +4,7 @@ #include <assert.h> #include <limits.h> #include <pthread.h> +#include <sched.h> #include <stdatomic.h> #include <stdbool.h> #include <stdio.h> @@ -1440,6 +1441,55 @@ static void test_snapshot_follows_new_terminal(void) term_snapshot_free(s); } +/* The getters take a NULL snapshot and an out-of-range row like a NULL + * terminal: they return the empty value. The view keeps its address. */ +static void test_snapshot_null_and_range(void) +{ + uint32_t cps[4] = { 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA }; + assert(term_snapshot_view(NULL) == NULL); + assert(term_snapshot_row(NULL, 0) == NULL); + assert(term_snapshot_row_key(NULL, 0) == 0); + struct term_span none = term_snapshot_row_selection(NULL, 0); + assert(none.begin == 0 && none.end == 0); + assert(term_snapshot_cluster(NULL, 'a', cps, 4) == 0); + assert(term_snapshot_cluster(NULL, TERM_TEXT_CLUSTER, cps, 4) == 0); + assert(cps[0] == 0xAAAA); + + struct term *t = tt_new(4, 2); + tt_write(t, "e\xcc\x81", 3); + struct term_snapshot *s = term_snapshot_new(); + assert(term_snapshot_update(s, t)); + + const struct term_view *view = term_snapshot_view(s); + const uint32_t *palette = view->palette; + assert(term_snapshot_update(s, t)); + assert(term_snapshot_view(s) == view && view->palette == palette); + + assert(term_snapshot_row(s, -1) == NULL); + assert(term_snapshot_row(s, 2) == NULL); + assert(term_snapshot_row_key(s, -1) == 0); + assert(term_snapshot_row_key(s, 2) == 0); + struct term_span off = term_snapshot_row_selection(s, 2); + assert(off.begin == 0 && off.end == 0); + + /* A cluster cell gives its count with no buffer, and with cap 0 it + * writes nothing. */ + uint32_t text = term_snapshot_row(s, 0)[0].text; + assert(text & TERM_TEXT_CLUSTER); + assert(term_snapshot_cluster(s, text, NULL, 0) == 2); + assert(term_snapshot_cluster(s, text, cps, 0) == 2); + assert(cps[0] == 0xAAAA); + assert(term_snapshot_cluster(s, text, NULL, 4) == 2); + /* A plain code point is one code point, and cap 0 writes nothing. */ + assert(term_snapshot_cluster(s, 'a', NULL, 0) == 1); + assert(term_snapshot_cluster(s, 'a', cps, 0) == 1); + assert(cps[0] == 0xAAAA); + assert(term_snapshot_cluster(s, 'a', cps, 4) == 1 && cps[0] == 'a'); + + term_snapshot_free(s); + term_free(t); +} + static void test_string_payloads(void) { static const char *prefixes[] = {"\033]0;", "\033]7;file://host/", "\033P"}; @@ -1485,6 +1535,32 @@ static void test_string_payloads(void) } } +/* term_new() refuses a name that a DCS reply could echo unsafely. */ +static void test_host_name_rejected(void) +{ + static const char *const bad[] = { + "\033x", "a\tb", "\177", "\300", "a\nb", + }; + char longest[65]; + memset(longest, 'a', 64); + longest[64] = 0; + char fits[64]; + memset(fits, 'b', 63); + fits[63] = 0; + + for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = bad[i] }; + assert(!term_new(&opt)); + } + struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = longest }; + assert(!term_new(&opt)); + + opt.name = fits; + struct term *t = term_new(&opt); + assert(t); + term_free(t); +} + static void test_newline_policy_survives_reset(void) { static const char *const resets[] = { "\033c", "\033[!p", "\033[20l" }; @@ -1762,21 +1838,22 @@ struct concurrent_worker_args { _Atomic bool done; }; +static const char concurrent_stream[] = + "The quick brown fox jumps over the lazy dog.\r\n" + "\033[31mRed text\033[0m and \033[32mgreen text\033[0m.\r\n" + "Some unicode: 漢字, ひらがな, and emojis: 😀🚀🎉\r\n" + "\033[2J\033[HCleared and reset\r\n" + "Line 1\r\nLine 2\r\nLine 3\r\nLine 4\r\nLine 5\r\n" + "\033]7;file:///tmp/dir\007\033]22;pointer\007" + "\033]8;;https://example.com\007link\033]8;;\007\r\n"; + static void *concurrent_feed_worker(void *arg) { struct concurrent_worker_args *w = arg; - static const char stream[] = - "The quick brown fox jumps over the lazy dog.\r\n" - "\033[31mRed text\033[0m and \033[32mgreen text\033[0m.\r\n" - "Some unicode: 漢字, ひらがな, and emojis: 😀🚀🎉\r\n" - "\033[2J\033[HCleared and reset\r\n" - "Line 1\r\nLine 2\r\nLine 3\r\nLine 4\r\nLine 5\r\n" - "\033]7;file:///tmp/dir\007\033]22;pointer\007" - "\033]8;;https://example.com\007link\033]8;;\007\r\n"; - size_t len = strlen(stream); + size_t len = strlen(concurrent_stream); for (int i = 0; i < 2000; i++) { - term_feed(w->t, stream, len, 0); + term_feed(w->t, concurrent_stream, len, 0); } atomic_store(&w->done, true); return NULL; @@ -1784,10 +1861,11 @@ static void *concurrent_feed_worker(void *arg) static void test_concurrent_feed_and_snapshot(void) { - struct term *t = term_new(&(struct term_options){ + struct term_options opt = { .size = { .x = 80, .y = 24 }, .scrollback = 1000, - }); + }; + struct term *t = term_new(&opt); assert(t != NULL); struct concurrent_worker_args w = { .t = t, .done = false }; @@ -1828,13 +1906,104 @@ static void test_concurrent_feed_and_snapshot(void) bool ok = term_snapshot_update(snap, t); assert(ok); + /* The reader changed only the view and the selection. The screen and + * the history must match a terminal that got the same bytes on one + * thread. */ + struct term *ref = term_new(&opt); + assert(ref != NULL); + for (int i = 0; i < 2000; i++) + tt_write(ref, concurrent_stream, strlen(concurrent_stream)); + assert(tt_cols(t) == tt_cols(ref) && tt_rows(t) == tt_rows(ref)); + assert(tt_history(t) == tt_history(ref)); + assert(tt_cursor_x(t) == tt_cursor_x(ref) && tt_cursor_y(t) == tt_cursor_y(ref)); + char *got = malloc((size_t)tt_cols(t) + 1); + char *want = malloc((size_t)tt_cols(ref) + 1); + assert(got && want); + for (int y = 0; y < tt_rows(t); y++) { + row_text(t, y, got); + row_text(ref, y, want); + assert(!strcmp(got, want)); + } + free(got); + free(want); + char *text = term_history_text(t, 1000); + char *text_ref = term_history_text(ref, 1000); + assert(text && text_ref && !strcmp(text, text_ref)); + term_text_free(text); + term_text_free(text_ref); + + term_free(ref); term_snapshot_free(snap); term_free(t); } +static void test_reserved_options_must_be_zero(void) +{ + struct term_options opt = { .size = { .x = 10, .y = 2 } }; + struct term *t = term_new(&opt); + assert(t != NULL); + term_free(t); + + size_t words = sizeof(opt.reserved) / sizeof(opt.reserved[0]); + for (size_t i = 0; i < words; i++) { + opt.reserved[i] = 1; + assert(term_new(&opt) == NULL); + opt.reserved[i] = 0; + } + opt.reserved[words - 1] = 0x80000000u; + assert(term_new(&opt) == NULL); + opt.reserved[words - 1] = 0; + t = term_new(&opt); + assert(t != NULL); + term_free(t); +} + +struct lock_probe { + struct term *t; + _Atomic bool entered; +}; + +static void *lock_probe_worker(void *arg) +{ + struct lock_probe *p = arg; + term_lock(p->t); + atomic_store(&p->entered, true); + term_unlock(p->t); + return NULL; +} + +/* term_lock() nests on the owning thread. Another thread waits until the + * last term_unlock(). */ +static void test_lock_reentrant_across_threads(void) +{ + struct term *t = term_new(&(struct term_options){ + .size = { .x = 80, .y = 24 }, + }); + assert(t != NULL); + + term_lock(t); + term_lock(t); + assert(tt_cols(t) == 80); + term_unlock(t); + + struct lock_probe p = { .t = t, .entered = false }; + pthread_t th; + assert(pthread_create(&th, NULL, lock_probe_worker, &p) == 0); + for (int i = 0; i < 1000; i++) + sched_yield(); + assert(!atomic_load(&p.entered)); + term_unlock(t); + + assert(pthread_join(th, NULL) == 0); + assert(atomic_load(&p.entered)); + term_free(t); +} + int main(void) { + test_reserved_options_must_be_zero(); test_concurrent_feed_and_snapshot(); + test_lock_reentrant_across_threads(); test_grow_past_reservation(); test_skipped_cells_read_blank(); test_csi_text_chunks(); @@ -1859,17 +2028,19 @@ int main(void) test_alt_shrink_then_flood(); test_alt_erase_scrollback(); test_snapshot_follows_new_terminal(); + test_snapshot_null_and_range(); test_bulk_rows_survive_edits(); - - assert(!strcmp(UNICODE_DATA_VERSION, "18.0.0")); - assert(!strcmp(term_unicode_version(), UNICODE_DATA_VERSION)); - assert(unicode_width('A') == 1); - assert(unicode_width(0x0301) == 0); - assert(unicode_width(0x754C) == 2); - assert(unicode_width(0x1FAE0) == 2); - assert(unicode_emoji_presentation(0x1FAE0)); - assert(unicode_width(0x1FAEB) == 2); - assert(unicode_emoji_presentation(0x1FAEB)); + test_host_name_rejected(); + + assert(!strcmp(TERM_UNICODE_DATA_VERSION, "18.0.0")); + assert(!strcmp(term_unicode_version(), TERM_UNICODE_DATA_VERSION)); + assert(term_unicode_width('A') == 1); + assert(term_unicode_width(0x0301) == 0); + assert(term_unicode_width(0x754C) == 2); + assert(term_unicode_width(0x1FAE0) == 2); + assert(term_unicode_emoji_presentation(0x1FAE0)); + assert(term_unicode_width(0x1FAEB) == 2); + assert(term_unicode_emoji_presentation(0x1FAEB)); { /* A Jamo Extended-B vowel draws inside its leading consonant. */ struct term *t = term_new(&(struct term_options){ .size = { .x = 10, .y = 2 } }); diff --git a/tests/unicode/unicode_test.c b/tests/unicode/unicode_test.c index 7fc6e3e..e3cf5a7 100644 --- a/tests/unicode/unicode_test.c +++ b/tests/unicode/unicode_test.c @@ -16,17 +16,17 @@ static void test_utf8_decode_one(void) for (size_t i = 0; i < sizeof(valid) / sizeof(*valid); i++) { uint32_t cp = 0xdeadbeef; for (int n = 0; n < valid[i].len; n++) { - int r = utf8_decode_one(valid[i].bytes, n, &cp); + int r = term_utf8_decode_one(valid[i].bytes, n, &cp); assert(r == 0); assert(cp == 0xdeadbeef); } - int r = utf8_decode_one(valid[i].bytes, valid[i].len, &cp); + int r = term_utf8_decode_one(valid[i].bytes, valid[i].len, &cp); assert(r == valid[i].len); assert(cp == valid[i].cp); char followed[8]; memcpy(followed, valid[i].bytes, valid[i].len); memcpy(followed + valid[i].len, "next", 4); - r = utf8_decode_one(followed, valid[i].len + 4, &cp); + r = term_utf8_decode_one(followed, valid[i].len + 4, &cp); assert(r == valid[i].len); assert(cp == valid[i].cp); } @@ -38,7 +38,7 @@ static void test_utf8_decode_one(void) }; for (size_t i = 0; i < sizeof(invalid) / sizeof(*invalid); i++) { uint32_t cp = 0xdeadbeef; - int r = utf8_decode_one(invalid[i], strlen(invalid[i]), &cp); + int r = term_utf8_decode_one(invalid[i], strlen(invalid[i]), &cp); assert(r == 0); assert(cp == 0xdeadbeef); } @@ -48,24 +48,24 @@ static void test_scalar_roundtrips(void) { for (uint32_t cp = 0; cp <= 0x10ffff; cp++) { if (cp >= 0xd800 && cp <= 0xdfff) continue; - char bytes[UTF8_MAX_BYTES]; - int len = utf8_encode(cp, bytes); - uint32_t scalar = 0, streamed = 0, state = UTF8_ACCEPT; - int r = utf8_decode_one(bytes, (size_t)len, &scalar); + char bytes[TERM_UTF8_MAX_BYTES]; + int len = term_utf8_encode(cp, bytes); + uint32_t scalar = 0, streamed = 0, state = TERM_UTF8_ACCEPT; + int r = term_utf8_decode_one(bytes, (size_t)len, &scalar); assert(r == len); assert(scalar == cp); for (int i = 0; i < len; i++) { - utf8_decode(&state, &streamed, (unsigned char)bytes[i]); - if (i + 1 < len) assert(state != UTF8_ACCEPT && state != UTF8_REJECT); + term_utf8_decode(&state, &streamed, (unsigned char)bytes[i]); + if (i + 1 < len) assert(state != TERM_UTF8_ACCEPT && state != TERM_UTF8_REJECT); } - assert(state == UTF8_ACCEPT && streamed == cp); + assert(state == TERM_UTF8_ACCEPT && streamed == cp); } static const uint32_t invalid[] = {0xd800, 0xdfff, 0x110000, UINT32_MAX}; for (size_t i = 0; i < sizeof(invalid) / sizeof(*invalid); i++) { - char bytes[UTF8_MAX_BYTES]; - int len = utf8_encode(invalid[i], bytes); + char bytes[TERM_UTF8_MAX_BYTES]; + int len = term_utf8_encode(invalid[i], bytes); uint32_t cp = 0; - int r = utf8_decode_one(bytes, len, &cp); + int r = term_utf8_decode_one(bytes, len, &cp); assert(len == 3 && r == len && cp == 0xfffd); } } @@ -74,26 +74,26 @@ static void test_utf8_sanitize(void) { char dst[64]; - utf8_sanitize(dst, sizeof(dst), "hello \xe2\x82\xac\xf0\x9f\x98\x80"); + term_utf8_sanitize(dst, sizeof(dst), "hello \xe2\x82\xac\xf0\x9f\x98\x80"); assert(!strcmp(dst, "hello \xe2\x82\xac\xf0\x9f\x98\x80")); - utf8_sanitize(dst, sizeof(dst), "a\xff\x80" "b\xed\xa0\x80" "c\xe2\x82"); + term_utf8_sanitize(dst, sizeof(dst), "a\xff\x80" "b\xed\xa0\x80" "c\xe2\x82"); assert(!strcmp(dst, "abc")); - utf8_sanitize(dst, sizeof(dst), ""); + term_utf8_sanitize(dst, sizeof(dst), ""); assert(!strcmp(dst, "")); - utf8_sanitize(dst, sizeof(dst), "\xff\xfe"); + term_utf8_sanitize(dst, sizeof(dst), "\xff\xfe"); assert(!strcmp(dst, "")); - utf8_sanitize(dst, 6, "ab\xe2\x82\xac" "cd"); + term_utf8_sanitize(dst, 6, "ab\xe2\x82\xac" "cd"); assert(!strcmp(dst, "ab\xe2\x82\xac")); - utf8_sanitize(dst, 5, "ab\xe2\x82\xac" "cd"); + term_utf8_sanitize(dst, 5, "ab\xe2\x82\xac" "cd"); assert(!strcmp(dst, "abcd")); memset(dst, 'x', sizeof(dst)); - utf8_sanitize(dst, 4, "abcdef"); + term_utf8_sanitize(dst, 4, "abcdef"); assert(!strcmp(dst, "abc")); memset(dst, 'x', sizeof(dst)); - utf8_sanitize(dst, 0, "abc"); + term_utf8_sanitize(dst, 0, "abc"); assert(dst[0] == 'x'); } @@ -101,30 +101,30 @@ static void test_utf8_sanitize(void) * stays unused. */ static void test_ground_invalid_subparts(void) { - uint32_t state = UTF8_ACCEPT, codep = 0, cp = 0xdead; - int n = utf8_decode_ground(&state, &codep, "\xe2\x80\x1b\xac", 4, &cp); + uint32_t state = TERM_UTF8_ACCEPT, codep = 0, cp = 0xdead; + int n = term_utf8_decode_ground(&state, &codep, "\xe2\x80\x1b\xac", 4, &cp); assert(n == 2); assert(cp == 0xFFFD); - assert(state == UTF8_ACCEPT); - n = utf8_decode_ground(&state, &codep, "\xac", 1, &cp); + assert(state == TERM_UTF8_ACCEPT); + n = term_utf8_decode_ground(&state, &codep, "\xac", 1, &cp); assert(n == 1); assert(cp == 0xFFFD); - assert(state == UTF8_ACCEPT); - n = utf8_decode_ground(&state, &codep, "\xe2\x82z", 3, &cp); + assert(state == TERM_UTF8_ACCEPT); + n = term_utf8_decode_ground(&state, &codep, "\xe2\x82z", 3, &cp); assert(n == 2); assert(cp == 0xFFFD); - assert(state == UTF8_ACCEPT); - n = utf8_decode_ground(&state, &codep, "\xe2\x82", 2, &cp); + assert(state == TERM_UTF8_ACCEPT); + n = term_utf8_decode_ground(&state, &codep, "\xe2\x82", 2, &cp); assert(n == 2); assert(cp == 0); - assert(state != UTF8_ACCEPT); + assert(state != TERM_UTF8_ACCEPT); - state = UTF8_ACCEPT; + state = TERM_UTF8_ACCEPT; codep = 0; - n = utf8_decode_ground(&state, &codep, "\xe2\x82\xac", 3, &cp); + n = term_utf8_decode_ground(&state, &codep, "\xe2\x82\xac", 3, &cp); assert(n == 3); assert(cp == 0x20ac); - assert(state == UTF8_ACCEPT); + assert(state == TERM_UTF8_ACCEPT); } int main(void) diff --git a/tests/version_test.c b/tests/version_test.c new file mode 100644 index 0000000..0eb889e --- /dev/null +++ b/tests/version_test.c @@ -0,0 +1,27 @@ +#include "term/term.h" + +#include <assert.h> +#include <stdio.h> +#include <string.h> + +/* The project version from meson.build. The build defines it. */ +#ifndef TERM_TEST_PROJECT_VERSION +#error "define TERM_TEST_PROJECT_VERSION as the project version, such as \"0.1.0\"" +#endif + +int main(void) +{ + char text[32]; + snprintf(text, sizeof(text), "%d.%d.%d", + TERM_VERSION_MAJOR, TERM_VERSION_MINOR, TERM_VERSION_PATCH); + assert(!strcmp(text, TERM_TEST_PROJECT_VERSION)); + + int major, minor, patch; + assert(sscanf(TERM_TEST_PROJECT_VERSION, "%d.%d.%d", &major, &minor, &patch) == 3); + assert(TERM_VERSION_CODE == TERM_VERSION(major, minor, patch)); + assert(term_version() == TERM_VERSION_CODE); + + assert(TERM_VERSION(0, 2, 0) > TERM_VERSION(0, 1, 255)); + assert(TERM_VERSION(1, 0, 0) > TERM_VERSION(0, 255, 255)); + return 0; +} diff --git a/tools/amalgamate.py b/tools/amalgamate.py index f2cdf3b..09c6c91 100644 --- a/tools/amalgamate.py +++ b/tools/amalgamate.py @@ -3,7 +3,7 @@ term.h joins the public headers. term.c joins every source file and puts each private header in at its first include. A host compiles term.c once, -with any C11 compiler, and links libm. +with any C11 compiler, and links libm and the thread library. amalgamate.py TERM_DIR OUT_DIR [DEPFILE] diff --git a/tools/generate_unicode.py b/tools/generate_unicode.py index 85c646d..22f12a6 100644 --- a/tools/generate_unicode.py +++ b/tools/generate_unicode.py @@ -71,7 +71,7 @@ def unicode_categories(path): return by_category def emit_table(out, name, ranges): - out.append(f"static const struct unicode_range {name}[] = {{") + out.append(f"static const struct term_unicode_range {name}[] = {{") for i in range(0, len(ranges), 4): chunk = ranges[i:i + 4] out.append("\t" + ",".join(f"{{0x{lo:X},0x{hi:X}}}" for lo, hi in chunk) + ",") @@ -104,13 +104,13 @@ def main(): emoji_presentation = field_ranges(args.emoji_data, {"Emoji_Presentation"}) out = [f"/* Generated from Unicode {version} data by tools/generate_unicode.py. */", "", - f'#define UNICODE_DATA_VERSION "{version}"', ""] - emit_table(out, "zero_width", zero) - emit_table(out, "nonprintable", nonprint) - emit_table(out, "unicode_wide", wide) - emit_table(out, "spacing_mark", spacing) - emit_table(out, "extended_pictographic", pictographic) - emit_table(out, "emoji_presentation", emoji_presentation) + f'#define TERM_UNICODE_DATA_VERSION "{version}"', ""] + emit_table(out, "term_zero_width", zero) + emit_table(out, "term_nonprintable", nonprint) + emit_table(out, "term_unicode_wide", wide) + emit_table(out, "term_spacing_mark", spacing) + emit_table(out, "term_extended_pictographic", pictographic) + emit_table(out, "term_emoji_presentation", emoji_presentation) args.output.write_text("\n".join(out)) if __name__ == "__main__": |
