summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--benchmarks/term_write_bench.c5
-rw-r--r--include/term/term.h52
-rw-r--r--include/term/term_input.h6
-rw-r--r--src/meson.build6
-rw-r--r--src/screen.c22
-rw-r--r--src/snapshot.c14
-rw-r--r--src/term.c5
-rw-r--r--src/text.c14
-rw-r--r--tests/api_args_test.c140
-rw-r--r--tests/meson.build14
-rw-r--r--tests/model.c2
-rw-r--r--tests/term_test.c72
-rw-r--r--tests/version_test.c27
13 files changed, 343 insertions, 36 deletions
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 aa1a3d7..9d5a882 100644
--- a/include/term/term.h
+++ b/include/term/term.h
@@ -41,6 +41,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,9 +149,14 @@ 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;
@@ -154,12 +169,16 @@ struct term_options {
*/
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
@@ -172,6 +191,7 @@ 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);
@@ -182,8 +202,9 @@ TERM_API void term_free(struct term *t);
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
@@ -204,7 +225,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);
@@ -415,11 +441,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. */
@@ -430,10 +461,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/src/meson.build b/src/meson.build
index 694ee0e..7223caa 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',
@@ -23,6 +27,8 @@ term_lib = both_libraries('term',
include_directories: term_private_inc,
dependencies: [m_dep],
gnu_symbol_visibility: 'hidden',
+ version: meson.project_version(),
+ soversion: term_soversion,
install: true
)
diff --git a/src/screen.c b/src/screen.c
index e728cec..0857f17 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;
}
@@ -858,13 +857,18 @@ struct term *term_new(const struct term_options *opt)
static const struct term_options defaults;
if (!opt)
opt = &defaults;
+ 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 < 1 ? 80 : opt->size.x;
- int rows = opt->size.y < 1 ? 24 : opt->size.y;
- if (cols > TERM_MAX_COLS)
- cols = TERM_MAX_COLS;
+ 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));
@@ -910,7 +914,7 @@ 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;
@@ -1538,10 +1542,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/snapshot.c b/src/snapshot.c
index 8dbb498..c578f18 100644
--- a/src/snapshot.c
+++ b/src/snapshot.c
@@ -314,34 +314,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;
}
@@ -350,7 +352,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..bf8f3cf 100644
--- a/src/term.c
+++ b/src/term.c
@@ -8,6 +8,11 @@ const char *term_unicode_version(void)
return UNICODE_DATA_VERSION;
}
+unsigned term_version(void)
+{
+ return TERM_VERSION_CODE;
+}
+
void term_lock(struct term *t)
{
term_write_begin(t);
diff --git a/src/text.c b/src/text.c
index 43c3ef5..a646195 100644
--- a/src/text.c
+++ b/src/text.c
@@ -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);
@@ -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/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/meson.build b/tests/meson.build
index 69f3c49..629f6d5 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')
@@ -83,7 +84,7 @@ term_amalgamation_lib = static_library(
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,
@@ -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 b538ba1..d212acb 100644
--- a/tests/model.c
+++ b/tests/model.c
@@ -1291,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;
diff --git a/tests/term_test.c b/tests/term_test.c
index e4ffb27..51544ef 100644
--- a/tests/term_test.c
+++ b/tests/term_test.c
@@ -1440,6 +1440,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"};
@@ -1858,8 +1907,30 @@ static void test_concurrent_feed_and_snapshot(void)
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);
+}
+
int main(void)
{
+ test_reserved_options_must_be_zero();
test_concurrent_feed_and_snapshot();
test_grow_past_reservation();
test_skipped_cells_read_blank();
@@ -1885,6 +1956,7 @@ 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();
test_host_name_rejected();
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;
+}