@valyrianjs/terminal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +71 -0
  2. package/dist/ansi.d.ts +6 -0
  3. package/dist/ansi.d.ts.map +1 -0
  4. package/dist/ansi.js +105 -0
  5. package/dist/ansi.js.map +1 -0
  6. package/dist/clipboard.d.ts +3 -0
  7. package/dist/clipboard.d.ts.map +1 -0
  8. package/dist/clipboard.js +71 -0
  9. package/dist/clipboard.js.map +1 -0
  10. package/dist/events.d.ts +62 -0
  11. package/dist/events.d.ts.map +1 -0
  12. package/dist/events.js +189 -0
  13. package/dist/events.js.map +1 -0
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/layout.d.ts +16 -0
  19. package/dist/layout.d.ts.map +1 -0
  20. package/dist/layout.js +153 -0
  21. package/dist/layout.js.map +1 -0
  22. package/dist/mouse.d.ts +5 -0
  23. package/dist/mouse.d.ts.map +1 -0
  24. package/dist/mouse.js +34 -0
  25. package/dist/mouse.js.map +1 -0
  26. package/dist/primitives.d.ts +15 -0
  27. package/dist/primitives.d.ts.map +1 -0
  28. package/dist/primitives.js +18 -0
  29. package/dist/primitives.js.map +1 -0
  30. package/dist/render.d.ts +5 -0
  31. package/dist/render.d.ts.map +1 -0
  32. package/dist/render.js +230 -0
  33. package/dist/render.js.map +1 -0
  34. package/dist/session.d.ts +3 -0
  35. package/dist/session.d.ts.map +1 -0
  36. package/dist/session.js +599 -0
  37. package/dist/session.js.map +1 -0
  38. package/dist/tree.d.ts +9 -0
  39. package/dist/tree.d.ts.map +1 -0
  40. package/dist/tree.js +103 -0
  41. package/dist/tree.js.map +1 -0
  42. package/dist/types.d.ts +224 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/api-reference.md +273 -0
  47. package/docs/cookbook.md +286 -0
  48. package/docs/core-concepts.md +93 -0
  49. package/docs/getting-started.md +148 -0
  50. package/docs/interaction-model.md +115 -0
  51. package/docs/local-demo.md +28 -0
  52. package/docs/session-runtime.md +451 -0
  53. package/package.json +44 -0
  54. package/tsconfig.build.json +15 -0
  55. package/tsconfig.json +25 -0
@@ -0,0 +1,451 @@
1
+ # Session Runtime
2
+
3
+ Esta guia documenta el runtime interactivo de `valyrianjs-terminal`: cuando conviene usar `mountTerminal()`, como configurar una sesion y que capacidades expone `TerminalSession` durante su ciclo de vida.
4
+
5
+ Si vienes desde cero, lee antes `docs/core-concepts.md`. Para el modelo general de interaccion por primitiva, consulta `docs/interaction-model.md`. Aqui el foco es operativo: montaje, rerender, streams, foco, teclado, clipboard y mouse.
6
+
7
+ ## Cuando usar `mountTerminal()`
8
+
9
+ Usa `mountTerminal()` cuando necesites una sesion viva que:
10
+
11
+ - reevalua el arbol al cambiar estado externo
12
+ - mantiene foco entre renders
13
+ - despacha teclado por sesion
14
+ - conecta `stdin` y `stdout`
15
+ - responde a coordenadas y eventos de mouse
16
+
17
+ Si solo quieres inspeccionar layout, generar snapshots o producir una salida estatica, `renderTerminal()` sigue siendo la opcion mas simple.
18
+
19
+ ```tsx
20
+ /** @jsx v */
21
+ /** @jsxFrag v.fragment */
22
+
23
+ import { v } from "valyrian.js";
24
+ import { Input, Screen, Text, mountTerminal } from "valyrianjs-terminal";
25
+
26
+ const state = { value: "" };
27
+
28
+ const session = mountTerminal(() => (
29
+ <Screen title="Runtime Demo">
30
+ <Input
31
+ id="name"
32
+ value={state.value}
33
+ onchange={(event) => {
34
+ state.value = event.value;
35
+ }}
36
+ />
37
+ <Text>Current: {state.value || "(empty)"}</Text>
38
+ </Screen>
39
+ ));
40
+
41
+ session.focus("name");
42
+ session.dispatchKey("A");
43
+ session.dispatchKey("B");
44
+
45
+ console.log(session.output());
46
+ ```
47
+
48
+ ## `TerminalMountOptions`
49
+
50
+ `mountTerminal(input, options?)` acepta un `TerminalMountOptions` con cuatro puntos de integracion:
51
+
52
+ ### `ansi?: boolean`
53
+
54
+ Activa salida ANSI incremental hacia `stdout`. `session.ansiOutput()` sigue disponible aunque no actives esta opcion; lo que cambia es la escritura automatica de la sesion cuando hace rerender. Si no lo activas, la salida principal enviada a `stdout` es texto plano.
55
+
56
+ ### `clipboard?: TerminalClipboardAdapter | false`
57
+
58
+ - si pasas un adapter, la sesion usa `readText()` y `writeText()` cuando ejecuta atajos como `CTRL_C` o `CTRL_V`
59
+ - si pasas `false`, desactivas la integracion con clipboard del sistema
60
+ - aun con `clipboard: false`, `session.clipboard()` y `session.setClipboard()` siguen funcionando como buffer de la sesion
61
+
62
+ ```tsx
63
+ /** @jsx v */
64
+ /** @jsxFrag v.fragment */
65
+
66
+ import { v } from "valyrian.js";
67
+ import { Screen, mountTerminal } from "valyrianjs-terminal";
68
+
69
+ const app = () => <Screen title="Clipboard Demo" />;
70
+
71
+ const session = mountTerminal(app, {
72
+ clipboard: {
73
+ readText() {
74
+ return "pasted value";
75
+ },
76
+ writeText(value) {
77
+ console.log("copied:", value);
78
+ }
79
+ }
80
+ });
81
+ ```
82
+
83
+ ```tsx
84
+ /** @jsx v */
85
+ /** @jsxFrag v.fragment */
86
+
87
+ import { v } from "valyrian.js";
88
+ import { Screen, mountTerminal } from "valyrianjs-terminal";
89
+
90
+ const app = () => <Screen title="Clipboard Disabled" />;
91
+
92
+ const session = mountTerminal(app, { clipboard: false });
93
+ session.setClipboard("fallback value");
94
+ ```
95
+
96
+ ### `stdin?`
97
+
98
+ Stream de entrada para teclado y mouse. La sesion se suscribe a `data`, intenta activar `raw mode` si existe `setRawMode(true)` y llama `resume()` si el stream lo soporta.
99
+
100
+ Contrato esperado:
101
+
102
+ - `on("data", listener)` obligatorio
103
+ - `off(...)` o `removeListener(...)` opcional para cleanup
104
+ - `setRawMode?(boolean)` opcional
105
+ - `resume?()` y `pause?()` opcionales
106
+
107
+ ### `stdout?`
108
+
109
+ Destino de escritura para la salida producida en cada rerender.
110
+
111
+ - con `ansi: false`, escribe texto plano
112
+ - con `ansi: true`, escribe diffs ANSI incrementales
113
+
114
+ ```tsx
115
+ /** @jsx v */
116
+ /** @jsxFrag v.fragment */
117
+
118
+ import { v } from "valyrian.js";
119
+ import { Screen, mountTerminal } from "valyrianjs-terminal";
120
+
121
+ const app = () => <Screen title="ANSI Stream Demo" />;
122
+
123
+ const writes: string[] = [];
124
+
125
+ const session = mountTerminal(app, {
126
+ ansi: true,
127
+ stdout: {
128
+ write(chunk) {
129
+ writes.push(String(chunk));
130
+ }
131
+ }
132
+ });
133
+ ```
134
+
135
+ ## Lifecycle de `TerminalSession`
136
+
137
+ La sesion mantiene un frame actual, una salida actual y el estado interactivo asociado al arbol enfocable. El ciclo practico es:
138
+
139
+ 1. montar con `mountTerminal()`
140
+ 2. leer con `output()` o `ansiOutput()`
141
+ 3. mutar estado externo o despachar interacciones
142
+ 4. rerender con `update()` o mediante eventos que ya disparan rerender
143
+ 5. cerrar con `destroy()`
144
+
145
+ ### `update()`
146
+
147
+ Vuelve a evaluar `input`, recalcula el frame y devuelve la salida plana actual.
148
+
149
+ Usalo cuando mutaste estado fuera de handlers de la sesion.
150
+
151
+ ```tsx
152
+ const state = { count: 0 };
153
+
154
+ const session = mountTerminal(() => (
155
+ <Screen>
156
+ <Text>Count: {state.count}</Text>
157
+ </Screen>
158
+ ));
159
+
160
+ state.count += 1;
161
+ session.update();
162
+
163
+ console.log(session.output());
164
+ ```
165
+
166
+ ### `output()`
167
+
168
+ Devuelve el frame actual en texto plano. Es la forma mas directa de:
169
+
170
+ - inspeccionar estado actual
171
+ - validar snapshots
172
+ - probar foco y contenido sin ANSI
173
+
174
+ ### `ansiOutput()`
175
+
176
+ Devuelve el frame actual serializado como ANSI completo, incluyendo cursor y spans visuales. Es util cuando quieres:
177
+
178
+ - inspeccionar posicion de cursor
179
+ - validar seleccion o foco en una terminal ANSI
180
+ - integrar una capa que consume escape sequences completas
181
+
182
+ ```tsx
183
+ /** @jsx v */
184
+ /** @jsxFrag v.fragment */
185
+
186
+ import { v } from "valyrian.js";
187
+ import { Screen, mountTerminal } from "valyrianjs-terminal";
188
+
189
+ const app = () => <Screen title="ANSI Frame Demo" />;
190
+
191
+ const session = mountTerminal(app, { ansi: true });
192
+ const ansi = session.ansiOutput();
193
+ console.log(ansi);
194
+ ```
195
+
196
+ ### `destroy()`
197
+
198
+ Desmonta listeners de `stdin`, intenta salir de raw mode con `setRawMode(false)` y llama `pause()` si existe.
199
+
200
+ Llamalo siempre cuando montaste la sesion con `stdin` real o un emisor conectado.
201
+
202
+ ```ts
203
+ session.destroy();
204
+ ```
205
+
206
+ ## Foco y coordenadas
207
+
208
+ El foco vive a nivel de sesion. Para participar en este flujo, los nodos interactivos necesitan `id`.
209
+
210
+ ### `focus(id)`
211
+
212
+ Enfoca un nodo por `id`. Devuelve `true` si lo encontro.
213
+
214
+ ### `focusNext()` y `focusPrev()`
215
+
216
+ Recorren el orden actual de elementos enfocables. `dispatchKey("TAB")` y `dispatchKey("SHIFT_TAB")` usan este mismo flujo.
217
+
218
+ ### `focusAt(x, y)`
219
+
220
+ Busca el hitbox en las coordenadas actuales, actualiza hover semantico cuando aplica y enfoca ese nodo.
221
+
222
+ ### `clickAt(x, y)`
223
+
224
+ - si cae sobre un `Button`, dispara su accion
225
+ - si cae sobre un `Input`, enfoca y coloca cursor segun la coordenada
226
+ - si cae sobre otra superficie enfocable, mueve el foco
227
+
228
+ ```tsx
229
+ const session = mountTerminal(() => (
230
+ <Screen>
231
+ <Input id="name" value="abc" />
232
+ <Button id="save">Save</Button>
233
+ </Screen>
234
+ ));
235
+
236
+ session.focusAt(2, 1);
237
+ session.clickAt(3, 2);
238
+ ```
239
+
240
+ ## Keyboard dispatch por sesion
241
+
242
+ `dispatchKey(key)` procesa una tecla ya normalizada contra el nodo enfocado y devuelve la salida plana actual.
243
+
244
+ Atajos soportados por la API publica observables en pruebas:
245
+
246
+ - globales: `TAB`, `SHIFT_TAB`
247
+ - `Input`: caracteres de un solo byte, `ENTER`, `LEFT`, `RIGHT`, `SHIFT_LEFT`, `SHIFT_RIGHT`, `ALT_LEFT`, `ALT_RIGHT`, `HOME`, `END`, `CTRL_A`, `CTRL_C`, `CTRL_X`, `CTRL_V`, `BACKSPACE`, `DELETE`
248
+ - `Button`: `ENTER`, `SPACE`
249
+ - `List`: `UP`, `DOWN`, `LEFT`, `RIGHT`, `ENTER`
250
+ - `ScrollView`: `UP`, `DOWN`
251
+
252
+ ```tsx
253
+ const state = { value: "", saved: "" };
254
+
255
+ const session = mountTerminal(() => (
256
+ <Screen>
257
+ <Input
258
+ id="name"
259
+ value={state.value}
260
+ onchange={(event) => {
261
+ state.value = event.value;
262
+ }}
263
+ onsubmit={(event) => {
264
+ state.saved = event.value;
265
+ }}
266
+ />
267
+ </Screen>
268
+ ));
269
+
270
+ session.focus("name");
271
+ session.dispatchKey("A");
272
+ session.dispatchKey("B");
273
+ session.dispatchKey("LEFT");
274
+ session.dispatchKey("C");
275
+ session.dispatchKey("ENTER");
276
+ ```
277
+
278
+ ## Clipboard adapter y `clipboard: false`
279
+
280
+ La sesion separa dos conceptos:
281
+
282
+ - buffer de clipboard de la sesion: `clipboard()` y `setClipboard()`
283
+ - integracion con el sistema: `options.clipboard`
284
+
285
+ Con un adapter, los atajos de input leen y escriben contra ese adapter. Sin adapter, la sesion conserva un valor local. Con `clipboard: false`, desactivas la integracion externa pero puedes seguir usando el buffer local de la sesion para pruebas o integraciones manuales.
286
+
287
+ ```tsx
288
+ const state = { value: "abcd" };
289
+
290
+ const session = mountTerminal(() => (
291
+ <Screen>
292
+ <Input
293
+ id="name"
294
+ value={state.value}
295
+ onchange={(event) => {
296
+ state.value = event.value;
297
+ }}
298
+ />
299
+ </Screen>
300
+ ), { clipboard: false });
301
+
302
+ session.focus("name");
303
+ session.setClipboard("XY");
304
+ session.dispatchKey("END");
305
+ session.dispatchKey("CTRL_V");
306
+ ```
307
+
308
+ ## Streams `stdin` y `stdout`
309
+
310
+ Conectar streams vuelve a la sesion util como runtime de CLI, no solo como helper de pruebas.
311
+
312
+ Comportamiento observable:
313
+
314
+ - al montar, la sesion se suscribe a `stdin.on("data", listener)`
315
+ - si `stdin.setRawMode` existe, intenta `setRawMode(true)`
316
+ - si `stdin.resume` existe, intenta `resume()`
317
+ - en cada rerender, si existe `stdout.write`, la sesion escribe el frame actual
318
+ - al destruir, intenta desuscribirse y restaurar `raw mode`
319
+
320
+ ```tsx
321
+ /** @jsx v */
322
+ /** @jsxFrag v.fragment */
323
+
324
+ import { EventEmitter } from "node:events";
325
+ import { v } from "valyrian.js";
326
+ import { Screen, mountTerminal } from "valyrianjs-terminal";
327
+
328
+ const app = () => <Screen title="Stream Demo" />;
329
+
330
+ const stdin = new EventEmitter() as EventEmitter & {
331
+ setRawMode?: (value: boolean) => void;
332
+ resume?: () => void;
333
+ pause?: () => void;
334
+ };
335
+
336
+ const writes: string[] = [];
337
+
338
+ const session = mountTerminal(app, {
339
+ stdin,
340
+ stdout: {
341
+ write(chunk) {
342
+ writes.push(String(chunk));
343
+ }
344
+ }
345
+ });
346
+
347
+ stdin.emit("data", "A");
348
+ stdin.emit("data", "\r");
349
+ session.destroy();
350
+ ```
351
+
352
+ ## ANSI output
353
+
354
+ Hay dos formas practicas de trabajar con ANSI:
355
+
356
+ - `ansi: true` + `stdout`: la sesion emite diffs incrementales en cada rerender
357
+ - `session.ansiOutput()`: recupera el frame ANSI completo del estado actual
358
+
359
+ Esto es especialmente util cuando la terminal consumidora necesita minimizar repaints o cuando quieres verificar cursor, foco y seleccion con escape sequences reales.
360
+
361
+ ```tsx
362
+ const state = { value: "AB" };
363
+
364
+ const session = mountTerminal(() => (
365
+ <Screen title="ANSI Demo">
366
+ <Input
367
+ id="name"
368
+ value={state.value}
369
+ onchange={(event) => {
370
+ state.value = event.value;
371
+ }}
372
+ />
373
+ </Screen>
374
+ ), { ansi: true });
375
+
376
+ session.focus("name");
377
+ session.dispatchKey("LEFT");
378
+
379
+ console.log(session.ansiOutput());
380
+ ```
381
+
382
+ ## Mouse SGR, wheel y pointer capture
383
+
384
+ Cuando conectas `stdin`, la sesion acepta secuencias SGR de mouse y las traduce a interaccion practica sobre el frame actual.
385
+
386
+ ### Press, drag y release
387
+
388
+ - `press`: enfoca y activa el hitbox en la coordenada
389
+ - `drag`: si hay un `Input` activo por mouse, extiende seleccion; en `List` y `ScrollView`, actualiza hover por fila
390
+ - `release`: termina seleccion por mouse y cierra captura cuando aplica
391
+
392
+ ```ts
393
+ stdin.emit("data", "\u001b[<0;4;1M");
394
+ stdin.emit("data", "\u001b[<32;7;1M");
395
+ stdin.emit("data", "\u001b[<0;7;1m");
396
+ ```
397
+
398
+ Ese patron es suficiente para seleccionar texto dentro de un `Input` por coordenadas.
399
+
400
+ ### Wheel
401
+
402
+ El wheel se despacha como navegacion vertical sobre el nodo bajo el puntero:
403
+
404
+ - `wheel-up` equivale a `dispatchKey("UP")`
405
+ - `wheel-down` equivale a `dispatchKey("DOWN")`
406
+
407
+ ```ts
408
+ stdin.emit("data", "\u001b[<65;2;1M");
409
+ ```
410
+
411
+ En un `ScrollView` enfocado o localizado por coordenadas, eso desplaza el viewport.
412
+
413
+ ### Pointer capture a nivel practico
414
+
415
+ `pointerCapture` hoy aplica a `List` y `ScrollView`.
416
+
417
+ Sin `pointerCapture`, el hover semantico depende de que el drag siga dentro del hitbox visible. Con `pointerCapture`, la sesion mantiene la interaccion durante el drag aunque el puntero salga del area inicial, y dispara:
418
+
419
+ - `oncapturestart`
420
+ - `oncaptureend`
421
+
422
+ Esto es util para:
423
+
424
+ - listas que deben seguir rastreando la fila activa durante drag
425
+ - scroll views que conservan hover o release aunque el puntero termine fuera del viewport
426
+
427
+ ```tsx
428
+ const session = mountTerminal(() => (
429
+ <Screen>
430
+ <List
431
+ id="menu"
432
+ pointerCapture
433
+ items={["Open", "Save", "Exit"]}
434
+ oncapturestart={(event) => console.log(event)}
435
+ oncaptureend={(event) => console.log(event)}
436
+ />
437
+ </Screen>
438
+ ), { stdin, clipboard: false });
439
+
440
+ stdin.emit("data", "\u001b[<0;2;1M");
441
+ stdin.emit("data", "\u001b[<32;99;99M");
442
+ stdin.emit("data", "\u001b[<0;99;99m");
443
+ ```
444
+
445
+ ## Recomendaciones practicas
446
+
447
+ - usa `renderTerminal()` para snapshots y `mountTerminal()` para runtime interactivo
448
+ - da `id` estables a `Input`, `Button`, `List` y `ScrollView` si vas a usar foco, coordenadas o dispatch programatico
449
+ - llama `update()` solo cuando mutaste estado fuera de handlers ya conectados a la sesion
450
+ - llama `destroy()` siempre que montes con `stdin`
451
+ - usa `clipboard: false` en pruebas o entornos donde no quieres depender del clipboard del sistema
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@valyrianjs/terminal",
3
+ "version": "0.1.0",
4
+ "description": "Terminal adapter for valyrian.js",
5
+ "license": "Apache-2.0",
6
+ "private": false,
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./render": {
18
+ "types": "./dist/render.d.ts",
19
+ "import": "./dist/render.js",
20
+ "default": "./dist/render.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "docs",
26
+ "README.md",
27
+ "tsconfig.json",
28
+ "tsconfig.build.json"
29
+ ],
30
+ "scripts": {
31
+ "test": "bun test",
32
+ "typecheck": "bunx tsc -p tsconfig.json --noEmit",
33
+ "build": "rm -rf dist && bunx tsc -p tsconfig.build.json"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.3.3",
37
+ "bun-types": "latest",
38
+ "typescript": "^5.9.3",
39
+ "valyrian.js": "^9.1.3"
40
+ },
41
+ "peerDependencies": {
42
+ "valyrian.js": "^9.1.3"
43
+ }
44
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "allowImportingTsExtensions": false,
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "emitDeclarationOnly": false,
12
+ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
13
+ },
14
+ "include": ["src"]
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["esnext", "dom"],
4
+ "module": "NodeNext",
5
+ "target": "ESNext",
6
+ "moduleResolution": "NodeNext",
7
+ "noEmit": true,
8
+ "moduleDetection": "force",
9
+ "jsx": "react",
10
+ "jsxFactory": "v",
11
+ "jsxFragmentFactory": "v.fragment",
12
+ "allowJs": true,
13
+ "esModuleInterop": true,
14
+ "strict": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "skipLibCheck": true,
17
+ "types": ["bun-types", "node"],
18
+ "downlevelIteration": true,
19
+ "allowSyntheticDefaultImports": true,
20
+ "allowImportingTsExtensions": true,
21
+ "resolveJsonModule": true,
22
+ "baseUrl": "."
23
+ },
24
+ "include": ["src", "test", "examples"]
25
+ }