summaryrefslogtreecommitdiff
path: root/tests/osc_test.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/osc_test.c')
-rw-r--r--tests/osc_test.c400
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;
+}