[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 / term-native.ts
5.8 kB 187 lines
1import { f32, offsets, struct } from "./typedef.ts"; 2 3export interface BoundingBox { 4 x: number; 5 y: number; 6 width: number; 7 height: number; 8} 9 10const BoundingBoxStruct = struct<BoundingBox>({ 11 x: f32(), 12 y: f32(), 13 width: f32(), 14 height: f32(), 15}); 16 17const BOUNDING_BOX = offsets(BoundingBoxStruct); 18 19const WASM_PAGE_BYTES = 65536; 20const TEXT_TRANSFER_BUFFER_BYTES = 1024 * 1024; 21const CLAY_DEFAULT_MAX_ELEMENT_COUNT = 8192; 22 23// Conservative fixed wire-format budget per element. This covers the largest 24// non-text open/close element encoding we currently support; text, element id, 25// and snapshot payload bytes live in TEXT_TRANSFER_BUFFER_BYTES. 26const MAX_FIXED_ELEMENT_WIRE_BYTES = 116; 27 28export interface Native { 29 memory: WebAssembly.Memory; 30 statePtr: number; 31 opsBuf: number; 32 reduce( 33 ct: number, 34 buf: number, 35 len: number, 36 mode: number, 37 row: number, 38 deltaTime: number, 39 ): void; 40 output(ct: number): number; 41 length(ct: number): number; 42 setPointer(x: number, y: number, down: boolean): void; 43 getPointerOverIds(): string[]; 44 getElementBounds(id: string): BoundingBox | undefined; 45 animating(ct: number): number; 46 errorCount(ct: number): number; 47 errorType(ct: number, index: number): number; 48 errorMessage(ct: number, index: number): string; 49} 50 51import { compiled } from "./wasm.ts"; 52 53export async function createTermNative( 54 w: number, 55 h: number, 56): Promise<Native> { 57 let memory = new WebAssembly.Memory({ initial: 2 }); 58 let exports: Record<string, CallableFunction> = {}; 59 60 let instance = await WebAssembly.instantiate(compiled, { 61 env: { memory }, 62 clay: { 63 measureTextFunction( 64 ret: number, 65 text: number, 66 _config: number, 67 _userData: number, 68 ) { 69 exports.measure(ret, text); 70 }, 71 queryScrollOffsetFunction( 72 ret: number, 73 _elementId: number, 74 _userData: number, 75 ) { 76 let view = new DataView(memory.buffer); 77 view.setFloat32(ret, 0, true); 78 view.setFloat32(ret + 4, 0, true); 79 }, 80 }, 81 }); 82 83 Object.assign(exports, instance.exports); 84 85 let ct = exports as unknown as { 86 __heap_base: WebAssembly.Global; 87 clayterm_size(w: number, h: number): number; 88 init(mem: number, w: number, h: number): number; 89 reduce( 90 ct: number, 91 buf: number, 92 len: number, 93 mode: number, 94 row: number, 95 deltaTime: number, 96 ): void; 97 output(ct: number): number; 98 length(ct: number): number; 99 Clay_SetPointerState(vec: number, down: number): void; 100 pointer_over_count(): number; 101 pointer_over_id_string_length(index: number): number; 102 pointer_over_id_string_ptr(index: number): number; 103 get_element_bounds(name: number, len: number, out: number): number; 104 animating(ct: number): number; 105 error_count(ct: number): number; 106 error_type(ct: number, index: number): number; 107 error_message_length(ct: number, index: number): number; 108 error_message_ptr(ct: number, index: number): number; 109 }; 110 111 let heap = ct.__heap_base.value as number; 112 let size = ct.clayterm_size(w, h); 113 114 // Grow memory once to fit heap + renderer state + fixed transfer buffer. 115 // The transfer budget is intentionally fixed: text/id/snapshot payload bytes 116 // get 1MB, and fixed op overhead gets one max-sized element per Clay element. 117 // Do not grow this dynamically per render; improve the wire format instead. 118 let transferBytes = TEXT_TRANSFER_BUFFER_BYTES + 119 CLAY_DEFAULT_MAX_ELEMENT_COUNT * MAX_FIXED_ELEMENT_WIRE_BYTES; 120 let needed = heap + size + transferBytes; 121 let pages = Math.ceil(needed / WASM_PAGE_BYTES); 122 let current = memory.buffer.byteLength / WASM_PAGE_BYTES; 123 if (pages > current) { 124 memory.grow(pages - current); 125 } 126 127 let statePtr = ct.init(heap, w, h); 128 let opsBuf = (heap + size + 3) & ~3; 129 130 return { 131 memory, 132 statePtr, 133 opsBuf, 134 reduce: ct.reduce, 135 output: ct.output, 136 length: ct.length, 137 animating: ct.animating, 138 setPointer(x: number, y: number, down: boolean) { 139 let view = new DataView(memory.buffer); 140 view.setFloat32(opsBuf, x, true); 141 view.setFloat32(opsBuf + 4, y, true); 142 ct.Clay_SetPointerState(opsBuf, down ? 1 : 0); 143 }, 144 getPointerOverIds(): string[] { 145 let decoder = new TextDecoder(); 146 let count = ct.pointer_over_count(); 147 let ids: string[] = []; 148 for (let i = 0; i < count; i++) { 149 let len = ct.pointer_over_id_string_length(i); 150 if (len === 0) continue; 151 let ptr = ct.pointer_over_id_string_ptr(i); 152 ids.push(decoder.decode(new Uint8Array(memory.buffer, ptr, len))); 153 } 154 return ids; 155 }, 156 getElementBounds(id: string): BoundingBox | undefined { 157 let enc = new TextEncoder(); 158 let bytes = enc.encode(id); 159 new Uint8Array(memory.buffer).set(bytes, opsBuf); 160 let out = opsBuf + 256; 161 let found = ct.get_element_bounds(opsBuf, bytes.length, out); 162 if (!found) { 163 return undefined; 164 } 165 let view = new DataView(memory.buffer); 166 return { 167 x: view.getFloat32(out + BOUNDING_BOX.x, true), 168 y: view.getFloat32(out + BOUNDING_BOX.y, true), 169 width: view.getFloat32(out + BOUNDING_BOX.width, true), 170 height: view.getFloat32(out + BOUNDING_BOX.height, true), 171 }; 172 }, 173 errorCount(ptr: number): number { 174 return ct.error_count(ptr); 175 }, 176 errorType(ptr: number, index: number): number { 177 return ct.error_type(ptr, index); 178 }, 179 errorMessage(ptr: number, index: number): string { 180 let len = ct.error_message_length(ptr, index); 181 if (len === 0) return ""; 182 let p = ct.error_message_ptr(ptr, index); 183 let decoder = new TextDecoder(); 184 return decoder.decode(new Uint8Array(memory.buffer, p, len)); 185 }, 186 }; 187}