Something went wrong. Try again.
[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 ops.ts
9.1 kB · 369 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370/* Command buffer opcodes — mirrors ops.h */const OP_OPEN_ELEMENT = 0x02;const OP_TEXT = 0x03;const OP_CLOSE_ELEMENT = 0x04;const OP_SNAPSHOT = 0x05;
/* Property group masks for OPEN_ELEMENT */const PROP_LAYOUT = 0x01;const PROP_BG_COLOR = 0x02;const PROP_CORNER_RADIUS = 0x04;const PROP_BORDER = 0x08;const PROP_CLIP = 0x10;const PROP_FLOATING = 0x20;
const encoder = new TextEncoder();
function packAxis(view: DataView, offset: number, axis: SizingAxis): number { let o = offset; switch (axis.type) { case "fit": view.setUint32(o, 0, true); o += 4; view.setFloat32(o, axis.min ?? 0, true); o += 4; view.setFloat32(o, axis.max ?? 0, true); o += 4; break; case "grow": view.setUint32(o, 1, true); o += 4; view.setFloat32(o, axis.min ?? 0, true); o += 4; view.setFloat32(o, axis.max ?? 0, true); o += 4; break; case "percent": view.setUint32(o, 2, true); o += 4; view.setFloat32(o, axis.value, true); o += 4; view.setFloat32(o, 0, true); o += 4; break; case "fixed": view.setUint32(o, 3, true); o += 4; view.setFloat32(o, axis.value, true); o += 4; view.setFloat32(o, 0, true); o += 4; break; } return o;}
function packString( view: DataView, bytes: Uint8Array, o: number, end: number, context: string,): number { let paddedLength = Math.ceil(bytes.length / 4) * 4; let next = o + 4 + paddedLength; if (next > end) { throw new RangeError( `clayterm transfer buffer capacity exceeded while packing ${context} ` + `(${next} byte offset, ${end} byte limit). ` + `Render a smaller visible slice or reduce frame content.`, ); }
view.setUint32(o, bytes.length, true); o += 4; new Uint8Array(view.buffer).set(bytes, o); o += paddedLength; return o;}
export function pack( ops: Op[], mem: ArrayBufferLike, offset: number, limit?: number,): number { let view = new DataView(mem); let end = limit ?? mem.byteLength; let o = offset;
for (let op of ops) { switch (op.directive) { case OP_CLOSE_ELEMENT: view.setUint32(o, op.directive, true); o += 4; break;
case OP_OPEN_ELEMENT: { view.setUint32(o, OP_OPEN_ELEMENT, true); o += 4;
let bytes = encoder.encode(op.id); o = packString(view, bytes, o, end, "element id");
let mask = 0; if (op.layout) mask |= PROP_LAYOUT; if (op.bg !== undefined) mask |= PROP_BG_COLOR; if (op.cornerRadius) mask |= PROP_CORNER_RADIUS; if (op.border) mask |= PROP_BORDER; if (op.clip) mask |= PROP_CLIP; if (op.floating) mask |= PROP_FLOATING; view.setUint32(o, mask, true); o += 4;
if (op.layout) { let l = op.layout; o = packAxis(view, o, l.width ?? { type: "fit" }); o = packAxis(view, o, l.height ?? { type: "fit" });
let p = l.padding ?? {}; view.setUint32( o, (p.left ?? 0) | ((p.right ?? 0) << 8) | ((p.top ?? 0) << 16) | ((p.bottom ?? 0) << 24), true, ); o += 4;
view.setUint32( o, (l.gap ?? 0) | ((l.direction === "ttb" ? 1 : 0) << 16), true, ); o += 4;
view.setUint32(o, (l.alignX ?? 0) | ((l.alignY ?? 0) << 8), true); o += 4; }
if (op.bg !== undefined) { view.setUint32(o, op.bg, true); o += 4; }
if (op.cornerRadius) { let cr = op.cornerRadius; view.setUint32( o, (cr.tl ?? 0) | ((cr.tr ?? 0) << 8) | ((cr.bl ?? 0) << 16) | ((cr.br ?? 0) << 24), true, ); o += 4; }
if (op.border) { let b = op.border; view.setUint32(o, b.color, true); o += 4; view.setUint32( o, (b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) | ((b.bottom ?? 0) << 24), true, ); o += 4; }
if (op.clip) { view.setUint32( o, (op.clip.horizontal ? 1 : 0) | ((op.clip.vertical ? 1 : 0) << 8), true, ); o += 4; }
if (op.floating) { let f = op.floating; view.setFloat32(o, f.x ?? 0, true); o += 4; view.setFloat32(o, f.y ?? 0, true); o += 4; view.setUint32(o, f.parent ?? 0, true); o += 4; view.setUint32( o, (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | ((f.zIndex ?? 0) << 16), true, ); o += 4; } break; }
case OP_SNAPSHOT: { new Uint8Array(mem).set(op.data, o); o += op.data.length; break; }
case OP_TEXT: { view.setUint32(o, OP_TEXT, true); o += 4; view.setUint32(o, op.color ?? 0xFFFFFFFF, true); o += 4; view.setUint32( o, (op.fontSize ?? 1) | ((op.fontId ?? 0) << 8) | ((op.wrap ?? 0) << 16) | ((op.attrs ?? 0) << 24), true, ); o += 4;
let str = encoder.encode(op.content); o = packString(view, str, o, end, "text content"); break; } } if (o > end) { throw new RangeError( `ops exceed buffer capacity (${o - offset} bytes packed, ${ end - offset } available)`, ); } }
return (o - offset) / 4;}
export function rgba(r: number, g: number, b: number, a = 255): number { return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF);}
export type SizingAxis = | { type: "fit"; min?: number; max?: number } | { type: "grow"; min?: number; max?: number } | { type: "percent"; value: number } | { type: "fixed"; value: number };
export const fit = (min = 0, max = 0): SizingAxis => ({ type: "fit", min, max,});export const grow = (min = 0, max = 0): SizingAxis => ({ type: "grow", min, max,});export const percent = (value: number): SizingAxis => ({ type: "percent", value,});export const fixed = (value: number): SizingAxis => ({ type: "fixed", value });
export interface CloseElement { directive: typeof OP_CLOSE_ELEMENT;}
export interface OpenElement { directive: typeof OP_OPEN_ELEMENT; id: string; layout?: { width?: SizingAxis; height?: SizingAxis; padding?: { left?: number; right?: number; top?: number; bottom?: number }; gap?: number; direction?: "ltr" | "ttb"; alignX?: number; alignY?: number; }; bg?: number; cornerRadius?: { tl?: number; tr?: number; bl?: number; br?: number }; border?: { color: number; left?: number; right?: number; top?: number; bottom?: number; }; clip?: { horizontal?: boolean; vertical?: boolean }; floating?: { x?: number; y?: number; parent?: number; attachTo?: number; attachPoints?: number; zIndex?: number; };}
export interface Text { directive: typeof OP_TEXT; content: string; color?: number; fontSize?: number; fontId?: number; wrap?: number; attrs?: number;}
interface Snapshot { directive: typeof OP_SNAPSHOT; data: Uint8Array;}
export type Op = OpenElement | Text | CloseElement | Snapshot;
export function open( id: string, props: Omit<OpenElement, "directive" | "id"> = {},): OpenElement { return { directive: OP_OPEN_ELEMENT, id, ...props };}
export function text( content: string, props: Omit<Text, "directive" | "content"> = {},): Text { return { directive: OP_TEXT, content, ...props };}
export function close(): CloseElement { return { directive: OP_CLOSE_ELEMENT };}
function packSize(ops: Op[]): number { let n = 0; for (let op of ops) { switch (op.directive) { case OP_CLOSE_ELEMENT: n += 4; break; case OP_SNAPSHOT: n += op.data.length; break; case OP_OPEN_ELEMENT: { n += 4; // opcode n += 4 + Math.ceil(encoder.encode(op.id).length / 4) * 4; // id string n += 4; // mask if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align if (op.bg !== undefined) n += 4; if (op.cornerRadius) n += 4; if (op.border) n += 8; if (op.clip) n += 4; if (op.floating) n += 16; break; } case OP_TEXT: { n += 4 + 4 + 4; // opcode + color + cfg n += 4 + Math.ceil(encoder.encode(op.content).length / 4) * 4; // string break; } } } return n;}
export function snapshot(ops: Op[]): Op { let size = packSize(ops); let buf = new ArrayBuffer(size); let words = pack(ops, buf, 0, size); return { directive: OP_SNAPSHOT, data: new Uint8Array(buf, 0, words * 4) };}