Skip to content

Commit 07ea00b

Browse files
authored
examples use node: api (#48)
* examples use `node:` api * move main to top * direct to examples folder * fmt * fix const ordering * fmt again
1 parent 4139212 commit 07ea00b

5 files changed

Lines changed: 338 additions & 224 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ for consumers.
2323

2424
### Examples
2525

26-
The application in this demo uses Clayterm for all layout and input parsing
26+
See this keyboard example and more in the [examples folder](examples/README.md).
27+
This demo uses Clayterm for all layout and input parsing.
2728

2829
#### Keyboard Events
2930

examples/README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ with information about your terminal, shell, operating system and any other
66
information that could be pertinent to reproducing the issue.
77

88
> [!NOTE]
9-
> Run the commands in this document from the repository root.
9+
> Run the commands in this document from the repository root. These examples use
10+
> `node:` terminal APIs so the same files can be run with either Deno or Node.
1011
1112
## Prerequisites
1213

@@ -24,6 +25,8 @@ Run it with:
2425

2526
```sh
2627
deno run examples/keyboard/index.ts
28+
# or
29+
node examples/keyboard/index.ts
2730
```
2831

2932
What it shows:
@@ -40,6 +43,20 @@ Related files:
4043
events
4144
- `examples/keyboard/use-stdin.ts` adapts stdin into a byte stream for the demo
4245

46+
#### Keyboard Events
47+
48+
The input parser decodes raw terminal bytes into structured events. Here you can
49+
see each key event as the string "hello world" is typed.
50+
51+
![Keyboard events demo](keyboard/keyboard-key-events.gif)
52+
53+
#### Pointer Events
54+
55+
Here we see hover styles applied to UI elements in response to the pointer
56+
state. Clay drives the hit testing; no manual coordinate math required.
57+
58+
![Pointer events demo](keyboard/keyboard-pointer-events.gif)
59+
4360
## Inline Regions
4461

4562
Path: `examples/inline-regions/index.ts`
@@ -48,12 +65,15 @@ Run it with:
4865

4966
```sh
5067
deno run examples/inline-regions/index.ts
68+
# or
69+
node examples/inline-regions/index.ts
5170
```
5271

5372
What it shows:
5473

5574
- rendering animated regions into normal terminal scrollback
56-
- querying cursor position with DSR to place later frames correctly
75+
- querying cursor position with Device Status Report (DSR) to place later frames
76+
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

examples/inline-regions/index.ts

Lines changed: 156 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
*
44
* Shows the region lifecycle:
55
* 1. Allocate space with raw newlines
6-
* 2. DSR — queries cursor position to compute `top`
6+
* 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
*/
1010

11-
import { main, type Operation, sleep, until } from "effection";
11+
import { Buffer } from "node:buffer";
12+
import { readSync } from "node:fs";
13+
import process from "node:process";
14+
import { ensure, main, type Operation, sleep, until } from "effection";
1215
import {
1316
close,
1417
createInput,
@@ -29,7 +32,7 @@ import { cursor, settings } from "../../settings.ts";
2932
import { validated } from "../../validate.ts";
3033

3134
const encode = (s: string) => new TextEncoder().encode(s);
32-
const write = (b: Uint8Array) => Deno.stdout.writeSync(b);
35+
const write = (b: Uint8Array) => process.stdout.write(Buffer.from(b));
3336

3437
const GREEN = rgba(80, 250, 123);
3538
const GRAY = rgba(100, 100, 100);
@@ -45,114 +48,19 @@ const RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET];
4548

4649
const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4750

48-
function* queryCursor(): Operation<CursorEvent> {
49-
let parser = yield* until(createInput({ escLatency: 100 }));
50-
write(DSR());
51-
52-
let buf = new Uint8Array(32);
53-
while (true) {
54-
let n = Deno.stdin.readSync(buf);
55-
if (n === null) continue;
56-
let result = parser.scan(buf.subarray(0, n));
57-
for (let ev of result.events) {
58-
if (ev.type === "cursor") {
59-
return ev;
60-
}
61-
}
62-
}
63-
}
64-
65-
function waitKey() {
66-
let buf = new Uint8Array(32);
67-
while (true) {
68-
let n = Deno.stdin.readSync(buf);
69-
if (n === null) continue;
70-
for (let i = 0; i < n; i++) {
71-
if (buf[i] === 0x03) {
72-
Deno.stdin.setRaw(false);
73-
write(SHOWCURSOR());
74-
Deno.exit(0);
75-
}
76-
}
77-
return;
78-
}
79-
}
80-
81-
function box(msg: string, fg: number, border: number): Op[] {
82-
return [
83-
open("root", {
84-
layout: { width: grow(), height: grow(), direction: "ttb" },
85-
}),
86-
open("box", {
87-
layout: {
88-
width: grow(),
89-
height: grow(),
90-
direction: "ttb",
91-
padding: { left: 1 },
92-
alignY: 2,
93-
},
94-
border: {
95-
color: border,
96-
left: 1,
97-
right: 1,
98-
top: 1,
99-
bottom: 1,
100-
},
101-
cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
102-
}),
103-
text(msg, { color: fg }),
104-
close(),
105-
close(),
106-
];
107-
}
108-
109-
function* transaction(
110-
height: number,
111-
renderFrame: (frame: number) => Op[],
112-
frames: number,
113-
interval: number,
114-
): Operation<void> {
115-
let { columns } = Deno.consoleSize();
116-
117-
write(encode("\n".repeat(height)));
118-
119-
let pos = yield* queryCursor();
120-
/** 1-based terminal row where the region starts */
121-
let row = pos.row - height + 1;
122-
123-
write(ESC("7"));
124-
let tty = settings(cursor(false));
125-
write(tty.apply);
126-
127-
let term = validated(
128-
yield* until(createTerm({ width: columns, height })),
129-
);
130-
for (let i = 0; i < frames; i++) {
131-
let result = term.render(renderFrame(i), { row });
132-
write(new Uint8Array(result.output));
133-
yield* sleep(interval);
134-
}
135-
136-
write(tty.revert);
137-
write(ESC("8"));
138-
write(encode("\n"));
139-
}
140-
141-
function say(msg: string) {
142-
write(encode(msg + "\n"));
143-
}
144-
145-
function pause() {
146-
waitKey();
147-
write(encode("\n"));
148-
}
149-
15051
await main(function* () {
151-
let { columns } = Deno.consoleSize();
152-
Deno.stdin.setRaw(true);
52+
let { columns } = terminalSize();
53+
setRawMode(true);
15354
let tty = settings(cursor(false));
15455
write(tty.apply);
15556

57+
yield* ensure(() => {
58+
// SGR reset sequence
59+
setRawMode(false);
60+
write(CSI("0m"));
61+
write(tty.revert);
62+
});
63+
15664
// Introduction
15765
say("Clayterm can render entire scenes, but it can also render");
15866
say('"inline" for a streaming UI. This is useful for semi-interactive');
@@ -338,6 +246,145 @@ await main(function* () {
338246

339247
write(CSI("0m"));
340248
write(encode("\n"));
341-
write(tty.revert);
342-
Deno.stdin.setRaw(false);
343249
});
250+
251+
function terminalSize(): { columns: number; rows: number } {
252+
return process.stdout.isTTY
253+
? {
254+
columns: process.stdout.columns ?? 80,
255+
rows: process.stdout.rows ?? 24,
256+
}
257+
: { columns: 80, rows: 24 };
258+
}
259+
260+
function setRawMode(enabled: boolean): void {
261+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
262+
process.stdin.setRawMode(enabled);
263+
}
264+
}
265+
266+
function* queryCursor(): Operation<CursorEvent> {
267+
let parser = yield* until(createInput({ escLatency: 100 }));
268+
write(DSR());
269+
270+
let buf = Buffer.allocUnsafe(32);
271+
while (true) {
272+
let n: number;
273+
try {
274+
n = readSync(process.stdin.fd, buf, 0, buf.length, null);
275+
} catch (error) {
276+
if (
277+
error && typeof error === "object" &&
278+
("code" in error && (error.code === "EAGAIN" || error.code === "EINTR"))
279+
) {
280+
continue;
281+
}
282+
throw error;
283+
}
284+
285+
if (n === 0) continue;
286+
let result = parser.scan(buf.subarray(0, n));
287+
for (let ev of result.events) {
288+
if (ev.type === "cursor") {
289+
return ev;
290+
}
291+
}
292+
}
293+
}
294+
295+
function waitKey(): void {
296+
let buf = Buffer.allocUnsafe(32);
297+
while (true) {
298+
let n: number;
299+
try {
300+
n = readSync(process.stdin.fd, buf, 0, buf.length, null);
301+
} catch (error) {
302+
if (
303+
error && typeof error === "object" &&
304+
("code" in error && (error.code === "EAGAIN" || error.code === "EINTR"))
305+
) {
306+
continue;
307+
}
308+
throw error;
309+
}
310+
311+
if (n === 0) continue;
312+
for (let i = 0; i < n; i++) {
313+
if (buf[i] === 0x03) {
314+
setRawMode(false);
315+
write(SHOWCURSOR());
316+
process.exit(0);
317+
}
318+
}
319+
return;
320+
}
321+
}
322+
323+
function box(msg: string, fg: number, border: number): Op[] {
324+
return [
325+
open("root", {
326+
layout: { width: grow(), height: grow(), direction: "ttb" },
327+
}),
328+
open("box", {
329+
layout: {
330+
width: grow(),
331+
height: grow(),
332+
direction: "ttb",
333+
padding: { left: 1 },
334+
alignY: 2,
335+
},
336+
border: {
337+
color: border,
338+
left: 1,
339+
right: 1,
340+
top: 1,
341+
bottom: 1,
342+
},
343+
cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
344+
}),
345+
text(msg, { color: fg }),
346+
close(),
347+
close(),
348+
];
349+
}
350+
351+
function* transaction(
352+
height: number,
353+
renderFrame: (frame: number) => Op[],
354+
frames: number,
355+
interval: number,
356+
): Operation<void> {
357+
let { columns } = terminalSize();
358+
359+
write(encode("\n".repeat(height)));
360+
361+
let pos = yield* queryCursor();
362+
/** 1-based terminal row where the region starts */
363+
let row = pos.row - height + 1;
364+
365+
write(ESC("7"));
366+
let tty = settings(cursor(false));
367+
write(tty.apply);
368+
369+
let term = validated(
370+
yield* until(createTerm({ width: columns, height })),
371+
);
372+
for (let i = 0; i < frames; i++) {
373+
let result = term.render(renderFrame(i), { row });
374+
write(new Uint8Array(result.output));
375+
yield* sleep(interval);
376+
}
377+
378+
write(tty.revert);
379+
write(ESC("8"));
380+
write(encode("\n"));
381+
}
382+
383+
function say(msg: string) {
384+
write(encode(msg + "\n"));
385+
}
386+
387+
function pause(): void {
388+
waitKey();
389+
write(encode("\n"));
390+
}

0 commit comments

Comments
 (0)