diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
| commit | 4384435941b8c7658856b381d071aee2e0035e89 (patch) | |
| tree | 5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /tests/osc_test.c | |
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject
with no dependency on the main tree.
- include/term/ holds term.h and the header-only unicode/. src/ holds
the core and its private headers. The main tree gets only
include/term through term_dep, so it cannot reach a private header.
- The term tests, the term benchmarks, the Kitty parser suite, and
tools/amalgamate.py and tools/generate_unicode.py move with it. The
tests run as term:<name>.
- The main build passes its -march (arch_flag) and b_ndebug to the
subproject. A directory configured before this commit needs
meson configure -Dterm:arch_flag=-march=... once.
- The term tests compile with -UNDEBUG. They do work inside assert.
- The subproject exports term_test_dep with term_api.h, for the main
tree's render, thumbnail, color, and selection tests.
- The Kitty bridge used struct vector2i and has not compiled since the
public API redesign. It builds again. 2 of its 14 tests fail: REP
after a tab, and a cursor report after a resize.
- dirgraph.py drops the term and unicode layers. Its self-test uses pty.
- The symbols test ignores the __odr_asan symbols that ASan adds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests/osc_test.c')
| -rw-r--r-- | tests/osc_test.c | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/tests/osc_test.c b/tests/osc_test.c new file mode 100644 index 0000000..ea0ef08 --- /dev/null +++ b/tests/osc_test.c @@ -0,0 +1,400 @@ +#include "term_api.h" +#include "term/unicode/unicode.h" +#include <assert.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void write_seq(struct term *t, const char *s) +{ + for (; *s; s++) tt_write(t, s, 1); +} + +static void write_buf(struct term *t, const char *s, size_t len) +{ + tt_write(t, s, len); +} + +static char reply[256]; +static void capture_reply(void *ud, const char *s, size_t len) +{ + (void)ud; + snprintf(reply, sizeof(reply), "%.*s", (int)len, s); +} + +static char title[256]; +static void capture_title(void *ud, const char *s) +{ + (void)ud; + snprintf(title, sizeof(title), "%s", s ? s : ""); +} + +static char clip_sel[16]; +static char clip_data[4096]; +static size_t clip_len; +static int clip_count; +static void capture_clip(void *ud, const char *sel, const char *data, size_t len) +{ + (void)ud; + snprintf(clip_sel, sizeof(clip_sel), "%s", sel ? sel : ""); + assert(len < sizeof(clip_data)); + memcpy(clip_data, data, len); + clip_len = len; + clip_count++; +} + +static char notify_title[256]; +static char notify_body[1024]; +static int notify_count; +static void capture_notify(void *ud, const char *t_, const char *b) +{ + (void)ud; + snprintf(notify_title, sizeof(notify_title), "%s", t_ ? t_ : ""); + snprintf(notify_body, sizeof(notify_body), "%s", b ? b : ""); + notify_count++; +} + +static struct term *osc_test_term(void) +{ + struct term *t = tt_new_opts(80, 24, &(struct term_callbacks){ + .reply = capture_reply, + .title = capture_title, + .clipboard = capture_clip, + .notify = capture_notify, + }); + assert(t); + return t; +} + +/* Check the OSC 8 link or the detected URL at a viewport cell. NULL means + * no link. */ +static void assert_link(struct term *t, int col, int row, const char *want) +{ + char *url = tt_link_at(t, col, row); + if (want) + assert(url && !strcmp(url, want)); + else + assert(!url); + free(url); +} + +static uint32_t cursor_color(struct term *t) +{ + return term_snapshot_view(tt_snapshot_for(t))->palette[TERM_PALETTE_CURSOR]; +} + +/* Send a color query and check the reply. */ +static void assert_query(struct term *t, const char *query, const char *want) +{ + reply[0] = 0; + write_seq(t, query); + if (strcmp(reply, want)) { + fprintf(stderr, "reply to %s: got \"%s\" want \"%s\"\n", query, reply, want); + assert(!"reply mismatch"); + } +} + +static void reset_captures(void) +{ + reply[0] = 0; + title[0] = clip_sel[0] = clip_data[0] = notify_title[0] = notify_body[0] = 0; + clip_len = 0; + clip_count = 0; + notify_count = 0; +} + +static void test_osc52(void) +{ + struct term *t = osc_test_term(); + reset_captures(); + write_seq(t, "\033]52;c;aGVsbG8=\007"); + assert(clip_count == 1 && !strcmp(clip_sel, "c")); + assert(clip_len == 5 && !memcmp(clip_data, "hello", 5)); + + reset_captures(); + write_seq(t, "\033]52;c;aGVsbG8=\033\\"); + assert(clip_count == 1 && clip_len == 5 && !memcmp(clip_data, "hello", 5)); + + reset_captures(); + static const char bulk[] = "\033]52;c;aGVsbG8=\007"; + write_buf(t, bulk, sizeof(bulk) - 1); + assert(clip_count == 1 && clip_len == 5 && !memcmp(clip_data, "hello", 5)); + + reset_captures(); + char big[7 + 400 + 1]; + memcpy(big, "\033]52;c;", 7); + for (int i = 0; i < 400; i += 4) + memcpy(big + 7 + i, "QUJD", 4); + big[407] = '\007'; + write_buf(t, big, sizeof(big)); + assert(clip_count == 1 && clip_len == 300); + for (int i = 0; i < 300; i += 3) + assert(!memcmp(clip_data + i, "ABC", 3)); + + reset_captures(); + write_seq(t, "\033]52;c;\007"); + assert(clip_count == 1 && clip_len == 0); + + reset_captures(); + write_seq(t, "\033]52;c;?\007"); + write_seq(t, "\033]52;c;!!!invalid!!!\007"); + write_seq(t, "\033]52;aGVsbG8=\007"); + write_seq(t, "\033]52;;aGVsbG8=\007"); + assert(clip_count == 0); + + write_seq(t, "\033]52;pc;aGVsbG8=\007"); + assert(clip_count == 1 && !strcmp(clip_sel, "pc")); + assert(clip_len == 5 && !memcmp(clip_data, "hello", 5)); + + reset_captures(); + write_seq(t, "\033]52;s;AAH9\007"); + assert(clip_count == 1 && !strcmp(clip_sel, "s") && clip_len == 3); + assert(clip_data[0] == 0 && clip_data[1] == 1 && clip_data[2] == (char)0xfd); + term_free(t); +} + +/* An SGR reset must not drop an open link. + * + * The fast SGR path caches the cell flags, and those flags carry the link id. + * The reset used to set the cached value to zero, which cleared the link from + * every cell written after it. Write the whole sequence at once so the fast + * path runs. */ +static void test_sgr_reset_keeps_link(void) +{ + struct term *t = osc_test_term(); + reset_captures(); + + static const char seq[] = "\033]8;;https://reset.dev\033\\\033[0mX"; + write_buf(t, seq, sizeof(seq) - 1); + assert(tt_cell(t, 0, 0)->attrs & TERM_ATTR_LINK); + assert_link(t, 0, 0, "https://reset.dev"); + + /* The same reset in its own write, which takes the normal parser. */ + static const char seq2[] = "\033]8;;https://second.dev\033\\\033[0m"; + write_buf(t, seq2, sizeof(seq2) - 1); + write_buf(t, "Y", 1); + assert(tt_cell(t, 1, 0)->attrs & TERM_ATTR_LINK); + assert_link(t, 1, 0, "https://second.dev"); + + term_free(t); +} + +static void test_osc8(void) +{ + struct term *t = osc_test_term(); + reset_captures(); + + write_seq(t, "\033]8;;https://example.com\033\\link text\033]8;;\033\\after"); + for (int c = 0; c < 9; c++) { + assert(tt_cell(t, c, 0)->attrs & TERM_ATTR_LINK); + assert_link(t, c, 0, "https://example.com"); + } + for (int c = 9; c < 14; c++) { + assert(!(tt_cell(t, c, 0)->attrs & TERM_ATTR_LINK)); + assert_link(t, c, 0, NULL); + } + + /* Each cell keeps the URI of its own link. */ + write_seq(t, "\033[2;1H\033]8;;https://other.org\033\\bbb\033]8;;\033\\"); + write_seq(t, "\033[3;1H\033]8;;https://example.com\033\\ccc\033]8;;\033\\"); + assert(tt_cell(t, 0, 1)->attrs & TERM_ATTR_LINK); + assert(tt_cell(t, 0, 2)->attrs & TERM_ATTR_LINK); + assert_link(t, 0, 1, "https://other.org"); + assert_link(t, 2, 1, "https://other.org"); + assert_link(t, 0, 2, "https://example.com"); + assert_link(t, 0, 0, "https://example.com"); + + /* An erase removes the link. */ + write_seq(t, "\033[4;1H\033]8;;https://gone.io\033\\zzz\033]8;;\033\\"); + assert(tt_cell(t, 0, 3)->attrs & TERM_ATTR_LINK); + write_seq(t, "\033[4;1H\033[K"); + assert(!(tt_cell(t, 0, 3)->attrs & TERM_ATTR_LINK)); + assert_link(t, 0, 3, NULL); + + write_seq(t, "\033[5;1H\033]8;x\033\\nop\033]8;xyz\033\\qq"); + assert(!(tt_cell(t, 0, 4)->attrs & TERM_ATTR_LINK)); + assert_link(t, 0, 4, NULL); + assert(!(tt_cell(t, 4, 4)->attrs & TERM_ATTR_LINK)); + write_seq(t, "\033]8;;https://stay.net\033\\\033]8;bogus\033\\ST\033]8;;\033\\"); + assert(tt_cell(t, 5, 4)->attrs & TERM_ATTR_LINK); + assert_link(t, 5, 4, "https://stay.net"); + + write_seq(t, "\033[6;1H\033]8;id=grp;https://params.dev\007p1\033]8;;\007"); + assert(tt_cell(t, 0, 5)->attrs & TERM_ATTR_LINK); + assert_link(t, 0, 5, "https://params.dev"); + + /* A link stays on its cells when they scroll into the history. */ + for (int i = 0; i < 30; i++) + write_buf(t, "filler\r\n", 8); + assert(tt_history(t) > 0); + term_scroll(t, tt_history(t)); + assert(tt_view_cell(t, 0, 0)->attrs & TERM_ATTR_LINK); + assert_link(t, 0, 0, "https://example.com"); + term_scroll_to(t, 0); + + write_seq(t, "\033[7;1H"); + for (int i = 0; i < 8300; i++) { + char seq[64]; + int n = snprintf(seq, sizeof(seq), + "\033]8;;https://h/%d\033\\A\010", i); + write_buf(t, seq, (size_t)n); + } + assert_link(t, 0, 6, "https://h/8299"); + assert_link(t, -1, 0, NULL); + assert_link(t, 0, -1, NULL); + assert_link(t, 80, 0, NULL); + assert_link(t, 0, 24, NULL); + + write_seq(t, "\033[?1049h"); + write_seq(t, "\033[1;1H\033]8;;https://altscreen.test\033\\ALT\033]8;;\033\\"); + assert_link(t, 0, 0, "https://altscreen.test"); + write_seq(t, "\033[?1049l"); + term_scroll(t, tt_history(t)); + assert_link(t, 0, 0, "https://example.com"); + term_scroll_to(t, 0); + write_seq(t, "\033[8;1HSee file:///home/user/code/file.c#L10-L20 for info"); + assert_link(t, 10, 7, "file:///home/user/code/file.c#L10-L20"); + + term_free(t); +} + +static bool text_is(size_t (*get)(const struct term *, char *, size_t), + const struct term *t, const char *want) +{ + char buf[256]; + return get(t, buf, sizeof(buf)) == strlen(want) && !strcmp(buf, want); +} + +static void test_osc_misc(void) +{ + struct term *t = osc_test_term(); + reset_captures(); + + write_seq(t, "\033]7;file://host/home/user/proj\007"); + assert(text_is(term_cwd, t, "/home/user/proj")); + + write_seq(t, "\033]7;file:///a%20b\033\\"); + assert(text_is(term_cwd, t, "/a b")); + + const char *bulk = "\033]7;file://h/bulk%20dir\007"; + write_buf(t, bulk, strlen(bulk)); + assert(text_is(term_cwd, t, "/bulk dir")); + + write_seq(t, "\033]7;https://x/y\007"); + write_seq(t, "\033]7;notauri\007"); + assert(text_is(term_cwd, t, "/bulk dir")); + + write_seq(t, "\033]9;hello there\033\\"); + assert(notify_count == 1); + assert(!strcmp(notify_title, "wsap_term")); + assert(!strcmp(notify_body, "hello there")); + write_seq(t, "\033]9;4;3;50\007"); + assert(notify_count == 1); + + write_seq(t, "\033]777;notify;Title;Body text\007"); + assert(notify_count == 2); + assert(!strcmp(notify_title, "Title")); + assert(!strcmp(notify_body, "Body text")); + write_seq(t, "\033]777;other;T;B\007"); + assert(notify_count == 2); + + write_seq(t, "\033]22;pointer\033\\"); + assert(text_is(term_pointer_shape, t, "pointer")); + write_seq(t, "\033]22;text\007"); + assert(text_is(term_pointer_shape, t, "text")); + write_seq(t, "\033]22;\033\\"); + assert(!term_pointer_shape(t, NULL, 0)); + + term_free(t); +} + +/* OSC 4, 10, 11, and 12 change the colors of one terminal. The reset codes + * restore the colors of its theme. */ +static void test_osc_colors(void) +{ + struct term_theme theme; + term_theme_default(&theme); + struct term *t = osc_test_term(); + struct term *other = osc_test_term(); + reset_captures(); + + /* tt_new starts with the built-in theme. */ + assert(tt_color(t, TERM_COLOR_FG) == 0xFFFFFFFFu && theme.fg == 0xFFFFFFFFu); + assert(tt_color(t, TERM_COLOR_BG) == 0xFF1E1E2Eu && theme.bg == 0xFF1E1E2Eu); + assert(cursor_color(t) == 0xFF89B4FAu && theme.cursor == 0xFF89B4FAu); + assert_query(t, "\033]10;?\007", "\033]10;rgb:ffff/ffff/ffff\033\\"); + + write_seq(t, "\033]10;#ff0000\007"); + assert(tt_color(t, TERM_COLOR_FG) == 0xFFFF0000u); + assert_query(t, "\033]10;?\007", "\033]10;rgb:ffff/0000/0000\033\\"); + write_seq(t, "\033]110\007"); + assert(tt_color(t, TERM_COLOR_FG) == theme.fg); + assert_query(t, "\033]10;?\033\\", "\033]10;rgb:ffff/ffff/ffff\033\\"); + + write_seq(t, "\033]11;#00ff00\033\\"); + assert(tt_color(t, TERM_COLOR_BG) == 0xFF00FF00u); + assert_query(t, "\033]11;?\007", "\033]11;rgb:0000/ffff/0000\033\\"); + write_seq(t, "\033]111\007"); + assert(tt_color(t, TERM_COLOR_BG) == theme.bg); + assert_query(t, "\033]11;?\007", "\033]11;rgb:1e1e/1e1e/2e2e\033\\"); + + write_seq(t, "\033]12;#0000ff\007"); + assert(cursor_color(t) == 0xFF0000FFu); + assert_query(t, "\033]12;?\007", "\033]12;rgb:0000/0000/ffff\033\\"); + write_seq(t, "\033]112\033\\"); + assert(cursor_color(t) == theme.cursor); + assert_query(t, "\033]12;?\007", "\033]12;rgb:8989/b4b4/fafa\033\\"); + + uint32_t ansi5 = tt_color(t, term_color_index(5)); + assert(ansi5 == theme.ansi[5]); + write_seq(t, "\033]4;5;#ff0000\007"); + assert(tt_color(t, term_color_index(5)) == 0xFFFF0000u); + assert(tt_color(t, term_color_index(4)) == theme.ansi[4]); + + /* A color change in one terminal does not change another terminal. */ + write_seq(t, "\033]10;#123456\007"); + assert(tt_color(other, term_color_index(5)) == theme.ansi[5]); + assert(tt_color(other, TERM_COLOR_FG) == theme.fg); + assert_query(other, "\033]10;?\007", "\033]10;rgb:ffff/ffff/ffff\033\\"); + assert(tt_color(t, TERM_COLOR_FG) == 0xFF123456u); + + write_seq(t, "\033]104;5\007"); + assert(tt_color(t, term_color_index(5)) == ansi5); + write_seq(t, "\033]4;1;#010203;2;#040506\007"); + assert(tt_color(t, term_color_index(1)) == 0xFF010203u); + assert(tt_color(t, term_color_index(2)) == 0xFF040506u); + write_seq(t, "\033]104\007"); + assert(tt_color(t, term_color_index(1)) == theme.ansi[1]); + assert(tt_color(t, term_color_index(2)) == theme.ansi[2]); + + term_free(other); + term_free(t); +} + +static void test_osc_legacy_titles(void) +{ + struct term *t = osc_test_term(); + reset_captures(); + + write_seq(t, "\033]legacy title\007"); + assert(!strcmp(title, "legacy title")); + write_seq(t, "\033]\007"); + assert(!strcmp(title, "")); + write_seq(t, "\033]2adjacent\033\\"); + assert(!strcmp(title, "adjacent")); + + term_free(t); +} + +int main(void) +{ + test_osc52(); + test_osc8(); + test_sgr_reset_keeps_link(); + test_osc_misc(); + test_osc_colors(); + test_osc_legacy_titles(); + puts("PASS: OSC tests"); + return 0; +} |
