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