Skip to content

Commit 9f838c1

Browse files
committed
♻️ make row a 1-based render option matching DSR native format
Move the row offset from a constructor parameter to a render-time option. Row is now 1-based (matching ECMA-48 DSR/CPR format) so callers can pass the queried cursor position directly without conversion. Remove line mode from the inline region demo in favor of raw newline allocation followed by CUP rendering for all frames.
1 parent 7e27e62 commit 9f838c1

7 files changed

Lines changed: 396 additions & 26 deletions

File tree

demo/inline-region.out.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[step 1] line mode render
2+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠋ Compiling modules...","color":-7607811},{"id":4},{"id":4}]
3+
[step 2] DSR query
4+
cursor at top=52, region top=50
5+
[step 3] save cursor, start CUP renders
6+
[step 4] CUP frame 1
7+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠙ Compiling modules...","color":-7607811},{"id":4},{"id":4}]
8+
[step 5] CUP frame 2
9+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠹ Compiling modules...","color":-7607811},{"id":4},{"id":4}]
10+
[step 6] CUP frame 3
11+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠸ Compiling modules...","color":-7607811},{"id":4},{"id":4}]
12+
[step 7] CUP frame 4
13+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠼ Compiling modules...","color":-7607811},{"id":4},{"id":4}]
14+
[step 8] CUP frame 5
15+
ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-11470213,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"✓ Compiling modules...","color":-11470213},{"id":4},{"id":4}]
16+
[step 9] commit — restore cursor, advance

demo/inline-region.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/**
2+
* Inline region demo — renders animated regions into normal scrollback.
3+
*
4+
* Shows the region lifecycle:
5+
* 1. Allocate space with raw newlines
6+
* 2. DSR — queries cursor position to compute `top`
7+
* 3. CUP mode (all frames) — renders at `top`
8+
* 4. Commit — restore cursor past region, advance with \n
9+
*/
10+
11+
import { main, type Operation, sleep, until } from "effection";
12+
import {
13+
close,
14+
createInput,
15+
createTerm,
16+
type CursorEvent,
17+
fixed,
18+
grow,
19+
type Op,
20+
open,
21+
rgba,
22+
text,
23+
} from "../mod.ts";
24+
import { cursor, settings } from "../settings.ts";
25+
import { validated } from "../validate.ts";
26+
27+
let write = (b: Uint8Array) => Deno.stdout.writeSync(b);
28+
let encode = (s: string) => new TextEncoder().encode(s);
29+
let esc = (s: string) => write(encode(s));
30+
31+
let GREEN = rgba(80, 250, 123);
32+
let GRAY = rgba(100, 100, 100);
33+
let CYAN = rgba(139, 233, 253);
34+
35+
let RED = rgba(255, 0, 0);
36+
let ORANGE = rgba(255, 153, 0);
37+
let YELLOW = rgba(255, 255, 0);
38+
let NGREEN = rgba(51, 255, 0);
39+
let BLUE = rgba(0, 153, 255);
40+
let VIOLET = rgba(102, 0, 255);
41+
let RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET];
42+
43+
let BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
44+
45+
function* queryCursor(): Operation<CursorEvent> {
46+
let parser = yield* until(createInput({ escLatency: 100 }));
47+
esc("\x1b[6n");
48+
49+
let buf = new Uint8Array(32);
50+
while (true) {
51+
let n = Deno.stdin.readSync(buf);
52+
if (n === null) continue;
53+
let result = parser.scan(buf.subarray(0, n));
54+
for (let ev of result.events) {
55+
if (ev.type === "cursor") {
56+
return ev;
57+
}
58+
}
59+
}
60+
}
61+
62+
function waitKey() {
63+
let buf = new Uint8Array(32);
64+
while (true) {
65+
let n = Deno.stdin.readSync(buf);
66+
if (n === null) continue;
67+
for (let i = 0; i < n; i++) {
68+
if (buf[i] === 0x03) {
69+
Deno.stdin.setRaw(false);
70+
esc("\x1b[?25h");
71+
Deno.exit(0);
72+
}
73+
}
74+
return;
75+
}
76+
}
77+
78+
function box(msg: string, fg: number, border: number): Op[] {
79+
return [
80+
open("root", {
81+
layout: { width: grow(), height: grow(), direction: "ttb" },
82+
}),
83+
open("box", {
84+
layout: {
85+
width: grow(),
86+
height: grow(),
87+
direction: "ttb",
88+
padding: { left: 1 },
89+
alignY: 2,
90+
},
91+
border: {
92+
color: border,
93+
left: 1,
94+
right: 1,
95+
top: 1,
96+
bottom: 1,
97+
},
98+
cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
99+
}),
100+
text(msg, { color: fg }),
101+
close(),
102+
close(),
103+
];
104+
}
105+
106+
function* transaction(
107+
height: number,
108+
renderFrame: (frame: number) => Op[],
109+
frames: number,
110+
interval: number,
111+
): Operation<void> {
112+
let { columns } = Deno.consoleSize();
113+
114+
esc("\n".repeat(height));
115+
116+
let pos = yield* queryCursor();
117+
/** 1-based terminal row where the region starts */
118+
let row = pos.top - height + 1;
119+
120+
esc("\x1b[s");
121+
let tty = settings(cursor(false));
122+
write(tty.apply);
123+
124+
let term = validated(
125+
yield* until(createTerm({ width: columns, height })),
126+
);
127+
for (let i = 0; i < frames; i++) {
128+
let result = term.render(renderFrame(i), { row });
129+
write(new Uint8Array(result.output));
130+
yield* sleep(interval);
131+
}
132+
133+
write(tty.revert);
134+
esc("\x1b[u");
135+
esc("\n");
136+
}
137+
138+
function say(msg: string) {
139+
esc(msg + "\n");
140+
}
141+
142+
function pause() {
143+
waitKey();
144+
esc("\n");
145+
}
146+
147+
await main(function* () {
148+
let { columns } = Deno.consoleSize();
149+
Deno.stdin.setRaw(true);
150+
let tty = settings(cursor(false));
151+
write(tty.apply);
152+
153+
// Introduction
154+
say("Clayterm can render entire scenes, but it can also render");
155+
say('"inline" for a streaming UI. This is useful for semi-interactive');
156+
say("CLI commands that write output to the normal console screen.");
157+
say("");
158+
159+
// Demo 1: Spinner box
160+
esc("\n\n\n");
161+
162+
let pos = yield* queryCursor();
163+
/** 1-based terminal row where the region starts */
164+
let row = pos.top - 2;
165+
166+
esc("\x1b[s");
167+
168+
let frames = 30;
169+
let term = validated(
170+
yield* until(createTerm({ width: columns, height: 3 })),
171+
);
172+
173+
let first = term.render(
174+
box("Press any key to compile modules.", CYAN, GRAY),
175+
{ row },
176+
);
177+
write(new Uint8Array(first.output));
178+
179+
waitKey();
180+
181+
for (let i = 0; i < frames; i++) {
182+
let done = i === frames - 1;
183+
let icon = done ? "✓" : BRAILLE[i % BRAILLE.length];
184+
let time = `${((i + 1) * 0.08).toFixed(1)}s`;
185+
let label = done ? "Compiled modules" : "Compiling modules...";
186+
let result = term.render(
187+
box(
188+
`${icon} ${label} ${time}`,
189+
done ? GREEN : CYAN,
190+
done ? GREEN : GRAY,
191+
),
192+
{ row },
193+
);
194+
write(new Uint8Array(result.output));
195+
yield* sleep(80);
196+
}
197+
198+
esc("\x1b[u");
199+
esc("\x1b[0m");
200+
esc("\n");
201+
202+
yield* sleep(500);
203+
204+
esc(
205+
"\nRegions can be multi-line, but they can be a single line too. (continue...)",
206+
);
207+
pause();
208+
209+
// Demo 2: Progress bar
210+
let barWidth = Math.min(columns, 50);
211+
let barFrames = 40;
212+
yield* transaction(
213+
1,
214+
(i) => {
215+
let done = i === barFrames - 1;
216+
if (done) {
217+
return [
218+
open("root", {
219+
layout: {
220+
width: fixed(barWidth),
221+
height: fixed(1),
222+
direction: "ltr",
223+
},
224+
}),
225+
text("✓ Frobnicated", { color: GREEN }),
226+
close(),
227+
];
228+
}
229+
let progress = i / (barFrames - 1);
230+
let label = "Frobnicating.. ";
231+
let remaining = barWidth - label.length - 5;
232+
let filled = Math.round(remaining * Math.min(progress, 1));
233+
let empty = remaining - filled;
234+
let pct = `${Math.round(progress * 100)}%`;
235+
let bar = "█".repeat(filled) + "░".repeat(empty);
236+
return [
237+
open("root", {
238+
layout: {
239+
width: fixed(barWidth),
240+
height: fixed(1),
241+
direction: "ltr",
242+
},
243+
}),
244+
text(label, { color: CYAN }),
245+
text(bar, { color: CYAN }),
246+
text(` ${pct.padStart(4)}`, { color: GRAY }),
247+
close(),
248+
];
249+
},
250+
barFrames,
251+
50,
252+
);
253+
254+
esc("\x1b[0m");
255+
yield* sleep(500);
256+
esc("\nGoodbye sadness with limitless sky. (continue...)");
257+
pause();
258+
259+
// Demo 3: Nyan cat
260+
let nyanWidth = Math.min(columns, 120);
261+
let nyanFrames = 50;
262+
let cat = [
263+
"╭─────╮",
264+
"│ ^.^ │",
265+
"╰─────╯",
266+
];
267+
let catWidth = cat[0].length;
268+
269+
yield* transaction(
270+
3,
271+
(i) => {
272+
let done = i === nyanFrames - 1;
273+
let progress = i / (nyanFrames - 1);
274+
let trail = Math.round((nyanWidth - catWidth) * Math.min(progress, 1));
275+
276+
if (done) {
277+
// "IMAGINATION IS BEAUTIFUL WORLD!" in 3-row block font
278+
let font: string[] = [
279+
"█ █▄█▄█ █▀█ █▀▀ █ █▀█ █▀█ ▀█▀ █ █▀█ █▀█ █ █▀▀ ██▄ █▀▀ █▀█ █ █ ▀█▀ █ █▀▀ █ █ █ █ █ █ █▀█ █▀█ █ █▀▄ █",
280+
"█ █ ▀ █ █▀█ █ █ █ █ █ █▀█ █ █ █ █ █ █ █ ▀▀█ █▀█ █▀▀ █▀█ █ █ █ █ █▀ █ █ █ █▄█▄█ █ █ █▀▄ █ █ █ ▀",
281+
"▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀ █",
282+
];
283+
let ops: Op[] = [
284+
open("root", {
285+
layout: {
286+
width: fixed(nyanWidth),
287+
height: fixed(3),
288+
direction: "ttb",
289+
},
290+
}),
291+
];
292+
for (let row = 0; row < 3; row++) {
293+
let color = RAINBOW[(row * 2) % RAINBOW.length];
294+
ops.push(text(font[row], { color }));
295+
}
296+
ops.push(close());
297+
return ops;
298+
}
299+
300+
let ops: Op[] = [
301+
open("root", {
302+
layout: {
303+
width: fixed(nyanWidth),
304+
height: fixed(3),
305+
direction: "ttb",
306+
},
307+
}),
308+
];
309+
310+
for (let row = 0; row < 3; row++) {
311+
ops.push(
312+
open(`row${row}`, {
313+
layout: { width: grow(), height: fixed(1), direction: "ltr" },
314+
}),
315+
);
316+
317+
if (trail > 0) {
318+
let color = RAINBOW[(row * 2 + i) % RAINBOW.length];
319+
ops.push(text("█".repeat(trail), { color }));
320+
}
321+
322+
ops.push(text(cat[row], { color: CYAN }));
323+
324+
ops.push(close());
325+
}
326+
327+
ops.push(close());
328+
return ops;
329+
},
330+
nyanFrames,
331+
60,
332+
);
333+
334+
esc("\x1b[0m\n");
335+
write(tty.revert);
336+
Deno.stdin.setRaw(false);
337+
});

0 commit comments

Comments
 (0)