[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.ts
3.9 kB 158 lines
1import { type Op, pack } from "./ops.ts"; 2import { type BoundingBox, createTermNative } from "./term-native.ts"; 3 4export interface TermOptions { 5 height: number; 6 width: number; 7} 8 9export interface RenderOptions { 10 mode?: "line"; 11 12 /** 13 * Row where to begin rendering. This should only be used when 14 * rendering into a region as part of the CLI main screen. For 15 * interfaces that use the entire screen, leave unset which will 16 * default to 0. This is 1-based which which is the DSR native 17 * format. 18 * 19 * https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ 20 */ 21 row?: number; 22 23 pointer?: { 24 x: number; 25 y: number; 26 down: boolean; 27 }; 28} 29 30export type PointerEvent = 31 | { type: "pointerenter"; id: string } 32 | { type: "pointerleave"; id: string } 33 | { type: "pointerclick"; id: string }; 34 35export type { BoundingBox }; 36 37export interface ElementInfo { 38 bounds: BoundingBox; 39} 40 41const ERROR_TYPES = [ 42 "TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED", 43 "ARENA_CAPACITY_EXCEEDED", 44 "ELEMENTS_CAPACITY_EXCEEDED", 45 "TEXT_MEASUREMENT_CAPACITY_EXCEEDED", 46 "DUPLICATE_ID", 47 "FLOATING_CONTAINER_PARENT_NOT_FOUND", 48 "PERCENTAGE_OVER_1", 49 "INTERNAL_ERROR", 50 "UNBALANCED_OPEN_CLOSE", 51] as const; 52 53export interface ClayError { 54 type: string; 55 message: string; 56} 57 58export interface RenderInfo { 59 get(id: string): ElementInfo | undefined; 60} 61 62export interface RenderResult { 63 output: Uint8Array; 64 events: PointerEvent[]; 65 info: RenderInfo; 66 errors: ClayError[]; 67} 68 69export interface Term { 70 render(ops: Op[], options?: RenderOptions): RenderResult; 71} 72 73export async function createTerm(options: TermOptions): Promise<Term> { 74 let { width, height } = options; 75 let native = await createTermNative(width, height); 76 let { memory, statePtr, opsBuf } = native; 77 78 let prev = new Set<string>(); 79 let pressed = new Set<string>(); 80 let wasDown = false; 81 82 return { 83 render(ops: Op[], options?: RenderOptions): RenderResult { 84 let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength); 85 let mode = options?.mode === "line" ? 1 : 0; 86 let row = options?.row ?? 1; 87 native.reduce(statePtr, opsBuf, len, mode, row); 88 89 if (options?.pointer) { 90 let { x, y, down } = options.pointer; 91 native.setPointer(x, y, down); 92 } 93 94 let output = new Uint8Array( 95 memory.buffer, 96 native.output(statePtr), 97 native.length(statePtr), 98 ); 99 100 let current = new Set( 101 options?.pointer ? native.getPointerOverIds() : [], 102 ); 103 let down = options?.pointer?.down ?? false; 104 let events: PointerEvent[] = []; 105 106 for (let id of current) { 107 if (!prev.has(id)) { 108 events.push({ type: "pointerenter", id }); 109 } 110 } 111 112 for (let id of prev) { 113 if (!current.has(id)) { 114 events.push({ type: "pointerleave", id }); 115 } 116 } 117 118 if (wasDown && !down) { 119 for (let id of pressed) { 120 if (current.has(id)) { 121 events.push({ type: "pointerclick", id }); 122 } 123 } 124 } 125 126 if (down && !wasDown) { 127 pressed = new Set(current); 128 } else if (!down) { 129 pressed.clear(); 130 } 131 132 prev = current; 133 wasDown = down; 134 135 let info: RenderInfo = { 136 get(id: string): ElementInfo | undefined { 137 let bounds = native.getElementBounds(id); 138 if (bounds) { 139 return { bounds }; 140 } 141 return undefined; 142 }, 143 }; 144 145 let errors: ClayError[] = []; 146 let count = native.errorCount(statePtr); 147 for (let i = 0; i < count; i++) { 148 let code = native.errorType(statePtr, i); 149 errors.push({ 150 type: ERROR_TYPES[code] ?? `UNKNOWN_${code}`, 151 message: native.errorMessage(statePtr, i), 152 }); 153 } 154 155 return { output, events, info, errors }; 156 }, 157 }; 158}