[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay bomb.sh/docs/tty
Something went wrong. Try again.
tty validate.ts
6.8 kB · 244 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245import { Type } from "@sinclair/typebox";import { TypeCompiler } from "@sinclair/typebox/compiler";import type { Op } from "./ops.ts";import type { RenderOptions, RenderResult, Term } from "./term.ts";
/* ── Range helpers (match bit-packing in pack()) ──────────────────── */
const u8 = Type.Integer({ minimum: 0, maximum: 255 });const u16 = Type.Integer({ minimum: 0, maximum: 65535 });const i16 = Type.Integer({ minimum: -32768, maximum: 32767 });
/* RGBA color packed as (a << 24 | r << 16 | g << 8 | b). When alpha >= 128, * bit 31 is set and JavaScript interprets the value as a negative int32. * Accept both signed and unsigned representations of the same bit pattern. */const rgba = Type.Integer({ minimum: -0x80000000, maximum: 0xFFFFFFFF });
/* ── Sizing axis (discriminated union) ────────────────────────────── */
const Fit = Type.Object({ type: Type.Literal("fit"), min: Type.Optional(Type.Number()), max: Type.Optional(Type.Number()),});
const Grow = Type.Object({ type: Type.Literal("grow"), min: Type.Optional(Type.Number()), max: Type.Optional(Type.Number()),});
const Percent = Type.Object({ type: Type.Literal("percent"), value: Type.Number(),});
const Fixed = Type.Object({ type: Type.Literal("fixed"), value: Type.Number(),});
const SizingAxis = Type.Union([Fit, Grow, Percent, Fixed]);
/* ── Sub-objects ──────────────────────────────────────────────────── */
const Padding = Type.Object({ left: Type.Optional(u8), right: Type.Optional(u8), top: Type.Optional(u8), bottom: Type.Optional(u8),});
const Layout = Type.Object({ width: Type.Optional(SizingAxis), height: Type.Optional(SizingAxis), padding: Type.Optional(Padding), gap: Type.Optional(u16), direction: Type.Optional( Type.Union([Type.Literal("ltr"), Type.Literal("ttb")]), ), alignX: Type.Optional( Type.Union([ Type.Literal("left"), Type.Literal("center"), Type.Literal("right"), ]), ), alignY: Type.Optional( Type.Union([ Type.Literal("top"), Type.Literal("center"), Type.Literal("bottom"), ]), ),});
const CornerRadius = Type.Object({ tl: Type.Optional(u8), tr: Type.Optional(u8), bl: Type.Optional(u8), br: Type.Optional(u8),});
const BorderSide = Type.Union([ u8, Type.Object({ width: u8, color: Type.Optional(rgba), bg: Type.Optional(rgba), }),]);
const Border = Type.Object({ color: rgba, bg: Type.Optional(rgba), left: Type.Optional(BorderSide), right: Type.Optional(BorderSide), top: Type.Optional(BorderSide), bottom: Type.Optional(BorderSide),});
const Clip = Type.Object({ horizontal: Type.Optional(Type.Boolean()), vertical: Type.Optional(Type.Boolean()),});
const AttachPoint = Type.Union([ Type.Literal("left-top"), Type.Literal("left-center"), Type.Literal("left-bottom"), Type.Literal("center-top"), Type.Literal("center-center"), Type.Literal("center-bottom"), Type.Literal("right-top"), Type.Literal("right-center"), Type.Literal("right-bottom"),]);
const AttachTo = Type.Union([ Type.Literal("none"), Type.Literal("parent"), Type.Literal("element"), Type.Literal("root"),]);
const PointerCaptureMode = Type.Union([ Type.Literal("capture"), Type.Literal("passthrough"),]);
const ClipTo = Type.Union([ Type.Literal("none"), Type.Literal("attached-parent"),]);
const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), expand: Type.Optional(Type.Object({ width: Type.Optional(Type.Number()), height: Type.Optional(Type.Number()), })), parent: Type.Optional(Type.Integer({ minimum: 0 })), attachTo: Type.Optional(AttachTo), attachPoints: Type.Optional(Type.Object({ element: Type.Optional(AttachPoint), parent: Type.Optional(AttachPoint), })), pointerCaptureMode: Type.Optional(PointerCaptureMode), clipTo: Type.Optional(ClipTo), zIndex: Type.Optional(i16),});
const TransitionProperty = Type.Union([ Type.Literal("x"), Type.Literal("y"), Type.Literal("position"), Type.Literal("width"), Type.Literal("height"), Type.Literal("size"), Type.Literal("bg"), Type.Literal("overlay"), Type.Literal("borderColor"), Type.Literal("borderWidth"), Type.Literal("all"),]);
const Easing = Type.Union([ Type.Literal("linear"), Type.Literal("easeIn"), Type.Literal("easeOut"), Type.Literal("easeInOut"),]);
const Transition = Type.Object({ duration: Type.Number({ minimum: 0 }), easing: Type.Optional(Easing), properties: Type.Array(TransitionProperty), interactive: Type.Optional(Type.Boolean()),});
/* ── Op types (discriminated on `directive`) ──────────────────────── */
const CloseElement = Type.Object({ directive: Type.Literal(0x04) });
const OpenElement = Type.Object({ directive: Type.Literal(0x02), id: Type.String(), layout: Type.Optional(Layout), bg: Type.Optional(rgba), cornerRadius: Type.Optional(CornerRadius), border: Type.Optional(Border), clip: Type.Optional(Clip), floating: Type.Optional(Floating), transition: Type.Optional(Transition),});
const TextOp = Type.Object({ directive: Type.Literal(0x03), content: Type.String(), color: Type.Optional(rgba), bg: Type.Optional(rgba), fontSize: Type.Optional(u8), fontId: Type.Optional(u8), wrap: Type.Optional(u8), attrs: Type.Optional(u8),});
const Ops = Type.Array(Type.Union([OpenElement, TextOp, CloseElement]));
/* ── Compiled validator ───────────────────────────────────────────── */
const compiled = TypeCompiler.Compile(Ops);
export function validate(ops: unknown): ops is Op[] { return compiled.Check(ops);}
export function assert(ops: unknown): asserts ops is Op[] { if (!compiled.Check(ops)) { let errors = [...compiled.Errors(ops)]; let msg = errors .slice(0, 5) .map((e) => `${e.path}: ${e.message}`) .join("\n"); throw new TypeError(`Invalid ops:\n${msg}`); }}
/* ── Term wrapper ─────────────────────────────────────────────────── */
export function validated(term: Term): Term { return { get capabilities() { return term.capabilities; }, render(ops: Op[], options?: RenderOptions): RenderResult { assert(ops); return term.render(ops, options); }, update(events) { return term.update(events); }, };}