Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 343 additions & 0 deletions demo/inline-region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
/**
* Inline region demo — renders animated regions into normal scrollback.
*
* Shows the region lifecycle:
* 1. Allocate space with raw newlines
* 2. DSR — queries cursor position to compute `top`
* 3. CUP mode (all frames) — renders at `top`
* 4. Commit — restore cursor past region, advance with \n
*/

import { main, type Operation, sleep, until } from "effection";
import {
close,
createInput,
createTerm,
CSI,
type CursorEvent,
DSR,
ESC,
fixed,
grow,
type Op,
open,
rgba,
SHOWCURSOR,
text,
} from "../mod.ts";
import { cursor, settings } from "../settings.ts";
import { validated } from "../validate.ts";

const encode = (s: string) => new TextEncoder().encode(s);
const write = (b: Uint8Array) => Deno.stdout.writeSync(b);

const GREEN = rgba(80, 250, 123);
const GRAY = rgba(100, 100, 100);
const CYAN = rgba(139, 233, 253);

const RED = rgba(255, 0, 0);
const ORANGE = rgba(255, 153, 0);
const YELLOW = rgba(255, 255, 0);
const NGREEN = rgba(51, 255, 0);
const BLUE = rgba(0, 153, 255);
const VIOLET = rgba(102, 0, 255);
const RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET];

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

function* queryCursor(): Operation<CursorEvent> {
let parser = yield* until(createInput({ escLatency: 100 }));
write(DSR());

let buf = new Uint8Array(32);
while (true) {
let n = Deno.stdin.readSync(buf);
if (n === null) continue;
let result = parser.scan(buf.subarray(0, n));
for (let ev of result.events) {
if (ev.type === "cursor") {
return ev;
}
}
}
}

function waitKey() {
let buf = new Uint8Array(32);
while (true) {
let n = Deno.stdin.readSync(buf);
if (n === null) continue;
for (let i = 0; i < n; i++) {
if (buf[i] === 0x03) {
Deno.stdin.setRaw(false);
write(SHOWCURSOR());
Deno.exit(0);
}
}
return;
}
}

function box(msg: string, fg: number, border: number): Op[] {
return [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
open("box", {
layout: {
width: grow(),
height: grow(),
direction: "ttb",
padding: { left: 1 },
alignY: 2,
},
border: {
color: border,
left: 1,
right: 1,
top: 1,
bottom: 1,
},
cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
}),
text(msg, { color: fg }),
close(),
close(),
];
}

function* transaction(
height: number,
renderFrame: (frame: number) => Op[],
frames: number,
interval: number,
): Operation<void> {
let { columns } = Deno.consoleSize();

write(encode("\n".repeat(height)));

let pos = yield* queryCursor();
/** 1-based terminal row where the region starts */
let row = pos.row - height + 1;

write(ESC("7"));
let tty = settings(cursor(false));
write(tty.apply);

let term = validated(
yield* until(createTerm({ width: columns, height })),
);
for (let i = 0; i < frames; i++) {
let result = term.render(renderFrame(i), { row });
write(new Uint8Array(result.output));
yield* sleep(interval);
}

write(tty.revert);
write(ESC("8"));
write(encode("\n"));
}

function say(msg: string) {
write(encode(msg + "\n"));
}

function pause() {
waitKey();
write(encode("\n"));
}

await main(function* () {
let { columns } = Deno.consoleSize();
Deno.stdin.setRaw(true);
let tty = settings(cursor(false));
write(tty.apply);

// Introduction
say("Clayterm can render entire scenes, but it can also render");
say('"inline" for a streaming UI. This is useful for semi-interactive');
say("CLI commands that write output to the normal console screen.");
say("");

// Demo 1: Spinner box
write(encode("\n\n\n"));

let pos = yield* queryCursor();
/** 1-based terminal row where the region starts */
let row = pos.row - 2;

write(ESC("7"));

let frames = 30;
let term = validated(
yield* until(createTerm({ width: columns, height: 3 })),
);

let first = term.render(
box("Press any key to compile modules.", CYAN, GRAY),
{ row },
);
write(new Uint8Array(first.output));

waitKey();

for (let i = 0; i < frames; i++) {
let done = i === frames - 1;
let icon = done ? "✓" : BRAILLE[i % BRAILLE.length];
let time = `${((i + 1) * 0.08).toFixed(1)}s`;
let label = done ? "Compiled modules" : "Compiling modules...";
let result = term.render(
box(
`${icon} ${label} ${time}`,
done ? GREEN : CYAN,
done ? GREEN : GRAY,
),
{ row },
);
write(new Uint8Array(result.output));
yield* sleep(80);
}

write(ESC("8"));
write(CSI("0m"));
write(encode("\n"));

yield* sleep(500);

write(
encode(
"\nRegions can be multi-line, but they can be a single line too. (continue...)",
),
);
pause();

// Demo 2: Progress bar
let barWidth = Math.min(columns, 50);
let barFrames = 40;
yield* transaction(
1,
(i) => {
let done = i === barFrames - 1;
if (done) {
return [
open("root", {
layout: {
width: fixed(barWidth),
height: fixed(1),
direction: "ltr",
},
}),
text("✓ Frobnicated", { color: GREEN }),
close(),
];
}
let progress = i / (barFrames - 1);
let label = "Frobnicating.. ";
let remaining = barWidth - label.length - 5;
let filled = Math.round(remaining * Math.min(progress, 1));
let empty = remaining - filled;
let pct = `${Math.round(progress * 100)}%`;
let bar = "█".repeat(filled) + "░".repeat(empty);
return [
open("root", {
layout: {
width: fixed(barWidth),
height: fixed(1),
direction: "ltr",
},
}),
text(label, { color: CYAN }),
text(bar, { color: CYAN }),
text(` ${pct.padStart(4)}`, { color: GRAY }),
close(),
];
},
barFrames,
50,
);

write(CSI("0m"));
yield* sleep(500);
write(encode("\nGoodbye sadness with limitless sky. (continue...)"));
pause();

// Demo 3: Nyan cat
let nyanWidth = Math.min(columns, 120);
let nyanFrames = 50;
let cat = [
"╭─────╮",
"│ ^.^ │",
"╰─────╯",
];
let catWidth = cat[0].length;

yield* transaction(
3,
(i) => {
let done = i === nyanFrames - 1;
let progress = i / (nyanFrames - 1);
let trail = Math.round((nyanWidth - catWidth) * Math.min(progress, 1));

if (done) {
// "IMAGINATION IS BEAUTIFUL WORLD!" in 3-row block font
let font: string[] = [
"█ █▄█▄█ █▀█ █▀▀ █ █▀█ █▀█ ▀█▀ █ █▀█ █▀█ █ █▀▀ ██▄ █▀▀ █▀█ █ █ ▀█▀ █ █▀▀ █ █ █ █ █ █ █▀█ █▀█ █ █▀▄ █",
"█ █ ▀ █ █▀█ █ █ █ █ █ █▀█ █ █ █ █ █ █ █ ▀▀█ █▀█ █▀▀ █▀█ █ █ █ █ █▀ █ █ █ █▄█▄█ █ █ █▀▄ █ █ █ ▀",
"▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀ █",
];
let ops: Op[] = [
open("root", {
layout: {
width: fixed(nyanWidth),
height: fixed(3),
direction: "ttb",
},
}),
];
for (let row = 0; row < 3; row++) {
let color = RAINBOW[(row * 2) % RAINBOW.length];
ops.push(text(font[row], { color }));
}
ops.push(close());
return ops;
}

let ops: Op[] = [
open("root", {
layout: {
width: fixed(nyanWidth),
height: fixed(3),
direction: "ttb",
},
}),
];

for (let row = 0; row < 3; row++) {
ops.push(
open(`row${row}`, {
layout: { width: grow(), height: fixed(1), direction: "ltr" },
}),
);

if (trail > 0) {
let color = RAINBOW[(row * 2 + i) % RAINBOW.length];
ops.push(text("█".repeat(trail), { color }));
}

ops.push(text(cat[row], { color: CYAN }));

ops.push(close());
}

ops.push(close());
return ops;
},
nyanFrames,
60,
);

write(CSI("0m"));
write(encode("\n"));
write(tty.revert);
Deno.stdin.setRaw(false);
});
22 changes: 11 additions & 11 deletions demo/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import {
import { useInput } from "./use-input.ts";
import { useStdin } from "./use-stdin.ts";

let active = rgba(60, 120, 220);
let inactive = rgba(50, 50, 60);
let on = rgba(40, 180, 80);
let label = rgba(220, 220, 220);
let dim = rgba(100, 100, 120);
let highlight = rgba(255, 220, 80);
const active = rgba(60, 120, 220);
const inactive = rgba(50, 50, 60);
const on = rgba(40, 180, 80);
const label = rgba(220, 220, 220);
const dim = rgba(100, 100, 120);
const highlight = rgba(255, 220, 80);

let KEY_W = 5;
let GAP = 1;
const KEY_W = 5;
const GAP = 1;

interface KeyDef {
label: string;
Expand All @@ -58,7 +58,7 @@ function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean {
event.code.toUpperCase() === k.code.toUpperCase();
}

let hovered = rgba(80, 80, 100);
const hovered = rgba(80, 80, 100);

function key(ops: Op[], k: KeyDef, ctx: AppContext): void {
let pressed = ctx.event && matches(k, ctx.event);
Expand Down Expand Up @@ -328,7 +328,7 @@ function toggle(ops: Op[], enabled: boolean, name: string): void {
);
}

let flagNames:
const flagNames:
(keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] =
[
"Disambiguate escape codes",
Expand All @@ -338,7 +338,7 @@ let flagNames:
"Report associated text",
];

let logEntries: { key: string; name: keyof EventFilter }[] = [
const logEntries: { key: string; name: keyof EventFilter }[] = [
{ key: "a", name: "keydown" },
{ key: "b", name: "keyup" },
{ key: "c", name: "keyrepeat" },
Expand Down
Loading
Loading