[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
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}