[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
0

Configure Feed

Select the types of activity you want to include in your feed.

tty / validate.ts
4.5 kB 147 lines
1import { Type } from "@sinclair/typebox"; 2import { TypeCompiler } from "@sinclair/typebox/compiler"; 3import type { Op } from "./ops.ts"; 4import type { RenderOptions, RenderResult, Term } from "./term.ts"; 5 6/* ── Range helpers (match bit-packing in pack()) ──────────────────── */ 7 8const u8 = Type.Integer({ minimum: 0, maximum: 255 }); 9const u16 = Type.Integer({ minimum: 0, maximum: 65535 }); 10 11/* RGBA color packed as (a << 24 | r << 16 | g << 8 | b). When alpha >= 128, 12 * bit 31 is set and JavaScript interprets the value as a negative int32. 13 * Accept both signed and unsigned representations of the same bit pattern. */ 14const rgba = Type.Integer({ minimum: -0x80000000, maximum: 0xFFFFFFFF }); 15 16/* ── Sizing axis (discriminated union) ────────────────────────────── */ 17 18const Fit = Type.Object({ 19 type: Type.Literal("fit"), 20 min: Type.Optional(Type.Number()), 21 max: Type.Optional(Type.Number()), 22}); 23 24const Grow = Type.Object({ 25 type: Type.Literal("grow"), 26 min: Type.Optional(Type.Number()), 27 max: Type.Optional(Type.Number()), 28}); 29 30const Percent = Type.Object({ 31 type: Type.Literal("percent"), 32 value: Type.Number(), 33}); 34 35const Fixed = Type.Object({ 36 type: Type.Literal("fixed"), 37 value: Type.Number(), 38}); 39 40const SizingAxis = Type.Union([Fit, Grow, Percent, Fixed]); 41 42/* ── Sub-objects ──────────────────────────────────────────────────── */ 43 44const Padding = Type.Object({ 45 left: Type.Optional(u8), 46 right: Type.Optional(u8), 47 top: Type.Optional(u8), 48 bottom: Type.Optional(u8), 49}); 50 51const Layout = Type.Object({ 52 width: Type.Optional(SizingAxis), 53 height: Type.Optional(SizingAxis), 54 padding: Type.Optional(Padding), 55 gap: Type.Optional(u16), 56 direction: Type.Optional( 57 Type.Union([Type.Literal("ltr"), Type.Literal("ttb")]), 58 ), 59 alignX: Type.Optional(u8), 60 alignY: Type.Optional(u8), 61}); 62 63const CornerRadius = Type.Object({ 64 tl: Type.Optional(u8), 65 tr: Type.Optional(u8), 66 bl: Type.Optional(u8), 67 br: Type.Optional(u8), 68}); 69 70const Border = Type.Object({ 71 color: rgba, 72 left: Type.Optional(u8), 73 right: Type.Optional(u8), 74 top: Type.Optional(u8), 75 bottom: Type.Optional(u8), 76}); 77 78const Clip = Type.Object({ 79 horizontal: Type.Optional(Type.Boolean()), 80 vertical: Type.Optional(Type.Boolean()), 81}); 82 83const Floating = Type.Object({ 84 x: Type.Optional(Type.Number()), 85 y: Type.Optional(Type.Number()), 86 parent: Type.Optional(Type.Integer({ minimum: 0 })), 87 attachTo: Type.Optional(u8), 88 attachPoints: Type.Optional(u8), 89 zIndex: Type.Optional(u16), 90}); 91 92/* ── Op types (discriminated on `id`) ─────────────────────────────── */ 93 94const CloseElement = Type.Object({ id: Type.Literal(0x04) }); 95 96const OpenElement = Type.Object({ 97 id: Type.Literal(0x02), 98 name: Type.String(), 99 layout: Type.Optional(Layout), 100 bg: Type.Optional(rgba), 101 cornerRadius: Type.Optional(CornerRadius), 102 border: Type.Optional(Border), 103 clip: Type.Optional(Clip), 104 floating: Type.Optional(Floating), 105}); 106 107const TextOp = Type.Object({ 108 id: Type.Literal(0x03), 109 content: Type.String(), 110 color: Type.Optional(rgba), 111 fontSize: Type.Optional(u8), 112 fontId: Type.Optional(u8), 113 wrap: Type.Optional(u8), 114 attrs: Type.Optional(u8), 115}); 116 117const Ops = Type.Array(Type.Union([OpenElement, TextOp, CloseElement])); 118 119/* ── Compiled validator ───────────────────────────────────────────── */ 120 121const compiled = TypeCompiler.Compile(Ops); 122 123export function validate(ops: unknown): ops is Op[] { 124 return compiled.Check(ops); 125} 126 127export function assert(ops: unknown): asserts ops is Op[] { 128 if (!compiled.Check(ops)) { 129 let errors = [...compiled.Errors(ops)]; 130 let msg = errors 131 .slice(0, 5) 132 .map((e) => `${e.path}: ${e.message}`) 133 .join("\n"); 134 throw new TypeError(`Invalid ops:\n${msg}`); 135 } 136} 137 138/* ── Term wrapper ─────────────────────────────────────────────────── */ 139 140export function validated(term: Term): Term { 141 return { 142 render(ops: Op[], options?: RenderOptions): RenderResult { 143 assert(ops); 144 return term.render(ops, options); 145 }, 146 }; 147}