summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md24
-rw-r--r--benchmarks/term_write_bench.c5
-rw-r--r--include/term/term.h71
-rw-r--r--include/term/term_input.h6
-rw-r--r--include/term/unicode/grapheme.h12
-rw-r--r--include/term/unicode/unicode_data.inc14
-rw-r--r--include/term/unicode/utf8.h72
-rw-r--r--include/term/unicode/width.h30
-rw-r--r--meson.build8
-rw-r--r--src/compat.h75
-rw-r--r--src/core.c24
-rw-r--r--src/escape.c4
-rw-r--r--src/gstore.c28
-rw-r--r--src/hist.c14
-rw-r--r--src/input.c4
-rw-r--r--src/internal.h20
-rw-r--r--src/meson.build8
-rw-r--r--src/osc/misc.c2
-rw-r--r--src/screen.c47
-rw-r--r--src/selection.c10
-rw-r--r--src/snapshot.c26
-rw-r--r--src/term.c7
-rw-r--r--src/text.c52
-rw-r--r--src/text_run.c24
-rw-r--r--src/text_run.h18
-rw-r--r--tests/api_args_test.c140
-rw-r--r--tests/dcs_test.c53
-rw-r--r--tests/gstore_test.c38
-rw-r--r--tests/header_test.sh75
-rw-r--r--tests/hist_test.c12
-rw-r--r--tests/meson.build18
-rw-r--r--tests/model.c34
-rw-r--r--tests/model.h7
-rw-r--r--tests/model_row_test.c2
-rw-r--r--tests/model_test.c14
-rw-r--r--tests/osc_test.c32
-rw-r--r--tests/term_test.c215
-rw-r--r--tests/unicode/unicode_test.c68
-rw-r--r--tests/version_test.c27
-rw-r--r--tools/amalgamate.py2
-rw-r--r--tools/generate_unicode.py16
41 files changed, 997 insertions, 361 deletions
diff --git a/README.md b/README.md
index e877a01..c77883a 100644
--- a/README.md
+++ b/README.md
@@ -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)
{
diff --git a/src/core.c b/src/core.c
index b352be1..dd6232a 100644
--- a/src/core.c
+++ b/src/core.c
@@ -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;
}
diff --git a/src/hist.c b/src/hist.c
index 344cdc8..fff0bfe 100644
--- a/src/hist.c
+++ b/src/hist.c
@@ -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;
}
diff --git a/src/term.c b/src/term.c
index 1b16670..5839da3 100644
--- a/src/term.c
+++ b/src/term.c
@@ -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)
diff --git a/src/text.c b/src/text.c
index 43c3ef5..17f3f4f 100644
--- a/src/text.c
+++ b/src/text.c
@@ -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__":