···23232424### Examples
25252626-The application in this demo uses Clayterm for all layout and input parsing
2626+See this keyboard example and more in the [examples folder](examples/README.md).
2727+This demo uses Clayterm for all layout and input parsing.
27282829#### Keyboard Events
2930
+22-2
examples/README.md
···66information that could be pertinent to reproducing the issue.
7788> [!NOTE]
99-> Run the commands in this document from the repository root.
99+> Run the commands in this document from the repository root. These examples use
1010+> `node:` terminal APIs so the same files can be run with either Deno or Node.
10111112## Prerequisites
1213···24252526```sh
2627deno run examples/keyboard/index.ts
2828+# or
2929+node examples/keyboard/index.ts
2730```
28312932What it shows:
···4043 events
4144- `examples/keyboard/use-stdin.ts` adapts stdin into a byte stream for the demo
42454646+#### Keyboard Events
4747+4848+The input parser decodes raw terminal bytes into structured events. Here you can
4949+see each key event as the string "hello world" is typed.
5050+5151+
5252+5353+#### Pointer Events
5454+5555+Here we see hover styles applied to UI elements in response to the pointer
5656+state. Clay drives the hit testing; no manual coordinate math required.
5757+5858+
5959+4360## Inline Regions
44614562Path: `examples/inline-regions/index.ts`
···48654966```sh
5067deno run examples/inline-regions/index.ts
6868+# or
6969+node examples/inline-regions/index.ts
5170```
52715372What it shows:
54735574- rendering animated regions into normal terminal scrollback
5656-- querying cursor position with DSR to place later frames correctly
7575+- querying cursor position with Device Status Report (DSR) to place later frames
7676+ correctly
5777- updating a previously allocated region without taking over the whole screen
5878- small animated demos including a spinner, a progress bar, and a nyan-cat-style
5979 sequence
+156-109
examples/inline-regions/index.ts
···33 *
44 * Shows the region lifecycle:
55 * 1. Allocate space with raw newlines
66- * 2. DSR — queries cursor position to compute `top`
66+ * 2. Device Status Report (DSR) — queries cursor position to compute `top`
77 * 3. CUP mode (all frames) — renders at `top`
88 * 4. Commit — restore cursor past region, advance with \n
99 */
10101111-import { main, type Operation, sleep, until } from "effection";
1111+import { Buffer } from "node:buffer";
1212+import { readSync } from "node:fs";
1313+import process from "node:process";
1414+import { ensure, main, type Operation, sleep, until } from "effection";
1215import {
1316 close,
1417 createInput,
···2932import { validated } from "../../validate.ts";
30333134const encode = (s: string) => new TextEncoder().encode(s);
3232-const write = (b: Uint8Array) => Deno.stdout.writeSync(b);
3535+const write = (b: Uint8Array) => process.stdout.write(Buffer.from(b));
33363437const GREEN = rgba(80, 250, 123);
3538const GRAY = rgba(100, 100, 100);
···45484649const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
47504848-function* queryCursor(): Operation<CursorEvent> {
4949- let parser = yield* until(createInput({ escLatency: 100 }));
5050- write(DSR());
5151-5252- let buf = new Uint8Array(32);
5353- while (true) {
5454- let n = Deno.stdin.readSync(buf);
5555- if (n === null) continue;
5656- let result = parser.scan(buf.subarray(0, n));
5757- for (let ev of result.events) {
5858- if (ev.type === "cursor") {
5959- return ev;
6060- }
6161- }
6262- }
6363-}
6464-6565-function waitKey() {
6666- let buf = new Uint8Array(32);
6767- while (true) {
6868- let n = Deno.stdin.readSync(buf);
6969- if (n === null) continue;
7070- for (let i = 0; i < n; i++) {
7171- if (buf[i] === 0x03) {
7272- Deno.stdin.setRaw(false);
7373- write(SHOWCURSOR());
7474- Deno.exit(0);
7575- }
7676- }
7777- return;
7878- }
7979-}
8080-8181-function box(msg: string, fg: number, border: number): Op[] {
8282- return [
8383- open("root", {
8484- layout: { width: grow(), height: grow(), direction: "ttb" },
8585- }),
8686- open("box", {
8787- layout: {
8888- width: grow(),
8989- height: grow(),
9090- direction: "ttb",
9191- padding: { left: 1 },
9292- alignY: 2,
9393- },
9494- border: {
9595- color: border,
9696- left: 1,
9797- right: 1,
9898- top: 1,
9999- bottom: 1,
100100- },
101101- cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
102102- }),
103103- text(msg, { color: fg }),
104104- close(),
105105- close(),
106106- ];
107107-}
108108-109109-function* transaction(
110110- height: number,
111111- renderFrame: (frame: number) => Op[],
112112- frames: number,
113113- interval: number,
114114-): Operation<void> {
115115- let { columns } = Deno.consoleSize();
116116-117117- write(encode("\n".repeat(height)));
118118-119119- let pos = yield* queryCursor();
120120- /** 1-based terminal row where the region starts */
121121- let row = pos.row - height + 1;
122122-123123- write(ESC("7"));
5151+await main(function* () {
5252+ let { columns } = terminalSize();
5353+ setRawMode(true);
12454 let tty = settings(cursor(false));
12555 write(tty.apply);
12656127127- let term = validated(
128128- yield* until(createTerm({ width: columns, height })),
129129- );
130130- for (let i = 0; i < frames; i++) {
131131- let result = term.render(renderFrame(i), { row });
132132- write(new Uint8Array(result.output));
133133- yield* sleep(interval);
134134- }
135135-136136- write(tty.revert);
137137- write(ESC("8"));
138138- write(encode("\n"));
139139-}
140140-141141-function say(msg: string) {
142142- write(encode(msg + "\n"));
143143-}
144144-145145-function pause() {
146146- waitKey();
147147- write(encode("\n"));
148148-}
149149-150150-await main(function* () {
151151- let { columns } = Deno.consoleSize();
152152- Deno.stdin.setRaw(true);
153153- let tty = settings(cursor(false));
154154- write(tty.apply);
5757+ yield* ensure(() => {
5858+ // SGR reset sequence
5959+ setRawMode(false);
6060+ write(CSI("0m"));
6161+ write(tty.revert);
6262+ });
1556315664 // Introduction
15765 say("Clayterm can render entire scenes, but it can also render");
···338246339247 write(CSI("0m"));
340248 write(encode("\n"));
249249+});
250250+251251+function terminalSize(): { columns: number; rows: number } {
252252+ return process.stdout.isTTY
253253+ ? {
254254+ columns: process.stdout.columns ?? 80,
255255+ rows: process.stdout.rows ?? 24,
256256+ }
257257+ : { columns: 80, rows: 24 };
258258+}
259259+260260+function setRawMode(enabled: boolean): void {
261261+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
262262+ process.stdin.setRawMode(enabled);
263263+ }
264264+}
265265+266266+function* queryCursor(): Operation<CursorEvent> {
267267+ let parser = yield* until(createInput({ escLatency: 100 }));
268268+ write(DSR());
269269+270270+ let buf = Buffer.allocUnsafe(32);
271271+ while (true) {
272272+ let n: number;
273273+ try {
274274+ n = readSync(process.stdin.fd, buf, 0, buf.length, null);
275275+ } catch (error) {
276276+ if (
277277+ error && typeof error === "object" &&
278278+ ("code" in error && (error.code === "EAGAIN" || error.code === "EINTR"))
279279+ ) {
280280+ continue;
281281+ }
282282+ throw error;
283283+ }
284284+285285+ if (n === 0) continue;
286286+ let result = parser.scan(buf.subarray(0, n));
287287+ for (let ev of result.events) {
288288+ if (ev.type === "cursor") {
289289+ return ev;
290290+ }
291291+ }
292292+ }
293293+}
294294+295295+function waitKey(): void {
296296+ let buf = Buffer.allocUnsafe(32);
297297+ while (true) {
298298+ let n: number;
299299+ try {
300300+ n = readSync(process.stdin.fd, buf, 0, buf.length, null);
301301+ } catch (error) {
302302+ if (
303303+ error && typeof error === "object" &&
304304+ ("code" in error && (error.code === "EAGAIN" || error.code === "EINTR"))
305305+ ) {
306306+ continue;
307307+ }
308308+ throw error;
309309+ }
310310+311311+ if (n === 0) continue;
312312+ for (let i = 0; i < n; i++) {
313313+ if (buf[i] === 0x03) {
314314+ setRawMode(false);
315315+ write(SHOWCURSOR());
316316+ process.exit(0);
317317+ }
318318+ }
319319+ return;
320320+ }
321321+}
322322+323323+function box(msg: string, fg: number, border: number): Op[] {
324324+ return [
325325+ open("root", {
326326+ layout: { width: grow(), height: grow(), direction: "ttb" },
327327+ }),
328328+ open("box", {
329329+ layout: {
330330+ width: grow(),
331331+ height: grow(),
332332+ direction: "ttb",
333333+ padding: { left: 1 },
334334+ alignY: 2,
335335+ },
336336+ border: {
337337+ color: border,
338338+ left: 1,
339339+ right: 1,
340340+ top: 1,
341341+ bottom: 1,
342342+ },
343343+ cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
344344+ }),
345345+ text(msg, { color: fg }),
346346+ close(),
347347+ close(),
348348+ ];
349349+}
350350+351351+function* transaction(
352352+ height: number,
353353+ renderFrame: (frame: number) => Op[],
354354+ frames: number,
355355+ interval: number,
356356+): Operation<void> {
357357+ let { columns } = terminalSize();
358358+359359+ write(encode("\n".repeat(height)));
360360+361361+ let pos = yield* queryCursor();
362362+ /** 1-based terminal row where the region starts */
363363+ let row = pos.row - height + 1;
364364+365365+ write(ESC("7"));
366366+ let tty = settings(cursor(false));
367367+ write(tty.apply);
368368+369369+ let term = validated(
370370+ yield* until(createTerm({ width: columns, height })),
371371+ );
372372+ for (let i = 0; i < frames; i++) {
373373+ let result = term.render(renderFrame(i), { row });
374374+ write(new Uint8Array(result.output));
375375+ yield* sleep(interval);
376376+ }
377377+341378 write(tty.revert);
342342- Deno.stdin.setRaw(false);
343343-});
379379+ write(ESC("8"));
380380+ write(encode("\n"));
381381+}
382382+383383+function say(msg: string) {
384384+ write(encode(msg + "\n"));
385385+}
386386+387387+function pause(): void {
388388+ waitKey();
389389+ write(encode("\n"));
390390+}
···88 type Stream,
99 until,
1010} from "effection";
1111+import process from "node:process";
11121213export function useStdin(): Operation<Stream<Uint8Array, void>> {
1314 return resource(function* (provide) {
1415 let channel = createChannel<Uint8Array, void>();
1515-1616- let iterator = Deno.stdin.readable[Symbol.asyncIterator]();
1616+ let iterator = process.stdin.iterator() as AsyncIterator<Uint8Array>;
17171818 yield* spawn(function* () {
1919 let next = yield* until(iterator.next());