aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
committerDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
commitda28548905dab7c60c3fcec4975ccfa23e315909 (patch)
tree9cef34a718563969a6bcda5cfeff033eeaf6b1a0 /lib
0.99.0
Diffstat (limited to 'lib')
-rw-r--r--lib/arg.h77
-rw-r--r--lib/bitset.h152
-rw-r--r--lib/date.h139
-rw-r--r--lib/readline.h529
-rw-r--r--lib/str.h183
-rw-r--r--lib/term.h206
-rw-r--r--lib/term_key.h292
-rw-r--r--lib/utf8.h185
-rw-r--r--lib/util.h345
-rw-r--r--lib/vt.h177
10 files changed, 2285 insertions, 0 deletions
diff --git a/lib/arg.h b/lib/arg.h
new file mode 100644
index 0000000..06d3024
--- /dev/null
+++ b/lib/arg.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_ARG
+#define DYLAN_ARG
+
+struct argv {
+ const char *const *argv;
+ int c;
+};
+
+struct arg {
+ const char *pos;
+ int sign;
+ int name;
+};
+
+static inline struct argv
+arg_init(int argc, char *a[])
+{
+ (void) argc;
+ return (struct argv) { .argv = (const char *const *) ++a };
+}
+
+static inline struct arg
+arg_next(struct argv *s)
+{
+ static const signed char t[256] = {
+ ['-'] = '-' - 1, ['+'] = '+' - 1,
+ };
+
+ struct arg a = {0};
+ a.pos = *s->argv;
+ a.sign = a.pos ? t[(unsigned char) **s->argv] + 1 : -1;
+
+ if (a.sign > 1) {
+ s->c |= !s->c;
+ a.name = (unsigned char) (a.pos[s->c] ? a.pos[s->c] : a.sign);
+ s->c += !!a.pos[s->c];
+ s->c *= !!a.pos[s->c];
+ }
+
+ s->argv += !s->c;
+ return a;
+}
+
+static inline const char *
+arg_next_positional(struct argv *s)
+{
+ const char *a = *s->argv;
+ if (!a) return a;
+ a += s->c;
+ s->c = 0;
+ s->argv++;
+ return a;
+}
+
+#endif // DYLAN_ARG
+
diff --git a/lib/bitset.h b/lib/bitset.h
new file mode 100644
index 0000000..d8979de
--- /dev/null
+++ b/lib/bitset.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_BITSET_H
+#define DYLAN_BITSET_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "util.h"
+
+enum {
+ BITSET_WORD_BITS = 64,
+ BITSET_WORD_SHIFT = 6,
+ BITSET_WORD_MASK = BITSET_WORD_BITS - 1,
+};
+
+#define BITSET_W(n) (((n) + BITSET_WORD_MASK) >> BITSET_WORD_SHIFT)
+
+static inline u8
+bitset_get(const u64 *b, usize i)
+{
+ return (b[i >> BITSET_WORD_SHIFT] >> (i & BITSET_WORD_MASK)) & 1ull;
+}
+
+static inline void
+bitset_set(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] |= 1ull << (i & BITSET_WORD_MASK);
+}
+
+static inline void
+bitset_clr(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] &= ~(1ull << (i & BITSET_WORD_MASK));
+}
+
+static inline void
+bitset_tog(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] ^= 1ull << (i & BITSET_WORD_MASK);
+}
+
+static inline void
+bitset_assign(u64 *b, usize i, int v)
+{
+ u64 *w = &b[i >> BITSET_WORD_SHIFT];
+ u64 m = 1ull << (i & BITSET_WORD_MASK);
+ *w = v ? (*w | m) : (*w & ~m);
+}
+
+static inline usize
+bitset_count(const u64 *b, usize l)
+{
+ usize c = 0;
+ for (usize i = 0, w = BITSET_W(l); i < w; i++)
+ c += u64_popcount(b[i]);
+ return c;
+}
+
+static inline void
+bitset_set_all(u64 *v, usize n)
+{
+ memset(v, 0xff, BITSET_W(n) * sizeof *v);
+}
+
+static inline void
+bitset_clr_all(u64 *v, usize n)
+{
+ memset(v, 0, BITSET_W(n) * sizeof *v);
+}
+
+static inline void
+bitset_invert(u64 *v, usize n)
+{
+ usize w = BITSET_W(n);
+ for (usize i = 0; i < w; i++) v[i] = ~v[i];
+ usize r = n & BITSET_WORD_MASK;
+ if (r) v[w - 1] &= (1ull << r) - 1;
+}
+
+static inline void
+bitset_swap(u64 *b, usize i, usize j)
+{
+ u8 bi = bitset_get(b, i);
+ u8 bj = bitset_get(b, j);
+ bitset_assign(b, i, bj);
+ bitset_assign(b, j, bi);
+}
+
+static inline usize
+bitset_next_set(const u64 *b, usize i, usize n)
+{
+ if (i >= n) return SIZE_MAX;
+ usize wi = i >> BITSET_WORD_SHIFT;
+ usize wN = BITSET_W(n);
+ u64 w = b[wi];
+ w &= (~0ull << (i & BITSET_WORD_MASK));
+
+ for (;;) {
+ if (w) {
+ usize j = (wi << BITSET_WORD_SHIFT) + u64_ctz(w);
+ return j < n ? j : SIZE_MAX;
+ }
+
+ if (++wi >= wN) break;
+ w = b[wi];
+ }
+
+ return SIZE_MAX;
+}
+
+static inline usize
+bitset_prev_set(const u64 *b, usize i, usize n)
+{
+ if (i >= n) i = n - 1;
+ usize wi = i >> BITSET_WORD_SHIFT;
+ u64 w = b[wi];
+ u64 r = (u64)(i & BITSET_WORD_MASK);
+ w &= (r == BITSET_WORD_MASK) ? ~0ull : ((1ull << (r + 1)) - 1ull);
+
+ for (;;) {
+ if (w)
+ return (wi << BITSET_WORD_SHIFT) + (BITSET_WORD_MASK - (usize)u64_clz(w));
+ if (!wi) break;
+ w = b[--wi];
+ }
+
+ return SIZE_MAX;
+}
+
+#endif // DYLAN_BITSET_H
+
diff --git a/lib/date.h b/lib/date.h
new file mode 100644
index 0000000..6d8b060
--- /dev/null
+++ b/lib/date.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_DATE_H
+#define DYLAN_DATE_H
+
+//
+// Fast date algorithm, C implementation.
+// Source: https://www.benjoffe.com/fast-date-64
+//
+#include <stddef.h>
+#include <stdint.h>
+
+#include "util.h"
+
+#define C1 505054698555331ull // floor(2^64 * 4 / 146097)
+#define C2 50504432782230121ull // ceil(2^64 * 4 / 1461)
+#define C3 8619973866219416ull // floor(2^64 / 2140)
+
+#define SCALE 32u
+#define SHIFT0 (30556u * SCALE)
+#define SHIFT1 (5980u * SCALE)
+
+#if defined(__SIZEOF_INT128__)
+#define ERAS 4726498270ull
+#define D_SHIFT ((u64)(146097ull * ERAS - 719469ull))
+#define Y_SHIFT ((u64)(400ull * ERAS - 1ull))
+
+static inline u64
+mulhi(u64 a, u64 b)
+{
+ __uint128_t p = (__uint128_t)a * (__uint128_t)b;
+ return (u64)(p >> 64);
+}
+
+static inline void
+mul128(u64 a, u64 b, u64 *hi, u64 *lo)
+{
+ __uint128_t p = (__uint128_t)a * (__uint128_t)b;
+ *hi = (u64)(p >> 64);
+ *lo = (u64)p;
+}
+
+#else
+
+#define ERAS 14704u
+#define D_SHIFT ((u64)(146097ull * (u64)ERAS - 719469ull))
+#define Y_SHIFT ((u64)(400ull * (u64)ERAS - 1ull))
+
+static inline u64
+mulhi(u64 a, u64 b)
+{
+ u64 a0 = (u32)a;
+ u64 a1 = a >> 32;
+ u64 b0 = (u32)b;
+ u64 b1 = b >> 32;
+ u64 p00 = a0 * b0;
+ u64 p01 = a0 * b1;
+ u64 p10 = a1 * b0;
+ u64 p11 = a1 * b1;
+ u64 mid = (p00 >> 32) + (u32)p01 + (u32)p10;
+ return p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
+}
+
+static inline void
+mul128(u64 a, u64 b, u64 *hi, u64 *lo)
+{
+ u64 a0 = (u32)a;
+ u64 a1 = a >> 32;
+ u64 b0 = (u32)b;
+ u64 b1 = b >> 32;
+ u64 p00 = a0 * b0;
+ u64 p01 = a0 * b1;
+ u64 p10 = a1 * b0;
+ u64 p11 = a1 * b1;
+ u64 mid = (p00 >> 32) + (u32)p01 + (u32)p10;
+ *hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
+ *lo = (mid << 32) | (u32)p00;
+}
+
+#endif // defined(__SIZEOF_INT128__)
+
+static inline void
+ut_to_date(s32 day, s32 *Y, u32 *M, u32 *D)
+{
+ u64 rev = (u64)D_SHIFT - (u64)(s64)day; // Reverse day count.
+ u64 cen = mulhi(C1, rev); // Divide 36524.25
+ u64 jul = rev - cen / 4u + cen; // Julian map.
+ u64 num_hi, num_lo;
+ mul128(C2, jul, &num_hi, &num_lo); // Divide 365.25
+ u64 yrs64 = (u64)Y_SHIFT - (u64)num_hi; // Forward year.
+ u64 yrs = (u32)yrs64;
+ u32 ypt = (u32)mulhi((u64)(24451u * SCALE), num_lo); // Year (backwards).
+ u32 bump = (ypt < (3952u * SCALE)); // Jan or Feb.
+ u32 shift = bump ? SHIFT1 : SHIFT0; // Month offset.
+ u32 N = (u32)((yrs % 4u) * (16u * SCALE) + shift - ypt); // Leap years.
+ u32 m = N / (2048u * SCALE);
+ u32 d = (u32)mulhi(C3, (u64)(N % (2048u * SCALE))); // Divide 2140
+ *Y = (s32)yrs + (s32)bump;
+ *M = m;
+ *D = d + 1u;
+}
+
+static inline void
+ut_to_date_time(s64 tz, s32 day, s32 *Y, u32 *M, u32 *D, u32 *h, u32 *m, u32 *s)
+{
+ s64 us = tz + day;
+ s32 days = (s32)(us / 86400);
+ s32 r = (s32)(us % 86400);
+ if (r < 0) { r += 86400; days -= 1; }
+ ut_to_date(days, Y, M, D);
+ u32 hr = r / 3600;
+ r -= hr * 3600;
+ *h = hr;
+ u32 mn = r / 60;
+ *m = mn;
+ *s = r - mn * 60;
+}
+
+#endif // DYLAN_DATE_H
+
diff --git a/lib/readline.h b/lib/readline.h
new file mode 100644
index 0000000..2c05341
--- /dev/null
+++ b/lib/readline.h
@@ -0,0 +1,529 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_READLINE
+#define DYLAN_READLINE
+
+#ifndef RL_MAX
+#error "RL_MAX not set"
+#endif
+
+#include <assert.h>
+#include <string.h>
+
+#include "str.h"
+#include "utf8.h"
+#include "util.h"
+
+enum {
+ RL_NONE,
+ RL_PARTIAL,
+ RL_FULL,
+ RL_CAP = (RL_MAX >> 1) - 3,
+};
+
+struct readline {
+ str cl;
+ str cr;
+ cut pr;
+
+ usize vx;
+ usize vw;
+
+ usize prw;
+ usize clw;
+ usize crw;
+};
+
+static inline int
+rl_is_ifs(int c)
+{
+ return c == ' ' || c == '\t';
+}
+
+static inline usize
+rl_prompt(const struct readline *r)
+{
+ return r->prw + !!r->prw;
+}
+
+static inline usize
+rl_cursor(const struct readline *r)
+{
+ return rl_prompt(r) + r->clw;
+}
+
+static inline usize
+rl_total(const struct readline *r)
+{
+ return rl_cursor(r) + r->crw;
+}
+
+static inline void
+rl_vw_set(struct readline *r, usize vw)
+{
+ assert(vw);
+ r->vw = vw;
+ if (r->vx >= vw) r->vx = vw - 1;
+}
+
+static inline void
+rl_pr_set(struct readline *r, cut pr)
+{
+ r->pr = pr;
+ usize lw;
+ r->prw = utf8_cols(pr.d, pr.l, &lw);
+}
+
+static inline void
+rl_cr_set(struct readline *r, cut c)
+{
+ assert(c.l <= RL_CAP);
+ memcpy(r->cr.m + (r->cr.c - c.l), c.d, c.l);
+ r->cr.l = c.l;
+ usize lw;
+ r->crw = utf8_cols(c.d, c.l, &lw);
+}
+
+static inline void
+rl_cl_sync(struct readline *r)
+{
+ usize lw;
+ r->clw = utf8_cols(r->cl.m, r->cl.l, &lw);
+ usize c = rl_cursor(r);
+ if (c < r->vw) r->vx = c;
+ else r->vx = r->vw - lw;
+}
+
+static inline void
+rl_init(struct readline *r, usize vw, cut pr)
+{
+ STR_INIT(&r->cl, RL_MAX, 0, 0);
+ STR_INIT(&r->cr, RL_MAX >> 1, 0, 0);
+ rl_vw_set(r, vw);
+ rl_pr_set(r, pr);
+ r->vx = rl_prompt(r);
+ r->clw = 0;
+ r->crw = 0;
+}
+
+static inline const char *
+rl_cr_ptr(const struct readline *r)
+{
+ return r->cr.m + (r->cr.c - r->cr.l);
+}
+
+static inline cut
+rl_cr_get(const struct readline *r)
+{
+ return (cut) { rl_cr_ptr(r), r->cr.l };
+}
+
+static inline cut
+rl_cl_get(const struct readline *r)
+{
+ return (cut) { r->cl.m, r->cl.l };
+}
+
+static inline usize
+rl_cl_last(const struct readline *r, u32 *cp, int *w)
+{
+ usize l = utf8_decode_rev((const unsigned char *)r->cl.m, r->cl.l, cp);
+ *w = utf8_width(*cp);
+ return l;
+}
+
+static inline usize
+rl_cr_first(const struct readline *r, u32 *cp, int *w)
+{
+ const char *p = rl_cr_ptr(r);
+ char *n = utf8_decode((void *)p, cp);
+ *w = utf8_width(*cp);
+ return (usize)(n - p);
+}
+
+static inline usize
+rl_offset(const struct readline *r)
+{
+ usize c = rl_cursor(r);
+ return c > r->vx ? c - r->vx : 0;
+}
+
+static inline int
+rl_empty(const struct readline *r)
+{
+ return !r->cl.l && !r->cr.l;
+}
+
+static inline void
+rl_clear(struct readline *r)
+{
+ rl_pr_set(r, CUT_NULL);
+ r->cl.l = 0;
+ r->cr.l = 0;
+ r->clw = 0;
+ r->crw = 0;
+ r->vx = 0;
+}
+
+static inline usize
+rl_consume_cl(struct readline *r)
+{
+ usize w = 0;
+ do {
+ u32 cp;
+ int cw;
+ usize l = rl_cl_last(r, &cp, &cw);
+ r->cl.l -= l;
+ r->clw -= cw;
+ w += (usize)cw;
+ if (cw != 0) break;
+ } while (r->cl.l);
+ return w;
+}
+
+static inline usize
+rl_consume_cr(struct readline *r)
+{
+ usize w = 0;
+ do {
+ u32 cp;
+ int cw;
+ usize l = rl_cr_first(r, &cp, &cw);
+ r->cr.l -= l;
+ r->crw -= cw;
+ w += (usize)cw;
+ if (cw != 0 && !r->cr.l) break;
+ if (cw != 0) {
+ u32 ncp;
+ int nw;
+ rl_cr_first(r, &ncp, &nw);
+ if (nw != 0) break;
+ }
+ } while (r->cr.l);
+ return w;
+}
+
+static inline int
+rl_take_left_to_right(struct readline *r, usize *wo)
+{
+ if (!r->cl.l) return 0;
+ usize tw = 0;
+ for (;;) {
+ u32 cp;
+ int w;
+ usize l = rl_cl_last(r, &cp, &w);
+ if (r->cr.l + l > RL_CAP)
+ return -1;
+ r->cl.l -= l;
+ r->clw -= w;
+ usize o = r->cr.c - r->cr.l - l;
+ memcpy(r->cr.m + o, r->cl.m + r->cl.l, l);
+ r->cr.l += l;
+ r->crw += w;
+ tw += (usize)w;
+ if (!r->cl.l || w != 0) break;
+ }
+ if (wo) *wo = tw;
+ return 1;
+}
+
+static inline int
+rl_take_right_to_left(struct readline *r, usize *wo)
+{
+ if (!r->cr.l) return 0;
+ usize tw = 0;
+ for (;;) {
+ u32 cp;
+ int w;
+ usize l = rl_cr_first(r, &cp, &w);
+ if (r->cl.l + l > RL_CAP)
+ return -1;
+ str_copy(&r->cl, rl_cr_ptr(r), l);
+ r->cr.l -= l;
+ r->clw += w;
+ r->crw -= w;
+ tw += (usize)w;
+ if (!r->cr.l || w != 0) break;
+ }
+ if (wo) *wo = tw;
+ return 1;
+}
+
+static inline int
+rl_insert(struct readline *r, u32 c, const u8 *b, usize l, usize *n)
+{
+ if (r->cl.l + l >= RL_CAP) return RL_NONE;
+ u8 w = utf8_width(c);
+ str_copy(&r->cl, (const char *)b, l);
+ r->clw += w;
+ if (n) *n = w;
+ if (r->vx + w < r->vw) {
+ r->vx += w;
+ return RL_PARTIAL;
+ } else {
+ r->vx = r->vw - w;
+ return RL_FULL;
+ }
+}
+
+static inline int
+rl_backspace(struct readline *r, usize *n)
+{
+ if (!r->cl.l) return RL_NONE;
+ usize w = rl_consume_cl(r);
+ if (n) *n = w;
+ usize c = rl_cursor(r);
+ if (rl_total(r) < r->vw && r->vx == c + w) {
+ r->vx = c;
+ return RL_PARTIAL;
+ }
+ if (r->vx > c) r->vx = c;
+ return RL_FULL;
+}
+
+static inline int
+rl_delete(struct readline *r, usize *n)
+{
+ if (n) *n = 0;
+ if (!r->cr.l) return RL_NONE;
+ usize w = rl_consume_cr(r);
+ if (n) *n = w;
+ return r->vx + r->crw + w < r->vw ? RL_PARTIAL : RL_FULL;
+}
+
+static inline int
+rl_delete_left(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ r->cl.l = 0;
+ r->clw = 0;
+ r->vx = rl_prompt(r);
+ return RL_FULL;
+}
+
+static inline int
+rl_delete_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ r->cr.l = 0;
+ r->crw = 0;
+ return RL_PARTIAL;
+}
+
+static inline int
+rl_left(struct readline *r, usize *n)
+{
+ if (n) *n = 0;
+ if (!r->cl.l) {
+ if (!rl_offset(r)) return RL_NONE;
+ r->vx = rl_cursor(r);
+ return RL_FULL;
+ }
+ usize w;
+ if (rl_take_left_to_right(r, &w) < 0) return RL_NONE;
+ if (n) *n = w;
+ if (r->vx >= w && r->vx - w > 0) {
+ r->vx -= w;
+ return RL_PARTIAL;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_right(struct readline *r, usize *n)
+{
+ if (!r->cr.l) return RL_NONE;
+ usize w;
+ if (rl_take_right_to_left(r, &w) < 0) return RL_NONE;
+ if (n) *n = w;
+ if (r->vx + w + w <= r->vw) {
+ r->vx += w;
+ return RL_PARTIAL;
+ }
+ return RL_FULL;
+}
+
+static inline void
+rl_join(struct readline *r)
+{
+ str_copy(&r->cl, rl_cr_ptr(r), r->cr.l);
+ str_terminate(&r->cl);
+ r->clw += r->crw;
+ r->cr.l = 0;
+ r->crw = 0;
+}
+
+static inline int
+rl_home(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ if (r->cr.l + r->cl.l > RL_CAP) return RL_NONE;
+ usize s = rl_offset(r);
+ usize o = r->cr.c - r->cr.l - r->cl.l;
+ memcpy(r->cr.m + o, r->cl.m, r->cl.l);
+ r->cr.l += r->cl.l;
+ r->crw += r->clw;
+ r->cl.l = 0;
+ r->clw = 0;
+ r->vx = rl_prompt(r);
+ return s ? RL_FULL : RL_PARTIAL;
+}
+
+static inline int
+rl_end(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ if (r->cl.l + r->cr.l > RL_CAP) return RL_NONE;
+ str_copy(&r->cl, rl_cr_ptr(r), r->cr.l);
+ r->clw += r->crw;
+ r->cr.l = 0;
+ r->crw = 0;
+ usize c = rl_cursor(r);
+ if (c < r->vw) {
+ r->vx = c;
+ return RL_PARTIAL;
+ }
+ u32 cp;
+ int w;
+ rl_cl_last(r, &cp, &w);
+ r->vx = r->vw - w;
+ return RL_FULL;
+}
+
+static inline int
+rl_word_left(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ u32 cp;
+ int w;
+ while (r->cl.l) {
+ rl_cl_last(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_left(r, NULL) == RL_NONE) break;
+ }
+ while (r->cl.l) {
+ rl_cl_last(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_left(r, NULL) == RL_NONE) break;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_word_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ u32 cp;
+ int w;
+ while (r->cr.l) {
+ rl_cr_first(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_right(r, NULL) == RL_NONE) break;
+ }
+ while (r->cr.l) {
+ rl_cr_first(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_right(r, NULL) == RL_NONE) break;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_delete_word_prev(struct readline *r)
+{
+ if (!r->cl.l) return -1;
+ usize d = 0;
+ u32 cp;
+ int w;
+ for (; r->cl.l; d++) {
+ rl_cl_last(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_backspace(r, NULL) == RL_NONE) break;
+ }
+ for (; r->cl.l; d++) {
+ rl_cl_last(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_backspace(r, NULL) == RL_NONE) break;
+ }
+ return d ? RL_FULL : RL_NONE;
+}
+
+static inline int
+rl_delete_word_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ usize d = 0;
+ u32 cp;
+ int w;
+ for (; r->cr.l; d++) {
+ rl_cr_first(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_delete(r, NULL) == RL_NONE) break;
+ }
+ for (; r->cr.l; d++) {
+ rl_cr_first(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_delete(r, NULL) == RL_NONE) break;
+ }
+ return d ? RL_FULL : RL_NONE;
+}
+
+static inline usize
+rl_write_seg(str *s, const unsigned char *p, usize l, usize c, usize x, usize e)
+{
+ const unsigned char *pe = p + l;
+ u32 cp;
+ while (p < pe) {
+ const unsigned char *nc = utf8_decode((void *)p, &cp);
+ int w = utf8_width(cp);
+ if (c < x && c + w > x)
+ str_push_c(s, ' ');
+ else if (c + w > x && c + w <= e)
+ str_push(s, (const char *)p, (usize)(nc - p));
+ c += w;
+ if (c >= e) return c;
+ p = nc;
+ }
+ return c;
+}
+
+static inline void
+rl_write_range(const struct readline *r, str *s, usize x, usize n)
+{
+ usize c = 0;
+ usize e = x + n;
+ c = rl_write_seg(s, (const unsigned char *)r->pr.d, r->pr.l, c, x, e);
+ if (c >= e) return;
+ c = rl_write_seg(s, (const unsigned char *)r->cl.m, r->cl.l, c, x, e);
+ if (c >= e) return;
+ c = rl_write_seg(s, (const unsigned char *)rl_cr_ptr(r), r->cr.l, c, x, e);
+ if (c < e) str_memset(s, ' ', e - c);
+}
+
+static inline void
+rl_write_visible(const struct readline *r, str *s)
+{
+ rl_write_range(r, s, rl_offset(r), r->vw);
+}
+
+#endif // DYLAN_READLINE
+
diff --git a/lib/str.h b/lib/str.h
new file mode 100644
index 0000000..6464dcf
--- /dev/null
+++ b/lib/str.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_STR_H
+#define DYLAN_STR_H
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "util.h"
+
+struct str;
+typedef usize (str_err)(struct str *, void *, usize l);
+
+typedef struct str {
+ char *m;
+ usize l;
+ usize c;
+
+ str_err *f;
+ void *ctx;
+} str;
+
+#define STR_ERR ((usize) -1)
+#define STR_PUSH(s, p) str_push((s), (p), sizeof(p) - 1)
+#define STR_COPY(s, p) str_copy((s), (p), sizeof(p) - 1)
+
+#define STR_INIT(s, m, f, ctx) do { \
+ static char b[(m)]; \
+ str_init((s), (b), sizeof(b), (f), (ctx)); \
+} while (0)
+
+static inline void
+str_init(str *s, char *b, usize c, str_err *f, void *ctx)
+{
+ s->m = b;
+ s->l = -(!b || !c);
+ s->c = c;
+ s->f = f;
+ s->ctx = ctx;
+}
+
+static inline usize
+str_cb(str *s, usize l)
+{
+ return s->f ? s->f(s, s->ctx, l) : STR_ERR;
+}
+
+static inline int
+str_fit(str *s, usize l)
+{
+ if (s->l + l < s->c) return 1;
+ return str_cb(s, l) != STR_ERR;
+}
+
+static inline void
+str_copy(str *s, const char *p, usize l)
+{
+ memcpy(&s->m[s->l], p, l);
+ s->l += l;
+}
+
+static inline void
+str_copy_c(str *s, char c)
+{
+ s->m[s->l++] = c;
+}
+
+static inline cut
+str_push(str *s, const char *p, usize l)
+{
+ if (!str_fit(s, l)) return (cut) { 0, 0 };
+ str_copy(s, p, l);
+ return (cut){ &s->m[s->l - l], l };
+}
+
+static inline void
+str_push_c(str *s, char c)
+{
+ if (!str_fit(s, 1)) return;
+ str_copy_c(s, c);
+}
+
+static inline void
+str_push_s(str *s, const char *p)
+{
+ str_push(s, p, strlen(p));
+}
+
+static inline void
+str_memset(str *s, int c, usize n)
+{
+ if (!str_fit(s, n)) return;
+ memset(&s->m[s->l], c, n);
+ s->l += n;
+}
+
+static inline void
+str_push_u32_b(str *s, u32 v, u32 b, int c, usize l)
+{
+ static const char d[] = "0123456789abcdef";
+ char o[33];
+ char *p = &o[sizeof(o)];
+ assert(b >= 2 && b <= 16);
+ do {
+ *--p = d[v % b];
+ v /= b;
+ } while (v);
+ usize n = (usize)(&o[sizeof(o)] - p);
+ if (n < l) str_memset(s, c, l - n);
+ str_push(s, p, n);
+}
+
+static inline void
+str_push_u32_p(str *s, u32 v, int c, usize l)
+{
+ str_push_u32_b(s, v, 10, c, l);
+}
+
+static inline void
+str_push_u32(str *s, u32 v)
+{
+ str_push_u32_p(s, v, 0, 0);
+}
+
+static inline void
+str_push_u64(str *s, u64 v)
+{
+ char b[21];
+ char *p = &b[sizeof(b)];
+ do {
+ *--p = (char)('0' + (v % 10));
+ v /= 10;
+ } while (v);
+ str_push(s, p, (usize)(&b[sizeof(b)] - p));
+}
+
+static inline void
+str_push_sanitize(str *s, const char *p, usize l)
+{
+ if (!str_fit(s, l)) return;
+ char *d = s->m + s->l;
+ const unsigned char *b = (const unsigned char *) p;
+ for (usize i = 0; i < l; i++) {
+ unsigned char c = b[i];
+ d[i] = (char)(c >= 0x20 && c != 0x7F ? c : '?');
+ }
+ s->l += l;
+}
+
+static inline int
+str_cmp(str *a, str *b)
+{
+ return a->l == b->l && *a->m == *b->m && !memcmp(a->m, b->m, b->l);
+}
+
+static inline void
+str_terminate(str *s)
+{
+ if (str_fit(s, 1)) s->m[s->l] = 0;
+}
+
+#endif // DYLAN_STR_H
+
diff --git a/lib/term.h b/lib/term.h
new file mode 100644
index 0000000..b3b1440
--- /dev/null
+++ b/lib/term.h
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_TERM_H
+#define DYLAN_TERM_H
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#include "util.h"
+#include "vt.h"
+
+enum {
+ TERM_LOADED = 1 << 0,
+ TERM_RESIZE = 1 << 1,
+};
+
+static struct term {
+ struct termios o;
+ int fd;
+ int null;
+ volatile sig_atomic_t flag;
+ volatile sig_atomic_t dead;
+} *TERM;
+
+static inline void
+term_set_dead(struct term *t, int s)
+{
+ t->dead = 128 + s;
+}
+
+static inline int
+term_dead(const struct term *t)
+{
+ return t->dead;
+}
+
+static inline int
+term_resize(const struct term *t)
+{
+ return t->flag & TERM_RESIZE;
+}
+
+static inline void
+term_restore_on_signal(int s)
+{
+ if (!TERM) return;
+ if (!(TERM->flag & TERM_LOADED)) return;
+ term_set_dead(TERM, s);
+ tcsetattr(TERM->fd, TCSAFLUSH, &TERM->o);
+
+ //
+ // TODO: Unhardcode this.
+ //
+#define TERM_COOKED \
+ S(VT_ED0 VT_BPASTE_OFF VT_DECAWM_Y VT_DECTCEM_Y VT_ALT_SCREEN_N)
+ write_all(TERM->fd, TERM_COOKED);
+ write_all(STDOUT_FILENO, TERM_COOKED);
+}
+
+static inline void
+term_signal_fatal(int s)
+{
+ term_restore_on_signal(s);
+ _exit(128 + s);
+}
+
+static inline void
+term_signal_crash(int s)
+{
+ term_restore_on_signal(s);
+ struct sigaction sa = {0};
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sa.sa_mask);
+ sigaction(s, &sa, NULL);
+ kill(getpid(), s);
+}
+
+static inline void
+term_signal_sigwinch(int s)
+{
+ (void) s;
+ if (TERM) TERM->flag |= TERM_RESIZE;
+}
+
+static inline void
+term_signal_setup(void) {
+ struct sigaction sa;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = term_signal_fatal;
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+ sigaction(SIGQUIT, &sa, NULL);
+ sa.sa_handler = term_signal_crash;
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGABRT, &sa, NULL);
+ sigaction(SIGBUS, &sa, NULL);
+ sigaction(SIGFPE, &sa, NULL);
+ sigaction(SIGILL, &sa, NULL);
+ sa.sa_handler = term_signal_sigwinch;
+ sigaction(SIGWINCH, &sa, NULL);
+}
+
+static inline int
+term_size_update(struct term *t, u16 *row, u16 *col)
+{
+ struct winsize ws;
+ if (ioctl(t->fd, TIOCGWINSZ, &ws) < 0)
+ return -1;
+ t->flag &= ~TERM_RESIZE;
+ *row = ws.ws_row;
+ *col = ws.ws_col;
+ return 0;
+}
+
+static inline int
+term_init_io(struct term *t)
+{
+ if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO))
+ t->fd = STDIN_FILENO;
+ else if (isatty(STDIN_FILENO)) {
+ t->fd = open("/dev/tty", O_RDWR|O_CLOEXEC);
+ if (t->fd < 0) return -1;
+ } else {
+ t->fd = -1;
+ return -1;
+ }
+ t->null = open("/dev/null", O_WRONLY|O_CLOEXEC);
+ return t->null;
+}
+
+static inline int
+term_raw(const struct term *t)
+{
+ struct termios n = t->o;
+ n.c_iflag &= ~(BRKINT|ICRNL|INPCK|ISTRIP|IXON);
+ n.c_oflag &= ~(OPOST);
+ n.c_cflag |= (CS8);
+ n.c_lflag &= ~(ECHO|ICANON|IEXTEN|ISIG);
+ n.c_cc[VMIN] = 1;
+ n.c_cc[VTIME] = 0;
+ return tcsetattr(t->fd, TCSAFLUSH, &n);
+}
+
+static inline int
+term_cooked(struct term *t)
+{
+ assert(t->flag & TERM_LOADED);
+ return tcsetattr(t->fd, TCSAFLUSH, &t->o);
+}
+
+static inline int
+term_init(struct term *t)
+{
+ if (term_init_io(t) < 0) return -1;
+ if (tcgetattr(t->fd, &t->o) < 0) return -1;
+ TERM = t;
+ t->flag |= TERM_LOADED;
+ term_signal_setup();
+ return 0;
+}
+
+static inline void
+term_reap(void)
+{
+ for (int st; waitpid(-1, &st, WNOHANG) > 0; );
+}
+
+static inline void
+term_destroy(const struct term *t)
+{
+ if (t->fd >= 0) close(t->fd);
+ if (t->null >= 0) close(t->null);
+ TERM = NULL;
+}
+
+#endif // DYLAN_TERM_H
+
diff --git a/lib/term_key.h b/lib/term_key.h
new file mode 100644
index 0000000..f92ef80
--- /dev/null
+++ b/lib/term_key.h
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_TERM_KEY_H
+#define DYLAN_TERM_KEY_H
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/time.h>
+
+#include "utf8.h"
+#include "util.h"
+
+//
+// Store encoded special keys alongside utf8 codepoints in an unused range.
+//
+#define KEY_TAG 0x80000000u
+#define KEY_SYM 0x40000000u
+#define KEY_MOD_SHIFT 27u
+#define KEY_MOD_MASK (0x7u << KEY_MOD_SHIFT)
+#define KEY_TXT_MASK 0x001FFFFFu
+#define KEY_SYM_MASK 0x000000FFu
+#define KEY_IS_SYM(k) ((((u32)(k)) & (KEY_TAG|KEY_SYM)) == (KEY_TAG|KEY_SYM))
+#define KEY_GET_MOD(k) ((((u32)(k)) & KEY_MOD_MASK) >> KEY_MOD_SHIFT)
+
+#define K(m, c) \
+((u32)(m) == 0u ? (u32)(c) : (KEY_IS_SYM(c) \
+? (((u32)(c) & ~KEY_MOD_MASK) | (((u32)(m) & 0x7u) << KEY_MOD_SHIFT)) \
+: (KEY_TAG | (((u32)(m) & 0x7u) << KEY_MOD_SHIFT) | ((u32)(c) & KEY_TXT_MASK))))
+
+#define KEY_REG(id) (KEY_TAG|KEY_SYM | ((u32)(id) & KEY_SYM_MASK))
+
+#define MOD_SHIFT (1u << 0)
+#define MOD_ALT (1u << 1)
+#define MOD_CTRL (1u << 2)
+
+#define KEY_ESCAPE 27
+#define KEY_BACKSPACE 127
+#define KEY_TAB K(MOD_CTRL, 'i')
+#define KEY_SHIFT_TAB K(MOD_SHIFT|MOD_CTRL, 'i')
+#define KEY_ENTER K(MOD_CTRL, 'm')
+#define KEY_UP KEY_REG(1)
+#define KEY_DOWN KEY_REG(2)
+#define KEY_LEFT KEY_REG(3)
+#define KEY_RIGHT KEY_REG(4)
+#define KEY_HOME KEY_REG(5)
+#define KEY_END KEY_REG(6)
+#define KEY_PAGE_UP KEY_REG(7)
+#define KEY_PAGE_DOWN KEY_REG(8)
+#define KEY_INSERT KEY_REG(9)
+#define KEY_DELETE KEY_REG(10)
+#define KEY_F1 KEY_REG(11)
+#define KEY_F2 KEY_REG(12)
+#define KEY_F3 KEY_REG(13)
+#define KEY_F4 KEY_REG(14)
+#define KEY_F5 KEY_REG(15)
+#define KEY_F6 KEY_REG(16)
+#define KEY_F7 KEY_REG(17)
+#define KEY_F8 KEY_REG(18)
+#define KEY_F9 KEY_REG(19)
+#define KEY_F10 KEY_REG(20)
+#define KEY_F11 KEY_REG(21)
+#define KEY_F12 KEY_REG(22)
+#define KEY_PASTE KEY_REG(23)
+#define KEY_PASTE_END KEY_REG(24)
+
+struct term_key {
+ u8 b[64];
+ u16 l;
+ u32 c;
+};
+
+static inline u32
+term_key_csi_tilde(u8 c, u32 m)
+{
+ switch (c) {
+ case 1: return K(m, KEY_HOME);
+ case 2: return K(m, KEY_INSERT);
+ case 3: return K(m, KEY_DELETE);
+ case 4: return K(m, KEY_END);
+ case 5: return K(m, KEY_PAGE_UP);
+ case 6: return K(m, KEY_PAGE_DOWN);
+ case 7: return K(m, KEY_HOME);
+ case 8: return K(m, KEY_END);
+ case 11: return K(m, KEY_F1);
+ case 12: return K(m, KEY_F2);
+ case 13: return K(m, KEY_F3);
+ case 14: return K(m, KEY_F4);
+ case 15: return K(m, KEY_F5);
+ case 17: return K(m, KEY_F6);
+ case 18: return K(m, KEY_F7);
+ case 19: return K(m, KEY_F8);
+ case 20: return K(m, KEY_F9);
+ case 21: return K(m, KEY_F10);
+ case 23: return K(m, KEY_F11);
+ case 24: return K(m, KEY_F12);
+ case 200: return KEY_PASTE;
+ case 201: return KEY_PASTE_END;
+ default: return 0;
+ }
+}
+
+static inline u32
+term_key_csi_final(u8 c, u32 m)
+{
+ switch (c) {
+ case 'A': return K(m, KEY_UP);
+ case 'B': return K(m, KEY_DOWN);
+ case 'C': return K(m, KEY_RIGHT);
+ case 'D': return K(m, KEY_LEFT);
+ case 'H': return K(m, KEY_HOME);
+ case 'F': return K(m, KEY_END);
+ case 'Z': return K(MOD_SHIFT|MOD_CTRL | m, 'i');
+ default: return 0;
+ }
+}
+
+static inline u32
+term_key_csi_ss3(u8 c)
+{
+ switch (c) {
+ case 'P': return K(0, KEY_F1);
+ case 'Q': return K(0, KEY_F2);
+ case 'R': return K(0, KEY_F3);
+ case 'S': return K(0, KEY_F4);
+ case 'A': return K(0, KEY_UP);
+ case 'B': return K(0, KEY_DOWN);
+ case 'C': return K(0, KEY_RIGHT);
+ case 'D': return K(0, KEY_LEFT);
+ case 'H': return K(0, KEY_HOME);
+ case 'F': return K(0, KEY_END);
+ default: return 0;
+ }
+}
+
+static inline s32
+term_key_csi_int(const u8 *b, usize l, usize *i)
+{
+ u32 v = 0;
+ usize j = *i;
+ if (j >= l || b[j] < '0' || b[j] > '9') return -1;
+ for (; j < l && b[j] >= '0' && b[j] <= '9'; j++)
+ v = v * 10u + (u32)(b[j] - '0');
+ *i = j;
+ return (s32) v;
+}
+
+static inline u32
+term_key_csi_xterm_mod(u32 x)
+{
+ u32 m = 0;
+ if (x < 2 || x > 8) return m;
+ if (x & 1u) m |= MOD_ALT;
+ if (x & 2u) m |= MOD_SHIFT;
+ if (x & 4u) m |= MOD_CTRL;
+ return m;
+}
+
+static inline bool
+term_key_csi_end(u8 c)
+{
+ return c >= 0x40 && c <= 0x7E;
+}
+
+static inline bool
+term_key_csi_read(int fd, struct term_key *k)
+{
+ for (;;) {
+ if (k->l >= sizeof(k->b))
+ return false;
+ if (read(fd, &k->b[k->l], 1) != 1)
+ return false;
+ u8 c = k->b[k->l++];
+ if (term_key_csi_end(c))
+ break;
+ }
+ return true;
+}
+
+static inline bool
+term_key_ss3(int fd, struct term_key *k)
+{
+ if (read(fd, &k->b[k->l], 1) != 1) return false;
+ k->l++;
+ k->c = term_key_csi_ss3(k->b[2]);
+ return !!k->c;
+}
+
+static inline bool
+term_key_csi(int fd, struct term_key *k)
+{
+ if (!term_key_csi_read(fd, k)) return false;
+ usize n = k->l;
+ if (n < 3) return false;
+ u8 f = k->b[n - 1];
+ usize i = 2;
+ if (i < n && (k->b[i] == '?' || k->b[i] == '>' || k->b[i] == '<'))
+ i++;
+ s32 p1 = term_key_csi_int(k->b, n, &i);
+ s32 p2 = -1;
+ if (p1 != -1 && i < n && k->b[i] == ';') {
+ i++;
+ p2 = term_key_csi_int(k->b, n, &i);
+ }
+ u32 m = p2 != -1 ? term_key_csi_xterm_mod(p2) : 0;
+ if (f == '~' && p1 != -1)
+ k->c = term_key_csi_tilde(p1, m);
+ else
+ k->c = term_key_csi_final(f, m);
+ return !!k->c;
+}
+
+static inline bool
+term_key_utf8(int fd, struct term_key *k, usize o, u32 m)
+{
+ u8 c = k->b[o];
+ if (c >= 1 && c <= 26) {
+ k->c = K(m | MOD_CTRL, 'a' + (c - 1));
+ k->l = o + 1;
+ return true;
+ }
+ if (c >= 0xC0) {
+ usize l = utf8_expected(c);
+ if (!l) return false;
+ for (usize i = 1; i < l; i++)
+ if (read(fd, &k->b[o + i], 1) != 1)
+ return false;
+ int e;
+ u32 cp;
+ utf8_decode_untrusted(&k->b[o], &cp, &e);
+ if (e) return false;
+ k->c = K(m, cp);
+ k->l = o + l;
+ return true;
+ }
+ k->c = K(m, c);
+ k->l = o + 1;
+ return true;
+}
+
+static inline bool
+term_key_read(int fd, struct term_key *k)
+{
+ if (read(fd, k->b, 1) != 1) return false;
+ k->l = 1;
+ if (unlikely(k->b[0] == '\033')) {
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 30000 };
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(fd, &rfds);
+ if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) {
+ k->c = k->b[0];
+ return true;
+ }
+ if (read(fd, &k->b[1], 1) != 1) return false;
+ k->l = 2;
+ switch (k->b[1]) {
+ case '[': return term_key_csi(fd, k);
+ case 'O': return term_key_ss3(fd, k);
+ default: return term_key_utf8(fd, k, 1, MOD_ALT);
+ }
+ }
+ return term_key_utf8(fd, k, 0, 0);
+}
+
+#endif // DYLAN_TERM_KEY_H
+
diff --git a/lib/utf8.h b/lib/utf8.h
new file mode 100644
index 0000000..0104661
--- /dev/null
+++ b/lib/utf8.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_UTF8_H
+#define DYLAN_UTF8_H
+
+#include "util.h"
+
+static inline usize
+utf8_expected(u8 b)
+{
+ static const u8 L[] = {
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,2,3,3,4,0
+ };
+ return L[b >> 3];
+}
+
+static inline int
+utf8_width(u32 c)
+{
+ if (c == 0) return 0;
+
+ // Control.
+ if (c < 0x20) return 0;
+ if (c >= 0x7f && c < 0xa0) return 0;
+
+ // Zero width joiner.
+ if (c == 0x200d) return 0;
+
+ // Combining.
+ if ((c >= 0x0300 && c <= 0x036f) ||
+ (c >= 0x1ab0 && c <= 0x1aff) ||
+ (c >= 0x1dc0 && c <= 0x1dff) ||
+ (c >= 0x20d0 && c <= 0x20ff) ||
+ (c >= 0xfe20 && c <= 0xfe2f) ||
+ (c >= 0xe0100 && c <= 0xe01ef))
+ return 0;
+
+ // Variation selectors.
+ if ((c >= 0xfe00 && c <= 0xfe0f))
+ return 0;
+
+ // Emoji modifiers.
+ if (c >= 0x1f3fb && c <= 0x1f3ff)
+ return 0;
+
+ // East asian wide.
+ if ((c >= 0x1100 && c <= 0x115f) ||
+ c == 0x2329 || c == 0x232a ||
+ (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) ||
+ (c >= 0xac00 && c <= 0xd7a3) ||
+ (c >= 0xf900 && c <= 0xfaff) ||
+ (c >= 0xfe10 && c <= 0xfe19) ||
+ (c >= 0xfe30 && c <= 0xfe6f) ||
+ (c >= 0xff00 && c <= 0xff60) ||
+ (c >= 0xffe0 && c <= 0xffe6) ||
+ (c >= 0x20000 && c <= 0x2fffd) ||
+ (c >= 0x30000 && c <= 0x3fffd))
+ return 2;
+
+ // Emoji block.
+ if ((c >= 0x1f300 && c <= 0x1faff) ||
+ (c >= 0x2600 && c <= 0x27bf) ||
+ (c >= 0x2b50 && c <= 0x2b55))
+ return 2;
+
+ return 1;
+}
+
+//
+// Branchless UTF8 decoder by Skeeto.
+// Source: https://nullprogram.com/blog/2017/10/06/
+//
+static inline void *
+utf8_decode(void *b, u32 *c)
+{
+ unsigned char *s = (unsigned char *)b;
+ usize l = utf8_expected(s[0]);
+ static const int m[] = {0x00, 0x7f, 0x1f, 0x0f, 0x07};
+ static const int shc[] = {0, 18, 12, 6, 0};
+ *c = (u32)(s[0] & m[l]) << 18;
+ *c |= (u32)(s[1] & 0x3f) << 12;
+ *c |= (u32)(s[2] & 0x3f) << 6;
+ *c |= (u32)(s[3] & 0x3f);
+ *c >>= shc[l];
+ return s + l + !l;
+}
+
+static void *
+utf8_decode_untrusted(void *b, u32 *c, int *e)
+{
+ static const u32 mi[] = {4194304, 0, 128, 2048, 65536};
+ static const int she[] = {0, 6, 4, 2, 0};
+ unsigned char *s = (unsigned char *)b;
+ unsigned char *n = utf8_decode(b, c);
+ usize l = utf8_expected(s[0]);
+ *e = (*c < mi[l]) << 6; // Non-canonical encoding.
+ *e |= ((*c >> 11) == 0x1b) << 7; // Surrogate half?
+ *e |= (*c > 0x10FFFF) << 8; // Out of range?
+ *e |= (s[1] & 0xc0) >> 2;
+ *e |= (s[2] & 0xc0) >> 4;
+ *e |= (s[3]) >> 6;
+ *e ^= 0x2a; // Top two bits of each tail byte correct?
+ *e >>= she[l];
+ return n;
+}
+
+static inline usize
+utf8_decode_rev(const unsigned char *s, usize x, u32 *c)
+{
+ usize i = x;
+ while (i > 0 && (s[i - 1] & 0xc0) == 0x80) i--;
+ if (i > 0) i--;
+ usize l = x - i;
+ utf8_decode((void *)(s + i), c);
+ return l;
+}
+
+static inline usize
+utf8_cols(const void *s, usize l, usize *lw)
+{
+ usize w = 0;
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ *lw = 0;
+ while (p < e) {
+ u32 cp;
+ p = utf8_decode((void *)p, &cp);
+ *lw = utf8_width(cp);
+ w += *lw;
+ }
+ return w;
+}
+
+static inline usize
+utf8_trunc_narrow(const char *s, usize l, usize c)
+{
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ for (usize i = 0; p < e && i < c; i++) {
+ unsigned char b = *p++;
+ if (!(b & 0x80)) continue;
+ for (; p < e && ((*p & 0xC0) == 0x80); p++);
+ }
+ return (usize)(p - (const unsigned char *)s);
+}
+
+static inline usize
+utf8_trunc_wide(const char *s, usize l, usize c)
+{
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ for (usize i = 0; p < e && i < c; ) {
+ u32 cp;
+ const unsigned char *n = (const unsigned char *)utf8_decode((void *)p, &cp);
+ usize a = (usize)(n - p);
+ if (!a) a = 1;
+ int w = utf8_width(cp);
+ if (i + w > c) break;
+ i += w;
+ p += a;
+ }
+ return (usize)(p - (const unsigned char *)s);
+}
+
+#endif // DYLAN_UTF8_H
+
diff --git a/lib/util.h b/lib/util.h
new file mode 100644
index 0000000..819e939
--- /dev/null
+++ b/lib/util.h
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_UTIL_H
+#define DYLAN_UTIL_H
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <spawn.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#define ARR_SIZE(a) ((intptr_t)(sizeof(a) / sizeof(*(a))))
+#define IS_POW2(x) ((x) > 0 && (((x) & ((x) - 1)) == 0))
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+
+#if defined(__GNUC__) || defined(__clang__)
+#define likely(x) __builtin_expect(!!(x), 1)
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#else
+#define likely(x) !!(x)
+#define unlikely(x) !!(x)
+#endif
+
+#if __STDC_VERSION__ >= 201112L
+#define STATIC_ASSERT _Static_assert
+#else
+#define SA_CAT_(a,b) a##b
+#define SA_CAT(a,b) SA_CAT_(a,b)
+#define STATIC_ASSERT(c, m) \
+ enum { SA_CAT(static_assert_line_, __LINE__) = 1 / (!!(c)) }
+#endif
+
+typedef union {
+ long long ll;
+ long double ld;
+ void *p;
+} align_max;
+
+typedef uint64_t u64;
+typedef uint32_t u32;
+typedef uint16_t u16;
+typedef uint8_t u8;
+typedef int64_t s64;
+typedef int32_t s32;
+typedef int16_t s16;
+typedef int8_t s8;
+typedef size_t usize;
+typedef ptrdiff_t size;
+
+typedef struct {
+ const char *d;
+ usize l;
+} cut;
+
+//
+// Add four bytes to the end of the CUT to allow the branchless utf8 decoder to
+// potentially overread the string.
+//
+#define CUT(s) (cut) { s "\0\0\0\0", sizeof(s) - 1 }
+#define CUT_NULL ((cut){0})
+#define STR_NULL (&(str){0})
+#define S(s) (s), (sizeof(s) - 1)
+
+static inline int
+cut_cmp(cut a, cut b)
+{
+ return a.l == b.l && *a.d == *b.d && !memcmp(a.d, b.d, b.l);
+}
+
+static inline u64
+bitfield_get64(u64 v, u8 s, u8 b)
+{
+ return (v >> s) & ((1ULL << b) - 1);
+}
+
+static inline void
+bitfield_set64(u64 *t, u64 v, u8 s, u8 b)
+{
+ u64 m = ((1ULL << b) - 1) << s;
+ *t = (*t & ~m) | ((v << s) & m);
+}
+
+static inline u32
+bitfield_get32(u32 v, int o, int l)
+{
+ return (v >> o) & ((1u << l) - 1);
+}
+
+static inline void
+bitfield_set32(u32 *v, u32 x, int o, int l)
+{
+ u32 m = ((1u << l) - 1) << o;
+ *v = (*v & ~m) | ((x << o) & m);
+}
+
+static inline void
+bitfield_set8(u8 *t, u8 v, u8 s, u8 b)
+{
+ u8 m = ((1ULL << b) - 1) << s;
+ *t = (*t & ~m) | ((v << s) & m);
+}
+
+static inline cut
+get_env(const char *e, const char *f)
+{
+ const char *p = getenv(e);
+ cut r;
+ r.d = p && *p ? p : f;
+ r.l = r.d ? strlen(r.d) : 0;
+ return r;
+}
+
+static inline int
+write_all(int fd, const char *b, usize l)
+{
+ for (usize o = 0; o < l; ) {
+ ssize_t r = write(fd, b + o, l - o);
+ if (r > 0) {
+ o += (usize) r;
+ continue;
+ }
+ if (r == -1 && errno == EINTR)
+ continue;
+ return -1;
+ }
+ return 0;
+}
+
+static inline int
+run_cmd(int tty, int in, const char *d, const char *const a[], bool bg)
+{
+ extern char **environ;
+ posix_spawn_file_actions_t fa;
+ pid_t pid;
+ int rc;
+ int st;
+ pid_t r;
+ rc = posix_spawn_file_actions_init(&fa);
+ if (rc) { errno = rc; return -1; }
+ if (in >= 0) {
+ rc = posix_spawn_file_actions_adddup2(&fa, in, 0);
+ if (rc) goto fail_fa;
+ }
+ if (tty >= 0) {
+ if ((rc = posix_spawn_file_actions_adddup2(&fa, tty, 1)) ||
+ (rc = posix_spawn_file_actions_adddup2(&fa, tty, 2)))
+ goto fail_fa;
+ }
+ if (d) {
+#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 202405L
+ rc = posix_spawn_file_actions_addchdir(&fa, d);
+#elif defined(_GNU_SOURCE) || defined(_BSD_SOURCE)
+ rc = posix_spawn_file_actions_addchdir_np(&fa, d);
+#else
+ posix_spawn_file_actions_destroy(&fa);
+ pid = fork();
+ if (pid == -1) return -1;
+ if (!pid) {
+ if (in >= 0) {
+ if (dup2(in, 0) == -1) _exit(127);
+ if (in != 0) close(in);
+ }
+ if (tty >= 0) {
+ if (dup2(tty, 1) == -1) _exit(127);
+ if (dup2(tty, 2) == -1) _exit(127);
+ if (tty != 1 && tty != 2) close(tty);
+ }
+ if (d && chdir(d) == -1)
+ _exit(127);
+ execvp(a[0], (char *const *)a);
+ _exit(127);
+ }
+ goto end;
+#endif
+ if (rc) goto fail_fa;
+ }
+ rc = posix_spawnp(&pid, a[0], &fa, NULL, (char *const *)a, environ);
+ posix_spawn_file_actions_destroy(&fa);
+ if (rc) { errno = rc; return -1; }
+ goto end; // Silence compiler warning when ifdefs cause no jump.
+end:
+ if (bg) return 0;
+ do r = waitpid(pid, &st, 0);
+ while (r == -1 && errno == EINTR);
+ return r == -1 ? -1 : st;
+fail_fa:
+ posix_spawn_file_actions_destroy(&fa);
+ errno = rc;
+ return -1;
+}
+
+static inline int
+fd_from_buf(const char *b, usize l)
+{
+ int fd[2];
+ if (pipe(fd)) return -1;
+ if (l > PIPE_BUF) {
+#ifdef F_GETPIPE_SZ
+ int c = fcntl(fd[1], F_GETPIPE_SZ);
+ if (c < 0 || l > (usize)c) {
+ close(fd[0]);
+ close(fd[1]);
+ return -1;
+ }
+#else
+ close(fd[0]);
+ close(fd[1]);
+ return -1;
+#endif
+ }
+ ssize_t w = write(fd[1], b, l);
+ close(fd[1]);
+ if (w < 0 || (usize)w != l) {
+ close(fd[0]);
+ return -1;
+ }
+ return fd[0];
+}
+
+static inline usize
+u64_popcount(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (usize) __builtin_popcountll(x);
+#else
+ usize c = 0;
+ for (; x; x &= x - 1) c++;
+ return c;
+#endif
+}
+
+static inline u64
+u64_ctz(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (u64)__builtin_ctzll(x);
+#else
+ u64 i = 0;
+ while (!(x & 1ull)) { x >>= 1; i++; }
+ return i;
+#endif
+}
+
+static inline u64
+u64_clz(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (u64)__builtin_clzll(x);
+#else
+ u64 i = 0;
+ for (u64 m = 1ull << 63; !(x & m); m >>= 1) i++;
+ return i;
+#endif
+}
+
+static inline u32
+hash_fnv1a32(const char *d, usize l)
+{
+ u32 h = 2166136261u;
+ for (usize i = 0; i < l; i++)
+ h = (h ^ (unsigned char)d[i]) * 16777619u;
+ return h | 1;
+}
+
+static inline s64
+tz_offset(void)
+{
+ time_t n = time(NULL);
+ struct tm lt;
+ struct tm gt;
+ if (!localtime_r(&n, &lt)) return 0;
+ if (!gmtime_r(&n, &gt)) return 0;
+ time_t lo = mktime(&lt);
+ time_t gm = mktime(&gt);
+ if (lo == (time_t)-1 || gm == (time_t)-1) return 0;
+ return (s64)(lo - gm);
+}
+
+static inline usize
+fm_path_resolve(char *s, usize l)
+{
+ char *m = s;
+ usize i = 0, w = 0;
+ while (i < l) {
+ while (i < l && m[i] == '/') i++;
+ if (i >= l) break;
+ usize b = i;
+ while (i < l && m[i] != '/') i++;
+ usize n = i - b;
+ if (n == 1 && m[b] == '.')
+ continue;
+ if (n == 2 && m[b] == '.' && m[b + 1] == '.') {
+ if (w > 1) {
+ if (m[w - 1] == '/') w--;
+ while (w > 1 && m[w - 1] != '/') w--;
+ }
+ continue;
+ }
+ if (w == 0 || m[w - 1] != '/')
+ m[w++] = '/';
+ if (w != b) memmove(m + w, m + b, n);
+ w += n;
+ }
+ if (w > 1 && m[w - 1] == '/') w--;
+ if (!w) m[w++] = '/';
+ m[w] = 0;
+ return w;
+}
+
+#endif // DYLAN_UTIL_H
+
diff --git a/lib/vt.h b/lib/vt.h
new file mode 100644
index 0000000..4262154
--- /dev/null
+++ b/lib/vt.h
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_VT_H
+#define DYLAN_VT_H
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "str.h"
+#include "util.h"
+
+#define VT_ESC "\x1b"
+#define VT_CR "\r"
+#define VT_LF "\n"
+#define VT_CUU1 VT_ESC "[A"
+#define VT_CUU(n) VT_ESC "[" #n "A"
+#define VT_CUD1 VT_ESC "[B"
+#define VT_CUD(n) VT_ESC "[" #n "B"
+#define VT_CUF1 VT_ESC "[C"
+#define VT_CUF(n) VT_ESC "[" #n "C"
+#define VT_CUB1 VT_ESC "[D"
+#define VT_CUB(n) VT_ESC "[" #n "D"
+#define VT_CUP(x, y) VT_ESC "[" #x ";" #y "H"
+#define VT_CUP1 VT_ESC "[H"
+#define VT_DECAWM_Y VT_ESC "[?7h"
+#define VT_DECAWM_N VT_ESC "[?7l"
+#define VT_DECSC VT_ESC "7"
+#define VT_DECRC VT_ESC "8"
+#define VT_DECSTBM(x, y) VT_ESC "[" #x ";" #y "r"
+#define VT_DECTCEM_Y VT_ESC "[?25h"
+#define VT_DECTCEM_N VT_ESC "[?25l"
+#define VT_ED0 VT_ESC "[J"
+#define VT_ED1 VT_ESC "[1J"
+#define VT_ED2 VT_ESC "[2J"
+#define VT_EL0 VT_ESC "[K"
+#define VT_EL1 VT_ESC "[1K"
+#define VT_EL2 VT_ESC "[2K"
+#define VT_IL0 VT_ESC "[L"
+#define VT_IL(n) VT_ESC "[" #n "L"
+#define VT_ICH1 VT_ESC "[@"
+#define VT_ICH(n) VT_ESC "[" #n "@"
+#define VT_DCH1 VT_ESC "[P"
+#define VT_DCH(n) VT_ESC "[" #n "P"
+#define VT_SGR0 VT_ESC "[m"
+
+//
+// XTerm Alternate Screen.
+// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
+//
+#define VT_ALT_SCREEN_Y VT_ESC "[?1049h"
+#define VT_ALT_SCREEN_N VT_ESC "[?1049l"
+
+//
+// Synchronized Updates.
+// https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
+// https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md
+//
+#define VT_BSU VT_ESC "[?2026h"
+#define VT_ESU VT_ESC "[?2026l"
+
+//
+// Bracketed Paste.
+//
+#define VT_BPASTE_ON VT_ESC "[?2004h"
+#define VT_BPASTE_OFF VT_ESC "[?2004l"
+
+//
+// VT_SGR(...) macro supporting 16 arguments.
+// NOTE: A byte can be saved by using VT_SGR0 instead of VT_SGR(0).
+//
+#define VT_Fa(f, x) f(x)
+#define VT_Fb(f, x, ...) f(x) ";" VT_Fa(f, __VA_ARGS__)
+#define VT_Fc(f, x, ...) f(x) ";" VT_Fb(f, __VA_ARGS__)
+#define VT_Fd(f, x, ...) f(x) ";" VT_Fc(f, __VA_ARGS__)
+#define VT_Fe(f, x, ...) f(x) ";" VT_Fd(f, __VA_ARGS__)
+#define VT_Ff(f, x, ...) f(x) ";" VT_Fe(f, __VA_ARGS__)
+#define VT_Fg(f, x, ...) f(x) ";" VT_Ff(f, __VA_ARGS__)
+#define VT_Fh(f, x, ...) f(x) ";" VT_Fg(f, __VA_ARGS__)
+#define VT_Fi(f, x, ...) f(x) ";" VT_Fh(f, __VA_ARGS__)
+#define VT_Fj(f, x, ...) f(x) ";" VT_Fi(f, __VA_ARGS__)
+#define VT_Fk(f, x, ...) f(x) ";" VT_Fj(f, __VA_ARGS__)
+#define VT_Fl(f, x, ...) f(x) ";" VT_Fk(f, __VA_ARGS__)
+#define VT_Fm(f, x, ...) f(x) ";" VT_Fl(f, __VA_ARGS__)
+#define VT_Fn(f, x, ...) f(x) ";" VT_Fm(f, __VA_ARGS__)
+#define VT_Fo(f, x, ...) f(x) ";" VT_Fn(f, __VA_ARGS__)
+#define VT_Fp(f, x, ...) f(x) ";" VT_Fo(f, __VA_ARGS__)
+#define VT_GET_q(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,...) q
+#define VT_CNT(...) VT_GET_q(__VA_ARGS__,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,0)
+#define VT_STR(x) #x
+#define VT_CAT(a,b) a##b
+#define VT_FN(N, f, ...) VT_CAT(VT_F, N)(f, __VA_ARGS__)
+#define VT_JOIN(...) VT_FN(VT_CNT(__VA_ARGS__), VT_STR, __VA_ARGS__)
+#define VT_SGR(...) VT_ESC "[" VT_JOIN(__VA_ARGS__) "m"
+
+static inline void
+vt_cup(str *s, u32 x, u32 y)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, y);
+ str_push_c(s, ';');
+ str_push_u32(s, x);
+ str_push_c(s, 'H');
+}
+
+static inline void
+vt_cuf(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'C');
+}
+
+static inline void
+vt_cub(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'D');
+}
+
+static inline void
+vt_ich(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, '@');
+}
+
+static inline void
+vt_dch(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'P');
+}
+
+static inline void
+vt_decstbm(str *s, u32 x, u32 y)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, x);
+ str_push_c(s, ';');
+ str_push_u32(s, y);
+ str_push_c(s, 'r');
+}
+
+static inline void
+vt_sgr(str *s, u32 a, u32 b)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, a);
+ str_push_c(s, ';');
+ str_push_u32(s, b);
+ str_push_c(s, 'm');
+}
+
+#endif // DYLAN_VT_H
+