summaryrefslogtreecommitdiff
path: root/src/osc
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src/osc
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 'src/osc')
-rw-r--r--src/osc/clipboard.c76
-rw-r--r--src/osc/dispatch.c227
-rw-r--r--src/osc/link.c260
-rw-r--r--src/osc/meson.build6
-rw-r--r--src/osc/misc.c69
-rw-r--r--src/osc/osc.h30
6 files changed, 668 insertions, 0 deletions
diff --git a/src/osc/clipboard.c b/src/osc/clipboard.c
new file mode 100644
index 0000000..a5ac59d
--- /dev/null
+++ b/src/osc/clipboard.c
@@ -0,0 +1,76 @@
+#include "osc.h"
+#include "../internal.h"
+#include <stdlib.h>
+#include <string.h>
+
+static int b64_val(char c)
+{
+ if (c >= 'A' && c <= 'Z') return c - 'A';
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
+ if (c >= '0' && c <= '9') return c - '0' + 52;
+ if (c == '+') return 62;
+ if (c == '/') return 63;
+ return -1;
+}
+
+static bool b64_decode(const char *s, size_t n, char *out, size_t *out_len)
+{
+ size_t pad = 0;
+ while (pad < n && s[n - pad - 1] == '=')
+ pad++;
+ n -= pad;
+ if (pad > 2 || (pad && n % 4 + pad != 4))
+ return false;
+ size_t rem = n % 4;
+ if (rem == 1)
+ return false;
+ size_t i = 0, o = 0;
+ for (; i + 4 <= n; i += 4) {
+ int a = b64_val(s[i]), b = b64_val(s[i + 1]);
+ int c = b64_val(s[i + 2]), d = b64_val(s[i + 3]);
+ if ((a | b | c | d) < 0)
+ return false;
+ out[o++] = (char)(a << 2 | b >> 4);
+ out[o++] = (char)(b << 4 | c >> 2);
+ out[o++] = (char)(c << 6 | d);
+ }
+ if (rem >= 2) {
+ int a = b64_val(s[i]), b = b64_val(s[i + 1]);
+ if ((a | b) < 0)
+ return false;
+ out[o++] = (char)(a << 2 | b >> 4);
+ if (rem == 3) {
+ int c = b64_val(s[i + 2]);
+ if (c < 0)
+ return false;
+ out[o++] = (char)(b << 4 | c >> 2);
+ }
+ }
+ *out_len = o;
+ return true;
+}
+
+void term_osc52(struct term *t, const char *params)
+{
+ const char *sep = strchr(params, ';');
+ if (!sep || sep == params)
+ return;
+ const char *pd = sep + 1;
+ if (!strcmp(pd, "?") || !t->cb.clipboard)
+ return;
+ size_t sel_len = (size_t)(sep - params);
+ char sel[64];
+ if (sel_len >= sizeof(sel))
+ return;
+ memcpy(sel, params, sel_len);
+ sel[sel_len] = '\0';
+ if (!*pd) {
+ t->cb.clipboard(t->cb.ud, sel, "", 0);
+ return;
+ }
+ size_t enc_len = strlen(pd);
+ char *dec = (char *)pd;
+ size_t dec_len;
+ if (b64_decode(pd, enc_len, dec, &dec_len))
+ t->cb.clipboard(t->cb.ud, sel, dec, dec_len);
+}
diff --git a/src/osc/dispatch.c b/src/osc/dispatch.c
new file mode 100644
index 0000000..4cf986e
--- /dev/null
+++ b/src/osc/dispatch.c
@@ -0,0 +1,227 @@
+#include "osc.h"
+#include "../internal.h"
+#include "../palette.h"
+#include "../screen.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define OSC_HEAP_INIT 4096
+#define OSC_HEAP_KEEP (1u << 20)
+
+size_t term_osc_room(const struct term *t)
+{
+ size_t cap = t->osc_heap ? t->osc_heap_cap : sizeof(t->osc_buf);
+ return cap - 1 - (size_t)t->osc_len;
+}
+
+char *term_osc_tail(struct term *t)
+{
+ return (t->osc_heap ? t->osc_heap : t->osc_buf) + t->osc_len;
+}
+
+void term_osc_commit(struct term *t, size_t n)
+{
+ t->osc_len += (int)n;
+}
+
+char *term_osc_data(struct term *t)
+{
+ return t->osc_heap ? t->osc_heap : t->osc_buf;
+}
+
+bool term_osc_grow(struct term *t)
+{
+ if (!t->osc_heap) {
+ char *p = malloc(OSC_HEAP_INIT);
+ if (!p) return false;
+ memcpy(p, t->osc_buf, (size_t)t->osc_len);
+ t->osc_heap = p;
+ t->osc_heap_cap = OSC_HEAP_INIT;
+ return true;
+ }
+ if (t->osc_heap_cap >= TERM_OSC_MAX) return false;
+ size_t cap = t->osc_heap_cap * 2;
+ if (cap > TERM_OSC_MAX) cap = TERM_OSC_MAX;
+ char *p = realloc(t->osc_heap, cap);
+ if (!p) return false;
+ t->osc_heap = p;
+ t->osc_heap_cap = cap;
+ return true;
+}
+
+void term_osc_cleanup(struct term *t)
+{
+ free(t->osc_heap);
+ t->osc_heap = NULL;
+ t->osc_heap_cap = 0;
+ free(t->dcs_buf);
+ t->dcs_buf = NULL;
+ t->dcs_cap = 0;
+ term_osc8_cleanup(t);
+ free(t->cwd);
+ t->cwd = NULL;
+ free(t->pointer_shape);
+ t->pointer_shape = NULL;
+}
+
+/* Reply to a color query. An OSC 4 reply names the index it answers for;
+ * index is negative for OSC 10, 11, and 12. */
+static void send_color_query_reply(struct term *t, int code, int index, uint32_t argb)
+{
+ char resp[64], prefix[16] = "";
+ uint8_t r = (argb >> 16) & 0xff, g = (argb >> 8) & 0xff, b = argb & 0xff;
+ if (index >= 0)
+ snprintf(prefix, sizeof(prefix), "%d;", index);
+ snprintf(resp, sizeof(resp),
+ "\x1b]%d;%srgb:%02x%02x/%02x%02x/%02x%02x\x1b\\",
+ code, prefix, r, r, g, g, b, b);
+ send_response(t, resp);
+}
+
+static void osc_color_set_or_query(struct term *t, int code, const char *params, int slot)
+{
+ if (!strcmp(params, "?")) {
+ send_color_query_reply(t, code, -1, t->palette.colors[slot]);
+ return;
+ }
+ uint32_t argb;
+ if (term_color_parse(params, &argb))
+ term_palette_set(&t->palette, slot, argb);
+}
+
+static long osc_next_index(const char **pp)
+{
+ char *end;
+ long idx = strtol(*pp, &end, 10);
+ if (end == *pp || (*end != ';' && *end != '\0'))
+ return -1;
+ *pp = end;
+ if (**pp == ';') (*pp)++;
+ return idx;
+}
+
+static void osc4_handle(struct term *t, const char *params)
+{
+ const char *p = params;
+ while (*p) {
+ long idx = osc_next_index(&p);
+ if (idx < 0)
+ return;
+
+ const char *spec = p;
+ const char *spec_end = spec;
+ while (*spec_end && *spec_end != ';')
+ spec_end++;
+ size_t spec_len = (size_t)(spec_end - spec);
+ if (spec_len == 1 && spec[0] == '?') {
+ if (idx <= 255)
+ send_color_query_reply(t, 4, (int)idx, t->palette.colors[idx]);
+ } else {
+ char buf[128];
+ if (idx <= 255 && spec_len < sizeof(buf)) {
+ memcpy(buf, spec, spec_len);
+ buf[spec_len] = '\0';
+ uint32_t argb;
+ if (term_color_parse(buf, &argb))
+ term_palette_set(&t->palette, (int)idx, argb);
+ }
+ }
+ p = spec_end;
+ if (*p == ';') p++;
+ }
+}
+
+static void osc104_handle(struct term *t, const char *params)
+{
+ if (!*params) {
+ for (int i = 0; i < 256; i++)
+ term_palette_reset(&t->palette, i);
+ return;
+ }
+ const char *p = params;
+ while (*p) {
+ long idx = osc_next_index(&p);
+ if (idx < 0)
+ return;
+ if (idx <= 255)
+ term_palette_reset(&t->palette, (int)idx);
+ }
+}
+
+void term_osc_finish(struct term *t)
+{
+ if (!term_str_was_truncated(t)) {
+ char *buf = term_osc_data(t);
+ buf[t->osc_len] = '\0';
+
+ if (!t->alternate && t->osc_len >= 5 && !strncmp(buf, "133;", 4) &&
+ (buf[5] == ';' || buf[5] == '\0')) {
+ if (buf[4] == 'A') {
+ t->prompt_y = t->cursor_y;
+ t->has_prompt = true;
+ }
+ if (buf[4] == 'C' || buf[4] == 'D') {
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ }
+ }
+
+ char *rest;
+ long code = strtol(buf, &rest, 10);
+ bool no_code = rest == buf;
+ bool legacy_title = no_code && !strchr(buf, ';');
+ if (no_code) {
+ code = legacy_title || *buf == ';' ? 0 : -1;
+ rest = buf;
+ }
+ if (code >= 0 && (legacy_title || *rest == ';' || *rest == '\0' || code <= 2)) {
+ const char *params = *rest == ';' ? rest + 1 : rest;
+ switch (code) {
+ case 0: case 1: case 2:
+ if (t->cb.title)
+ t->cb.title(t->cb.ud, params);
+ break;
+ case 4:
+ osc4_handle(t, params);
+ break;
+ case 8:
+ term_osc8(t, params);
+ break;
+ case 10:
+ osc_color_set_or_query(t, 10, params, TERM_PALETTE_FG);
+ break;
+ case 11:
+ osc_color_set_or_query(t, 11, params, TERM_PALETTE_BG);
+ break;
+ case 12:
+ osc_color_set_or_query(t, 12, params, TERM_PALETTE_CURSOR);
+ break;
+ case 52:
+ term_osc52(t, params);
+ break;
+ case 104:
+ osc104_handle(t, params);
+ break;
+ case 110:
+ term_palette_reset(&t->palette, TERM_PALETTE_FG);
+ break;
+ case 111:
+ term_palette_reset(&t->palette, TERM_PALETTE_BG);
+ break;
+ case 112:
+ term_palette_reset(&t->palette, TERM_PALETTE_CURSOR);
+ break;
+ default:
+ term_osc_misc_dispatch(t, code, params);
+ break;
+ }
+ }
+ }
+ t->osc_len = 0;
+ if (t->osc_heap && t->osc_heap_cap > OSC_HEAP_KEEP) {
+ free(t->osc_heap);
+ t->osc_heap = NULL;
+ t->osc_heap_cap = 0;
+ }
+}
diff --git a/src/osc/link.c b/src/osc/link.c
new file mode 100644
index 0000000..f31a750
--- /dev/null
+++ b/src/osc/link.c
@@ -0,0 +1,260 @@
+#include "osc.h"
+#include "../internal.h"
+#include "../screen.h"
+#include <stdlib.h>
+#include <string.h>
+
+#define TERM_LINK_MAX 8192
+#define TERM_LINK_URI_MAX 4096
+
+struct link_hash_slot {
+ uint64_t hash;
+ uint16_t id;
+};
+
+struct term_links {
+ char **uri;
+ uint16_t *free_ids;
+ struct link_hash_slot *hash_table;
+ uint32_t hash_cap;
+ uint16_t capacity;
+ uint16_t free_count;
+ uint16_t next;
+};
+
+static inline uint64_t hash_uri(const char *s)
+{
+ uint64_t h = HASH64_BASIS;
+ while (*s)
+ h = hash64_step(h, (uint8_t)*s++);
+ h = hash64_finish(h);
+ return h ? h : 1;
+}
+
+static void links_insert_hash(struct term_links *l, uint64_t h, uint16_t id)
+{
+ if (!l->hash_table || !l->hash_cap)
+ return;
+ uint32_t mask = l->hash_cap - 1;
+ uint32_t idx = (uint32_t)h & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ struct link_hash_slot *slot = &l->hash_table[idx];
+ if (slot->hash == 0) {
+ slot->hash = h;
+ slot->id = id;
+ return;
+ }
+ idx = (idx + 1) & mask;
+ }
+}
+
+static bool links_grow(struct term_links *l)
+{
+ uint16_t new_cap = l->capacity == 0 ? 64 : l->capacity < 4096 ? l->capacity * 2 : TERM_LINK_MAX;
+ if (new_cap <= l->capacity)
+ return false;
+ char **new_uri = realloc(l->uri, sizeof(char *) * new_cap);
+ if (!new_uri) return false;
+ uint16_t *new_free = malloc(sizeof(uint16_t) * new_cap);
+ if (!new_free) {
+ l->uri = new_uri;
+ return false;
+ }
+ uint32_t new_hash_cap = (uint32_t)new_cap * 2;
+ struct link_hash_slot *new_hash = calloc(new_hash_cap, sizeof(*new_hash));
+ if (!new_hash) {
+ free(new_free);
+ l->uri = new_uri;
+ return false;
+ }
+ uint16_t *old_free = l->free_ids;
+ l->free_ids = new_free;
+ if (l->capacity)
+ memcpy(new_free, old_free, sizeof(uint16_t) * l->capacity);
+ free(old_free);
+ memset(new_uri + l->capacity, 0, sizeof(char *) * (new_cap - l->capacity));
+ l->uri = new_uri;
+ l->capacity = new_cap;
+
+ free(l->hash_table);
+ l->hash_table = new_hash;
+ l->hash_cap = new_hash_cap;
+
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i])
+ links_insert_hash(l, hash_uri(l->uri[i]), (uint16_t)(i + 1));
+ }
+ return true;
+}
+
+static void links_mark_row(const struct cell *row, int len, uint64_t *mark, uint16_t cap)
+{
+ if (len > TERM_MAX_COLS) len = TERM_MAX_COLS;
+ for (int i = 0; i < len; i++) {
+ uint16_t id = row[i].pad;
+ if (id && id <= cap)
+ mark[(id - 1) >> 6] |= 1ull << ((id - 1) & 63);
+ }
+}
+
+static void links_mark_id(uint16_t id, uint64_t *mark, uint16_t cap)
+{
+ if (id && id <= cap)
+ mark[(id - 1) >> 6] |= 1ull << ((id - 1) & 63);
+}
+
+struct links_mark {
+ uint64_t *mark;
+ uint16_t cap;
+};
+
+static void links_mark_hist(void *ud, uint16_t id)
+{
+ struct links_mark *m = ud;
+ links_mark_id(id, m->mark, m->cap);
+}
+
+static void links_mark_screen(const struct screen *s, struct links_mark *m)
+{
+ for (int y = 0; y < s->rows; y++) {
+ const struct srow *row = screen_get_row(s, y);
+ if (row->len == 0)
+ continue;
+ if (row->kind == ROW_CELLS)
+ links_mark_row(row->cells, row->len, m->mark, m->cap);
+ else
+ term_hist_text_mark_links(&row->text, links_mark_hist, m);
+ }
+}
+
+static void links_gc(struct term *t, struct term_links *l)
+{
+ uint64_t mark[TERM_LINK_MAX / 64] = { 0 };
+ struct links_mark m = { mark, l->capacity };
+ links_mark_id(t->cur_link, mark, l->capacity);
+ links_mark_screen(&t->scr, &m);
+ if (t->alternate && t->alt_allocated)
+ links_mark_screen(&t->alt_scr, &m);
+ term_hist_mark_links(&t->hist, links_mark_hist, &m);
+ l->free_count = 0;
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i] && !(mark[i >> 6] & (1ull << (i & 63)))) {
+ free(l->uri[i]);
+ l->uri[i] = NULL;
+ }
+ if (!l->uri[i])
+ l->free_ids[l->free_count++] = (uint16_t)(i + 1);
+ }
+ if (l->hash_table && l->hash_cap) {
+ memset(l->hash_table, 0, sizeof(*l->hash_table) * l->hash_cap);
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i])
+ links_insert_hash(l, hash_uri(l->uri[i]), (uint16_t)(i + 1));
+ }
+ }
+}
+
+static uint16_t links_find(const struct term_links *l, const char *uri, uint64_t h)
+{
+ if (!l->hash_table || !l->hash_cap)
+ return 0;
+ uint32_t mask = l->hash_cap - 1;
+ uint32_t idx = (uint32_t)h & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ const struct link_hash_slot *slot = &l->hash_table[idx];
+ if (slot->hash == 0)
+ return 0;
+ if (slot->hash == h) {
+ uint16_t id = slot->id;
+ if (id && id <= l->capacity && l->uri[id - 1] && !strcmp(l->uri[id - 1], uri))
+ return id;
+ }
+ idx = (idx + 1) & mask;
+ }
+ return 0;
+}
+
+static uint16_t links_alloc(struct term *t, struct term_links *l)
+{
+ if (l->free_count)
+ return l->free_ids[--l->free_count];
+ if (l->next <= l->capacity || links_grow(l))
+ return l->next++;
+ links_gc(t, l);
+ if (l->free_count)
+ return l->free_ids[--l->free_count];
+ return 0;
+}
+
+void term_osc8(struct term *t, const char *params)
+{
+ const char *sep = strchr(params, ';');
+ if (!sep)
+ return;
+ const char *uri = sep + 1;
+ if (!*uri) {
+ t->cur_link = 0;
+ return;
+ }
+ if (strlen(uri) > TERM_LINK_URI_MAX) {
+ t->cur_link = 0;
+ return;
+ }
+ struct term_links *l = t->links;
+ if (!l) {
+ l = calloc(1, sizeof(*l));
+ if (!l) {
+ t->cur_link = 0;
+ return;
+ }
+ l->next = 1;
+ t->links = l;
+ }
+ uint64_t h = hash_uri(uri);
+ uint16_t id = links_find(l, uri, h);
+ if (!id) {
+ id = links_alloc(t, l);
+ if (!id) {
+ t->cur_link = 0;
+ return;
+ }
+ l->uri[id - 1] = term_strdup(uri);
+ if (!l->uri[id - 1]) {
+ l->free_ids[l->free_count++] = id;
+ t->cur_link = 0;
+ return;
+ }
+ links_insert_hash(l, h, id);
+ }
+ t->cur_link = id;
+}
+
+const char *term_osc8_url_at(const struct term *t, int col, int row)
+{
+ const struct term_links *l = t->links;
+ if (!l || col < 0 || col >= t->cols)
+ return NULL;
+ const struct cell *cells = term_screen_view_row(t, row);
+ if (!cells)
+ return NULL;
+ uint16_t id = cells[col].pad;
+ if (!id || id > l->capacity ||
+ !(cells[col].flags & CELL_LINK))
+ return NULL;
+ return l->uri[id - 1];
+}
+
+void term_osc8_cleanup(struct term *t)
+{
+ struct term_links *l = t->links;
+ if (!l)
+ return;
+ for (uint16_t i = 0; i < l->capacity; i++)
+ free(l->uri[i]);
+ free(l->uri);
+ free(l->free_ids);
+ free(l->hash_table);
+ free(l);
+ t->links = NULL;
+ t->cur_link = 0;
+}
diff --git a/src/osc/meson.build b/src/osc/meson.build
new file mode 100644
index 0000000..a3b9efa
--- /dev/null
+++ b/src/osc/meson.build
@@ -0,0 +1,6 @@
+term_osc_src = files(
+ 'clipboard.c',
+ 'dispatch.c',
+ 'link.c',
+ 'misc.c',
+)
diff --git a/src/osc/misc.c b/src/osc/misc.c
new file mode 100644
index 0000000..5d6b506
--- /dev/null
+++ b/src/osc/misc.c
@@ -0,0 +1,69 @@
+#include "osc.h"
+#include "../internal.h"
+#include <stdlib.h>
+#include <string.h>
+
+static void osc7_cwd(struct term *t, const char *uri)
+{
+ if (!term_has_prefix_ci(uri, "file://"))
+ return;
+ const char *p = uri + 7;
+ const char *slash = strchr(p, '/');
+ const char *path = slash ? slash : p;
+ char *cwd = malloc(strlen(path) + 1);
+ if (!cwd)
+ return;
+ char *d = cwd;
+ while (*path) {
+ int hi, lo;
+ if (*path == '%' && (hi = term_hex_nib(path[1])) >= 0 &&
+ (lo = term_hex_nib(path[2])) >= 0) {
+ *d++ = (char)(hi * 16 + lo);
+ path += 3;
+ } else {
+ *d++ = *path++;
+ }
+ }
+ *d = '\0';
+ free(t->cwd);
+ t->cwd = cwd;
+}
+
+static void osc777_notify(struct term *t, const char *params)
+{
+ if (strncmp(params, "notify;", 7))
+ return;
+ const char *title = params + 7;
+ const char *sep = strchr(title, ';');
+ if (!sep)
+ return;
+ char *wsep = (char *)sep;
+ *wsep = '\0';
+ t->cb.notify(t->cb.ud, title, sep + 1);
+ *wsep = ';';
+}
+
+void term_osc_misc_dispatch(struct term *t, long code, const char *params)
+{
+ if (!params)
+ return;
+ switch (code) {
+ case 7:
+ osc7_cwd(t, params);
+ break;
+ case 9:
+ if (t->cb.notify && strncmp(params, "4;", 2))
+ t->cb.notify(t->cb.ud, "wsap_term", params);
+ break;
+ case 22:
+ free(t->pointer_shape);
+ t->pointer_shape = *params ? term_strdup(params) : NULL;
+ break;
+ case 777:
+ if (t->cb.notify)
+ osc777_notify(t, params);
+ break;
+ default:
+ break;
+ }
+}
diff --git a/src/osc/osc.h b/src/osc/osc.h
new file mode 100644
index 0000000..1398534
--- /dev/null
+++ b/src/osc/osc.h
@@ -0,0 +1,30 @@
+#ifndef TERM_OSC_H
+#define TERM_OSC_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct term;
+
+#define TERM_OSC_MAX (16u << 20)
+
+size_t term_osc_room(const struct term *t);
+char *term_osc_tail(struct term *t);
+void term_osc_commit(struct term *t, size_t n);
+bool term_osc_grow(struct term *t);
+
+char *term_osc_data(struct term *t);
+
+void term_osc_finish(struct term *t);
+
+void term_osc_cleanup(struct term *t);
+
+void term_osc8(struct term *t, const char *params);
+void term_osc52(struct term *t, const char *params);
+void term_osc_misc_dispatch(struct term *t, long code, const char *params);
+
+const char *term_osc8_url_at(const struct term *t, int col, int row);
+void term_osc8_cleanup(struct term *t);
+
+#endif