[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 / ops.ts
8.2 kB 312 lines
1/* Command buffer opcodes — mirrors ops.h */ 2const OP_OPEN_ELEMENT = 0x02; 3const OP_TEXT = 0x03; 4const OP_CLOSE_ELEMENT = 0x04; 5 6/* Property group masks for OPEN_ELEMENT */ 7const PROP_LAYOUT = 0x01; 8const PROP_BG_COLOR = 0x02; 9const PROP_CORNER_RADIUS = 0x04; 10const PROP_BORDER = 0x08; 11const PROP_CLIP = 0x10; 12const PROP_FLOATING = 0x20; 13 14/* ── Packing ──────────────────────────────────────────────────────── */ 15 16const encoder = new TextEncoder(); 17 18function packAxis(view: DataView, offset: number, axis: SizingAxis): number { 19 let o = offset; 20 switch (axis.type) { 21 case "fit": 22 view.setUint32(o, 0, true); 23 o += 4; 24 view.setFloat32(o, axis.min ?? 0, true); 25 o += 4; 26 view.setFloat32(o, axis.max ?? 0, true); 27 o += 4; 28 break; 29 case "grow": 30 view.setUint32(o, 1, true); 31 o += 4; 32 view.setFloat32(o, axis.min ?? 0, true); 33 o += 4; 34 view.setFloat32(o, axis.max ?? 0, true); 35 o += 4; 36 break; 37 case "percent": 38 view.setUint32(o, 2, true); 39 o += 4; 40 view.setFloat32(o, axis.value, true); 41 o += 4; 42 view.setFloat32(o, 0, true); 43 o += 4; 44 break; 45 case "fixed": 46 view.setUint32(o, 3, true); 47 o += 4; 48 view.setFloat32(o, axis.value, true); 49 o += 4; 50 view.setFloat32(o, 0, true); 51 o += 4; 52 break; 53 } 54 return o; 55} 56 57function packString(view: DataView, bytes: Uint8Array, o: number): number { 58 view.setUint32(o, bytes.length, true); 59 o += 4; 60 new Uint8Array(view.buffer).set(bytes, o); 61 o += Math.ceil(bytes.length / 4) * 4; 62 return o; 63} 64 65export function pack( 66 ops: Op[], 67 mem: ArrayBufferLike, 68 offset: number, 69 limit?: number, 70): number { 71 let view = new DataView(mem); 72 let end = limit ?? mem.byteLength; 73 let o = offset; 74 75 for (let op of ops) { 76 switch (op.id) { 77 case OP_CLOSE_ELEMENT: 78 view.setUint32(o, op.id, true); 79 o += 4; 80 break; 81 82 case OP_OPEN_ELEMENT: { 83 view.setUint32(o, OP_OPEN_ELEMENT, true); 84 o += 4; 85 86 let id = encoder.encode(op.name); 87 o = packString(view, id, o); 88 89 let mask = 0; 90 if (op.layout) mask |= PROP_LAYOUT; 91 if (op.bg !== undefined) mask |= PROP_BG_COLOR; 92 if (op.cornerRadius) mask |= PROP_CORNER_RADIUS; 93 if (op.border) mask |= PROP_BORDER; 94 if (op.clip) mask |= PROP_CLIP; 95 if (op.floating) mask |= PROP_FLOATING; 96 view.setUint32(o, mask, true); 97 o += 4; 98 99 if (op.layout) { 100 let l = op.layout; 101 o = packAxis(view, o, l.width ?? { type: "fit" }); 102 o = packAxis(view, o, l.height ?? { type: "fit" }); 103 104 let p = l.padding ?? {}; 105 view.setUint32( 106 o, 107 (p.left ?? 0) | ((p.right ?? 0) << 8) | ((p.top ?? 0) << 16) | 108 ((p.bottom ?? 0) << 24), 109 true, 110 ); 111 o += 4; 112 113 view.setUint32( 114 o, 115 (l.gap ?? 0) | ((l.direction === "ttb" ? 1 : 0) << 16), 116 true, 117 ); 118 o += 4; 119 120 view.setUint32(o, (l.alignX ?? 0) | ((l.alignY ?? 0) << 8), true); 121 o += 4; 122 } 123 124 if (op.bg !== undefined) { 125 view.setUint32(o, op.bg, true); 126 o += 4; 127 } 128 129 if (op.cornerRadius) { 130 let cr = op.cornerRadius; 131 view.setUint32( 132 o, 133 (cr.tl ?? 0) | ((cr.tr ?? 0) << 8) | ((cr.bl ?? 0) << 16) | 134 ((cr.br ?? 0) << 24), 135 true, 136 ); 137 o += 4; 138 } 139 140 if (op.border) { 141 let b = op.border; 142 view.setUint32(o, b.color, true); 143 o += 4; 144 view.setUint32( 145 o, 146 (b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) | 147 ((b.bottom ?? 0) << 24), 148 true, 149 ); 150 o += 4; 151 } 152 153 if (op.clip) { 154 view.setUint32( 155 o, 156 (op.clip.horizontal ? 1 : 0) | ((op.clip.vertical ? 1 : 0) << 8), 157 true, 158 ); 159 o += 4; 160 } 161 162 if (op.floating) { 163 let f = op.floating; 164 view.setFloat32(o, f.x ?? 0, true); 165 o += 4; 166 view.setFloat32(o, f.y ?? 0, true); 167 o += 4; 168 view.setUint32(o, f.parent ?? 0, true); 169 o += 4; 170 view.setUint32( 171 o, 172 (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | 173 ((f.zIndex ?? 0) << 16), 174 true, 175 ); 176 o += 4; 177 } 178 break; 179 } 180 181 case OP_TEXT: { 182 view.setUint32(o, OP_TEXT, true); 183 o += 4; 184 view.setUint32(o, op.color ?? 0xFFFFFFFF, true); 185 o += 4; 186 view.setUint32( 187 o, 188 (op.fontSize ?? 1) | 189 ((op.fontId ?? 0) << 8) | 190 ((op.wrap ?? 0) << 16) | 191 ((op.attrs ?? 0) << 24), 192 true, 193 ); 194 o += 4; 195 196 let str = encoder.encode(op.content); 197 o = packString(view, str, o); 198 break; 199 } 200 } 201 if (o > end) { 202 throw new RangeError( 203 `ops exceed buffer capacity (${o - offset} bytes packed, ${ 204 end - offset 205 } available)`, 206 ); 207 } 208 } 209 210 return (o - offset) / 4; 211} 212 213/* ── Color ────────────────────────────────────────────────────────── */ 214 215export function rgba(r: number, g: number, b: number, a = 255): number { 216 return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | 217 (b & 0xFF); 218} 219 220/* ── Sizing axis types ────────────────────────────────────────────── */ 221 222export type SizingAxis = 223 | { type: "fit"; min?: number; max?: number } 224 | { type: "grow"; min?: number; max?: number } 225 | { type: "percent"; value: number } 226 | { type: "fixed"; value: number }; 227 228export const fit = (min = 0, max = 0): SizingAxis => ({ 229 type: "fit", 230 min, 231 max, 232}); 233export const grow = (min = 0, max = 0): SizingAxis => ({ 234 type: "grow", 235 min, 236 max, 237}); 238export const percent = (value: number): SizingAxis => ({ 239 type: "percent", 240 value, 241}); 242export const fixed = (value: number): SizingAxis => ({ type: "fixed", value }); 243 244/* ── Op descriptors ───────────────────────────────────────────────── */ 245 246export interface CloseElement { 247 id: typeof OP_CLOSE_ELEMENT; 248} 249 250export interface OpenElement { 251 id: typeof OP_OPEN_ELEMENT; 252 name: string; 253 layout?: { 254 width?: SizingAxis; 255 height?: SizingAxis; 256 padding?: { left?: number; right?: number; top?: number; bottom?: number }; 257 gap?: number; 258 direction?: "ltr" | "ttb"; 259 alignX?: number; 260 alignY?: number; 261 }; 262 bg?: number; 263 cornerRadius?: { tl?: number; tr?: number; bl?: number; br?: number }; 264 border?: { 265 color: number; 266 left?: number; 267 right?: number; 268 top?: number; 269 bottom?: number; 270 }; 271 clip?: { horizontal?: boolean; vertical?: boolean }; 272 floating?: { 273 x?: number; 274 y?: number; 275 parent?: number; 276 attachTo?: number; 277 attachPoints?: number; 278 zIndex?: number; 279 }; 280} 281 282export interface Text { 283 id: typeof OP_TEXT; 284 content: string; 285 color?: number; 286 fontSize?: number; 287 fontId?: number; 288 wrap?: number; 289 attrs?: number; 290} 291 292export type Op = OpenElement | Text | CloseElement; 293 294/* ── Descriptor constructors ──────────────────────────────────────── */ 295 296export function open( 297 name: string, 298 props: Omit<OpenElement, "id" | "name"> = {}, 299): OpenElement { 300 return { id: OP_OPEN_ELEMENT, name, ...props }; 301} 302 303export function text( 304 content: string, 305 props: Omit<Text, "id" | "content"> = {}, 306): Text { 307 return { id: OP_TEXT, content, ...props }; 308} 309 310export function close(): CloseElement { 311 return { id: OP_CLOSE_ELEMENT }; 312}