diff --git a/demo/keyboard-node.ts b/demo/keyboard-node.ts
new file mode 100644
index 0000000..ac9e503
--- /dev/null
+++ b/demo/keyboard-node.ts
@@ -0,0 +1,242 @@
+///
+
+import { Buffer } from "node:buffer";
+import {
+ alternateBuffer,
+ close,
+ createInput,
+ createTerm,
+ cursor,
+ fixed,
+ grow,
+ mouseTracking,
+ open,
+ progressiveInput,
+ rgba,
+ settings,
+ text,
+} from "../build/npm/esm/mod.js";
+import type { InputEvent, PointerEvent } from "../mod.ts";
+import { createKeyboardDemo } from "./keyboard-shared.ts";
+
+type PointerState = {
+ x: number;
+ y: number;
+ down: boolean;
+};
+
+type NodeInput = Awaited>;
+type NodeScanResult = {
+ events: InputEvent[];
+ pending?: { delay: number };
+};
+
+let demo = createKeyboardDemo(
+ {
+ close,
+ fixed,
+ grow,
+ mouseTracking,
+ open,
+ progressiveInput,
+ rgba,
+ settings,
+ text,
+ } as Parameters[0],
+);
+
+let term: Awaited> | null = null;
+let input: NodeInput | null = null;
+let size = getSize();
+let flushTimer: ReturnType | null = null;
+let tty = settings(alternateBuffer(), cursor(false));
+let modality = demo.recognizer();
+let context = modality.next().value;
+let flags = demo.ttyFlags(context);
+
+let pointer: { state: PointerState | undefined } = {
+ state: undefined,
+};
+
+function getSize() {
+ return {
+ width: process.stdout.columns || 80,
+ height: process.stdout.rows || 24,
+ };
+}
+
+function write(bytes: Uint8Array): void {
+ process.stdout.write(Buffer.from(bytes));
+}
+
+function scan(chunk?: Uint8Array): NodeScanResult {
+ let normalized = chunk ? new Uint8Array(chunk) : undefined;
+ return input!.scan(normalized) as NodeScanResult;
+}
+
+function resetFlushTimer(delay: number): void {
+ if (flushTimer !== null) {
+ clearTimeout(flushTimer);
+ }
+ flushTimer = setTimeout(() => {
+ flushTimer = null;
+ let result = scan();
+ for (let event of result.events) {
+ handleEvent(event);
+ }
+ }, delay);
+}
+
+function applyFlags(): void {
+ write(flags.revert);
+ flags = demo.ttyFlags(context);
+ write(flags.apply);
+}
+
+function render(): PointerEvent[] {
+ if (!term) {
+ return [];
+ }
+ let result = term.render(demo.keyboard(context), {
+ pointer: pointer.state,
+ });
+ write(result.output);
+ return result.events as PointerEvent[];
+}
+
+function dispatchLogged(event: InputEvent | PointerEvent): void {
+ let previous = context.logged;
+ context = modality.next(event).value;
+ if (
+ context.event &&
+ context.event.type in context.log &&
+ context.log[context.event.type as keyof typeof context.log]
+ ) {
+ context = { ...context, logged: context.event };
+ } else {
+ context = { ...context, logged: previous };
+ }
+}
+
+function updatePointer(event: InputEvent | PointerEvent): void {
+ if (!context["Capture mouse events"]) {
+ pointer.state = undefined;
+ return;
+ }
+ if (!("x" in event)) {
+ return;
+ }
+ pointer.state = {
+ x: event.x,
+ y: event.y,
+ down: event.type === "mousedown",
+ };
+}
+
+function handleEvent(event: InputEvent | PointerEvent): void {
+ if (event.type === "keydown" && event.ctrl && event.key === "c") {
+ cleanup();
+ process.exit(0);
+ }
+
+ if (event.type === "pointerenter") {
+ context.entered.add(event.id);
+ }
+ if (event.type === "pointerleave") {
+ context.entered.delete(event.id);
+ }
+
+ dispatchLogged(event);
+ applyFlags();
+ updatePointer(event);
+
+ let queue = render();
+ while (queue.length > 0) {
+ let next = queue.shift();
+ if (!next) {
+ continue;
+ }
+ if (next.type === "pointerenter") {
+ context.entered.add(next.id);
+ }
+ if (next.type === "pointerleave") {
+ context.entered.delete(next.id);
+ }
+ dispatchLogged(next);
+ applyFlags();
+ let emitted = render();
+ queue.push(...emitted);
+ }
+}
+
+async function resetTerm(): Promise {
+ size = getSize();
+ term = await createTerm(size);
+}
+
+function cleanup(): void {
+ if (flushTimer !== null) {
+ clearTimeout(flushTimer);
+ flushTimer = null;
+ }
+ try {
+ write(flags.revert);
+ write(tty.revert);
+ } catch {
+ // ignore cleanup write failures during shutdown
+ }
+ if (process.stdin.isTTY) {
+ process.stdin.setRawMode(false);
+ }
+ process.stdin.pause();
+}
+
+try {
+ input = await createInput();
+ await resetTerm();
+
+ write(tty.apply);
+ write(flags.apply);
+ render();
+
+ if (process.stdin.isTTY) {
+ process.stdin.setRawMode(true);
+ }
+ process.stdin.resume();
+ process.stdin.on("data", (chunk: Uint8Array) => {
+ let result = scan(new Uint8Array(chunk));
+ for (let event of result.events) {
+ handleEvent(event);
+ }
+ if (result.pending) {
+ resetFlushTimer(result.pending.delay);
+ }
+ });
+
+ if (process.stdout.isTTY) {
+ process.stdout.on("resize", async () => {
+ try {
+ await resetTerm();
+ render();
+ } catch (error) {
+ cleanup();
+ console.error(error);
+ process.exit(1);
+ }
+ });
+ }
+
+ process.on("SIGINT", () => {
+ cleanup();
+ process.exit(0);
+ });
+ process.on("SIGTERM", () => {
+ cleanup();
+ process.exit(0);
+ });
+ process.on("exit", cleanup);
+} catch (error) {
+ cleanup();
+ console.error(error);
+ process.exit(1);
+}
diff --git a/demo/keyboard-shared.ts b/demo/keyboard-shared.ts
new file mode 100644
index 0000000..b6f89de
--- /dev/null
+++ b/demo/keyboard-shared.ts
@@ -0,0 +1,656 @@
+// deno-lint-ignore-file no-fallthrough
+import type { InputEvent, KeyEvent, Op, PointerEvent } from "../mod.ts";
+import type { SizingAxis } from "../ops.ts";
+import type { Setting } from "../settings.ts";
+
+type EventFilter = {
+ keydown: boolean;
+ keyrepeat: boolean;
+ keyup: boolean;
+ mousedown: boolean;
+ mouseup: boolean;
+ mousemove: boolean;
+ wheel: boolean;
+ resize: boolean;
+ pointerenter: boolean;
+ pointerleave: boolean;
+ pointerclick: boolean;
+};
+
+type AppContext = {
+ mode: "input" | "config";
+ event: InputEvent | PointerEvent | null;
+ logged: InputEvent | PointerEvent | null;
+ log: EventFilter;
+ entered: Set;
+ ["Disambiguate escape codes"]: boolean;
+ ["Report event types"]: boolean;
+ ["Report alternate keys"]: boolean;
+ ["Report all keys as escapes"]: boolean;
+ ["Report associated text"]: boolean;
+ ["Capture mouse events"]: boolean;
+};
+
+type KeyDef = {
+ label: string;
+ code: string;
+ width?: number;
+};
+
+type KeyboardDemoApi = {
+ close(): Op;
+ fixed(value: number): SizingAxis;
+ grow(min?: number, max?: number): SizingAxis;
+ mouseTracking(): Setting;
+ open(id: string, properties: Record): Op;
+ progressiveInput(bits: number): Setting;
+ rgba(r: number, g: number, b: number, a?: number): number;
+ settings(...parts: Setting[]): Setting;
+ text(value: string, properties?: Record): Op;
+};
+
+type Mode = Iterable;
+
+export function createKeyboardDemo(api: KeyboardDemoApi) {
+ let {
+ close,
+ fixed,
+ grow,
+ mouseTracking,
+ open,
+ progressiveInput,
+ rgba,
+ settings,
+ text,
+ } = api;
+
+ 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);
+ let hovered = rgba(80, 80, 100);
+
+ let KEY_W = 5;
+ let GAP = 1;
+
+ let flagNames: (keyof Omit<
+ AppContext,
+ "mode" | "event" | "logged" | "log" | "entered"
+ >)[] = [
+ "Disambiguate escape codes",
+ "Report event types",
+ "Report alternate keys",
+ "Report all keys as escapes",
+ "Report associated text",
+ ];
+
+ let logEntries: { key: string; name: keyof EventFilter }[] = [
+ { key: "a", name: "keydown" },
+ { key: "b", name: "keyup" },
+ { key: "c", name: "keyrepeat" },
+ { key: "d", name: "mousedown" },
+ { key: "e", name: "mouseup" },
+ { key: "f", name: "mousemove" },
+ { key: "g", name: "wheel" },
+ { key: "h", name: "resize" },
+ { key: "i", name: "pointerenter" },
+ { key: "j", name: "pointerleave" },
+ { key: "k", name: "pointerclick" },
+ ];
+
+ function isKeyEvent(event: InputEvent | PointerEvent): event is KeyEvent {
+ return event.type === "keydown" || event.type === "keyrepeat" ||
+ event.type === "keyup";
+ }
+
+ function matches(keyDef: KeyDef, event: InputEvent | PointerEvent): boolean {
+ return isKeyEvent(event) && event.type === "keydown" &&
+ event.code.toUpperCase() === keyDef.code.toUpperCase();
+ }
+
+ function key(ops: Op[], keyDef: KeyDef, context: AppContext): void {
+ let pressed = context.event && matches(keyDef, context.event);
+ let hover = context.entered.has(`key:${keyDef.code}`);
+ let bg = pressed ? active : hover ? hovered : inactive;
+ let width = keyDef.width ?? KEY_W;
+ ops.push(
+ open(`key:${keyDef.code}`, {
+ layout: {
+ width: fixed(width),
+ height: grow(),
+ padding: { left: 1, right: 1 },
+ alignX: 2,
+ alignY: 2,
+ },
+ bg,
+ border: hover
+ ? { color: highlight, left: 1, right: 1, top: 1, bottom: 1 }
+ : undefined,
+ }),
+ text(keyDef.label, { color: hover ? highlight : label }),
+ close(),
+ );
+ }
+
+ function row(ops: Op[], keys: KeyDef[], context: AppContext): void {
+ ops.push(
+ open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }),
+ );
+ for (let keyDef of keys) {
+ key(ops, keyDef, context);
+ }
+ ops.push(close());
+ }
+
+ function spacer(ops: Op[], width: number): void {
+ ops.push(
+ open("", { layout: { width: fixed(width), height: grow() } }),
+ close(),
+ );
+ }
+
+ function mainKeys(ops: Op[], context: AppContext): void {
+ ops.push(open("main-keys", { layout: { direction: "ttb", gap: GAP } }));
+
+ row(ops, [
+ { label: "Esc", code: "Escape", width: 11 },
+ { label: "F1", code: "F1" },
+ { label: "F2", code: "F2" },
+ { label: "F3", code: "F3" },
+ { label: "F4", code: "F4" },
+ { label: "F5", code: "F5" },
+ { label: "F6", code: "F6" },
+ { label: "F7", code: "F7" },
+ { label: "F8", code: "F8" },
+ { label: "F9", code: "F9" },
+ { label: "F10", code: "F10" },
+ { label: "F11", code: "F11" },
+ { label: "F12", code: "F12" },
+ ], context);
+
+ row(ops, [
+ { label: "`", code: "`" },
+ { label: "1", code: "1" },
+ { label: "2", code: "2" },
+ { label: "3", code: "3" },
+ { label: "4", code: "4" },
+ { label: "5", code: "5" },
+ { label: "6", code: "6" },
+ { label: "7", code: "7" },
+ { label: "8", code: "8" },
+ { label: "9", code: "9" },
+ { label: "0", code: "0" },
+ { label: "-", code: "-" },
+ { label: "=", code: "=" },
+ { label: "Bksp", code: "Backspace", width: 9 },
+ ], context);
+
+ row(ops, [
+ { label: "Tab", code: "Tab", width: 7 },
+ { label: "Q", code: "q" },
+ { label: "W", code: "w" },
+ { label: "E", code: "e" },
+ { label: "R", code: "r" },
+ { label: "T", code: "t" },
+ { label: "Y", code: "y" },
+ { label: "U", code: "u" },
+ { label: "I", code: "i" },
+ { label: "O", code: "o" },
+ { label: "P", code: "p" },
+ { label: "[", code: "[" },
+ { label: "]", code: "]" },
+ { label: "\\", code: "\\", width: 7 },
+ ], context);
+
+ row(ops, [
+ { label: "Caps", code: "CapsLock", width: 9 },
+ { label: "A", code: "a" },
+ { label: "S", code: "s" },
+ { label: "D", code: "d" },
+ { label: "F", code: "f" },
+ { label: "G", code: "g" },
+ { label: "H", code: "h" },
+ { label: "J", code: "j" },
+ { label: "K", code: "k" },
+ { label: "L", code: "l" },
+ { label: ";", code: ";" },
+ { label: "'", code: "'" },
+ { label: "Enter", code: "Enter", width: 10 },
+ ], context);
+
+ row(ops, [
+ { label: "Shift", code: "ShiftLeft", width: 11 },
+ { label: "Z", code: "z" },
+ { label: "X", code: "x" },
+ { label: "C", code: "c" },
+ { label: "V", code: "v" },
+ { label: "B", code: "b" },
+ { label: "N", code: "n" },
+ { label: "M", code: "m" },
+ { label: ",", code: "," },
+ { label: ".", code: "." },
+ { label: "/", code: "/" },
+ { label: "Shift", code: "ShiftRight", width: 13 },
+ ], context);
+
+ row(ops, [
+ { label: "Ctrl", code: "ControlLeft", width: 7 },
+ { label: "Win", code: "SuperLeft", width: 6 },
+ { label: "Alt", code: "AltLeft", width: 6 },
+ { label: "", code: " ", width: 33 },
+ { label: "Alt", code: "AltRight", width: 6 },
+ { label: "Win", code: "SuperRight", width: 6 },
+ { label: "Menu", code: "Menu", width: 6 },
+ { label: "Ctrl", code: "ControlRight", width: 7 },
+ ], context);
+
+ ops.push(close());
+ }
+
+ function navKeys(ops: Op[], context: AppContext): void {
+ ops.push(open("nav-keys", { layout: { direction: "ttb", gap: GAP } }));
+
+ row(ops, [
+ { label: "Ins", code: "Insert", width: 6 },
+ { label: "Home", code: "Home", width: 6 },
+ { label: "PgUp", code: "PageUp", width: 6 },
+ ], context);
+
+ row(ops, [
+ { label: "Del", code: "Delete", width: 6 },
+ { label: "End", code: "End", width: 6 },
+ { label: "PgDn", code: "PageDown", width: 6 },
+ ], context);
+
+ ops.push(open("", { layout: { height: fixed(3) } }), close());
+
+ ops.push(
+ open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }),
+ );
+ spacer(ops, 6);
+ key(ops, { label: "↑", code: "ArrowUp", width: 6 }, context);
+ spacer(ops, 6);
+ ops.push(close());
+
+ row(ops, [
+ { label: "←", code: "ArrowLeft", width: 6 },
+ { label: "↓", code: "ArrowDown", width: 6 },
+ { label: "→", code: "ArrowRight", width: 6 },
+ ], context);
+
+ ops.push(close());
+ }
+
+ function numpad(ops: Op[], context: AppContext): void {
+ ops.push(open("numpad", { layout: { direction: "ttb", gap: GAP } }));
+
+ row(ops, [
+ { label: "Num", code: "NumLock", width: 6 },
+ { label: "/", code: "NumpadDivide", width: 6 },
+ { label: "*", code: "NumpadMultiply", width: 6 },
+ { label: "-", code: "NumpadSubtract", width: 6 },
+ ], context);
+
+ ops.push(open("", { layout: { direction: "ltr", gap: GAP } }));
+ ops.push(open("", { layout: { direction: "ttb", gap: GAP } }));
+ row(ops, [
+ { label: "7", code: "Numpad7", width: 6 },
+ { label: "8", code: "Numpad8", width: 6 },
+ { label: "9", code: "Numpad9", width: 6 },
+ ], context);
+ row(ops, [
+ { label: "4", code: "Numpad4", width: 6 },
+ { label: "5", code: "Numpad5", width: 6 },
+ { label: "6", code: "Numpad6", width: 6 },
+ ], context);
+ ops.push(close());
+
+ key(ops, { label: "+", code: "NumpadAdd" }, context);
+ ops.push(close());
+
+ ops.push(open("", { layout: { direction: "ltr", gap: GAP } }));
+ ops.push(open("", { layout: { direction: "ttb", gap: GAP } }));
+ row(ops, [
+ { label: "1", code: "Numpad1", width: 6 },
+ { label: "2", code: "Numpad2", width: 6 },
+ { label: "3", code: "Numpad3", width: 6 },
+ ], context);
+ row(ops, [
+ { label: "0", code: "Numpad0", width: 13 },
+ { label: ".", code: "NumpadDecimal", width: 6 },
+ ], context);
+ ops.push(close());
+
+ key(ops, { label: "Ent", code: "NumpadEnter" }, context);
+ ops.push(close());
+ ops.push(close());
+ }
+
+ function toggle(ops: Op[], enabled: boolean, name: string): void {
+ let indicator = enabled ? "●───" : "───○";
+ ops.push(
+ open("", { layout: { direction: "ltr", height: fixed(1), gap: 1 } }),
+ text(indicator, { color: enabled ? on : dim }),
+ text(name, { color: enabled ? label : dim }),
+ close(),
+ );
+ }
+
+ function logToggle(
+ ops: Op[],
+ entries: typeof logEntries,
+ context: AppContext,
+ ): void {
+ for (let entry of entries) {
+ ops.push(
+ open(`log:${entry.name}`, {
+ layout: { direction: "ltr", height: fixed(1), gap: 1 },
+ }),
+ );
+ ops.push(text(`${entry.key}.`, { color: dim }));
+ toggle(ops, context.log[entry.name], entry.name);
+ ops.push(close());
+ }
+ }
+
+ function configPanel(ops: Op[], context: AppContext): void {
+ let color = context.mode === "config" ? active : rgba(0, 0, 0, 0);
+ ops.push(open("config", {
+ layout: {
+ direction: "ltr",
+ gap: 3,
+ padding: { left: 1, right: 1, top: 1, bottom: 1 },
+ },
+ border: { color, left: 1, right: 1, top: 1, bottom: 1 },
+ }));
+
+ ops.push(open("protocol-level", { layout: { direction: "ttb", gap: 1 } }));
+ ops.push(
+ open("", { layout: { height: fixed(1) } }),
+ text("Keyboard Protocol Level", { color: highlight }),
+ close(),
+ );
+ for (let index = 0; index < flagNames.length; index++) {
+ let name = flagNames[index];
+ ops.push(
+ open(`flag:${name}`, {
+ layout: { direction: "ltr", height: fixed(1), gap: 1 },
+ }),
+ );
+ ops.push(text(`${index + 1}.`, { color: dim }));
+ toggle(ops, context[name], name);
+ ops.push(close());
+ }
+ ops.push(close());
+
+ let col1 = logEntries.slice(0, 6);
+ let col2 = logEntries.slice(6);
+
+ ops.push(open("log-events", { layout: { direction: "ttb", gap: 1 } }));
+ ops.push(
+ open("", { layout: { height: fixed(1) } }),
+ text("Log Events", { color: highlight }),
+ close(),
+ );
+ logToggle(ops, col1, context);
+ ops.push(close());
+
+ ops.push(open("log-events-2", { layout: { direction: "ttb", gap: 1 } }));
+ ops.push(open("", { layout: { height: fixed(1) } }), close());
+ logToggle(ops, col2, context);
+ ops.push(close());
+
+ ops.push(close());
+ }
+
+ function keyboard(context: AppContext): Op[] {
+ let ops: Op[] = [];
+
+ ops.push(open("root", {
+ layout: {
+ width: grow(),
+ height: grow(),
+ direction: "ttb",
+ alignX: 2,
+ alignY: 2,
+ padding: { left: 2, top: 1 },
+ },
+ }));
+
+ ops.push(open("", { layout: { direction: "ttb" } }));
+
+ ops.push(open("", {
+ layout: {
+ width: grow(),
+ direction: "ltr",
+ alignY: 0,
+ padding: { bottom: 1 },
+ },
+ }));
+
+ let badgeBg = context.mode === "input"
+ ? rgba(40, 120, 200)
+ : rgba(200, 120, 40);
+ let badgeLabel = context.mode === "input" ? "input" : "config";
+ let badgeHint = context.mode === "input"
+ ? "Ctrl+X Ctrl+X to enter config"
+ : "Set flags with keys [0-5], Enter to save";
+ let mouseBg = context["Capture mouse events"]
+ ? rgba(40, 180, 80)
+ : rgba(80, 80, 80);
+ let mouseLabel = context["Capture mouse events"] ? "capture" : "system";
+
+ ops.push(
+ open("badges", {
+ layout: { direction: "ttb", gap: 1, padding: { top: 1 } },
+ }),
+ open("badge:mode", {
+ layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } },
+ }),
+ open("", {
+ layout: { padding: { left: 1, right: 1 } },
+ bg: rgba(60, 60, 60),
+ }),
+ text("mode", { color: rgba(220, 220, 220) }),
+ close(),
+ open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }),
+ text(badgeLabel, { color: rgba(255, 255, 255) }),
+ close(),
+ text(` ${badgeHint}`, { color: dim }),
+ close(),
+ open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }),
+ open("", {
+ layout: { padding: { left: 1, right: 1 } },
+ bg: rgba(60, 60, 60),
+ }),
+ text("mouse", { color: rgba(220, 220, 220) }),
+ close(),
+ open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }),
+ text(mouseLabel, { color: rgba(255, 255, 255) }),
+ close(),
+ text(" Ctrl+X Ctrl+M to toggle", { color: dim }),
+ close(),
+ close(),
+ );
+
+ ops.push(
+ open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } }),
+ );
+ configPanel(ops, context);
+ ops.push(close());
+ ops.push(close());
+
+ let keyboardColor = context.mode === "input" ? active : rgba(0, 0, 0, 0);
+ ops.push(open("keyboard", {
+ layout: {
+ direction: "ltr",
+ gap: 3,
+ alignY: 1,
+ padding: { left: 1, right: 1, top: 1, bottom: 1 },
+ },
+ border: { color: keyboardColor, left: 1, right: 1, top: 1, bottom: 1 },
+ }));
+
+ mainKeys(ops, context);
+ navKeys(ops, context);
+ numpad(ops, context);
+
+ ops.push(close());
+ ops.push(close());
+
+ ops.push(
+ open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } }),
+ );
+ ops.push(
+ text(
+ context.logged ? JSON.stringify(context.logged) : "Press any key...",
+ {
+ color: highlight,
+ },
+ ),
+ );
+ ops.push(close());
+ ops.push(close());
+
+ return ops;
+ }
+
+ function ttyFlags(context: AppContext): Setting {
+ let parts: Setting[] = [];
+ let bits = 0;
+ if (context["Disambiguate escape codes"]) bits |= 1;
+ if (context["Report event types"]) bits |= 2;
+ if (context["Report alternate keys"]) bits |= 4;
+ if (context["Report all keys as escapes"]) bits |= 8;
+ if (context["Report associated text"]) bits |= 16;
+ parts.push(progressiveInput(bits));
+ if (context["Capture mouse events"]) {
+ parts.push(mouseTracking());
+ }
+ return settings(...parts);
+ }
+
+ function createInitialContext(): AppContext {
+ return {
+ mode: "input",
+ "Disambiguate escape codes": true,
+ "Report event types": true,
+ "Report alternate keys": true,
+ "Report all keys as escapes": true,
+ "Report associated text": true,
+ "Capture mouse events": true,
+ log: {
+ keydown: true,
+ keyrepeat: false,
+ keyup: false,
+ mousedown: false,
+ mouseup: false,
+ mousemove: false,
+ wheel: true,
+ resize: true,
+ pointerenter: false,
+ pointerleave: false,
+ pointerclick: true,
+ },
+ entered: new Set(),
+ event: null,
+ logged: null,
+ };
+ }
+
+ function* recognizer(): Iterator<
+ AppContext,
+ never,
+ InputEvent | PointerEvent
+ > {
+ let current = createInitialContext();
+ let event = yield current;
+ let mode = inputmode({ ...current, event });
+
+ while (true) {
+ mode = yield* mode;
+ }
+ }
+
+ function* inputmode(context: AppContext): Mode {
+ context = { ...context, mode: "input" };
+ let event = context.event ? context.event : yield context;
+ while (true) {
+ context = { ...context, event };
+ if (event.type === "keydown" && event.key === "x" && event.ctrl) {
+ let next = yield context;
+ while (next.type !== "keydown") {
+ context = { ...context, event: next };
+ next = yield context;
+ }
+ context = { ...context, event: next };
+ if (next.key === "x" && next.ctrl) {
+ return configmode({ ...context, event: null });
+ } else if (next.key === "m" && next.ctrl) {
+ context = {
+ ...context,
+ "Capture mouse events": !context["Capture mouse events"],
+ event: null,
+ };
+ event = yield context;
+ continue;
+ }
+ }
+ event = yield context;
+ }
+ }
+
+ function* configmode(context: AppContext): Mode {
+ context = { ...context, mode: "config" };
+ let event = yield context;
+ while (true) {
+ if (event.type === "keydown" && event.key === "Enter") {
+ return inputmode({ ...context, event: null });
+ }
+ if (event.type === "keydown") {
+ let keyEvent = event as KeyEvent;
+ let entry = logEntries.find((candidate) =>
+ candidate.key === keyEvent.key
+ );
+ if (entry) {
+ context = {
+ ...context,
+ log: { ...context.log, [entry.name]: !context.log[entry.name] },
+ };
+ }
+ if ("012345".indexOf(keyEvent.key) >= 0) {
+ context = { ...context };
+ context["Report associated text"] = false;
+ context["Report all keys as escapes"] = false;
+ context["Report alternate keys"] = false;
+ context["Report event types"] = false;
+ context["Disambiguate escape codes"] = false;
+ switch (keyEvent.key) {
+ case "5":
+ context["Report associated text"] = true;
+ case "4":
+ context["Report all keys as escapes"] = true;
+ case "3":
+ context["Report alternate keys"] = true;
+ case "2":
+ context["Report event types"] = true;
+ case "1":
+ context["Disambiguate escape codes"] = true;
+ break;
+ case "0":
+ break;
+ }
+ }
+ }
+ event = yield context;
+ }
+ }
+
+ return {
+ keyboard,
+ recognizer,
+ ttyFlags,
+ };
+}
diff --git a/demo/keyboard.ts b/demo/keyboard.ts
index 0dce0c3..ddd6fa5 100644
--- a/demo/keyboard.ts
+++ b/demo/keyboard.ts
@@ -1,4 +1,3 @@
-// deno-lint-ignore-file no-fallthrough
import {
createChannel,
each,
@@ -9,557 +8,38 @@ import {
type Stream,
until,
} from "effection";
-import {
+import { createTerm, type PointerEvent } from "../mod.ts";
+import { alternateBuffer, settings } from "../settings.ts";
+import { close, fixed, grow, open, rgba, text } from "../mod.ts";
+import { cursor, mouseTracking, progressiveInput } from "../settings.ts";
+import { createKeyboardDemo } from "./keyboard-shared.ts";
+import { useInput } from "./use-input.ts";
+import { useStdin } from "./use-stdin.ts";
+
+const demo = createKeyboardDemo({
close,
- createTerm,
fixed,
grow,
- type InputEvent,
- type KeyEvent,
- type Op,
- open,
- type PointerEvent,
- rgba,
- text,
-} from "../mod.ts";
-import {
- alternateBuffer,
- cursor,
mouseTracking,
+ open,
progressiveInput,
- type Setting,
+ rgba,
settings,
-} from "../settings.ts";
-import { useInput } from "./use-input.ts";
-import { useStdin } from "./use-stdin.ts";
-
-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);
-
-const KEY_W = 5;
-const GAP = 1;
-
-interface KeyDef {
- label: string;
- code: string;
- width?: number;
-}
-
-function isKeyEvent(e: InputEvent | PointerEvent): e is KeyEvent {
- return e.type === "keydown" || e.type === "keyrepeat" || e.type === "keyup";
-}
-
-function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean {
- return isKeyEvent(event) && event.type === "keydown" &&
- event.code.toUpperCase() === k.code.toUpperCase();
-}
-
-const hovered = rgba(80, 80, 100);
-
-function key(ops: Op[], k: KeyDef, ctx: AppContext): void {
- let pressed = ctx.event && matches(k, ctx.event);
- let hover = ctx.entered.has(`key:${k.code}`);
- let bg = pressed ? active : hover ? hovered : inactive;
- let w = k.width ?? KEY_W;
- ops.push(
- open(`key:${k.code}`, {
- layout: {
- width: fixed(w),
- height: grow(),
- padding: { left: 1, right: 1 },
- alignX: 2,
- alignY: 2,
- },
- bg,
- border: hover
- ? { color: highlight, left: 1, right: 1, top: 1, bottom: 1 }
- : undefined,
- }),
- text(k.label, { color: hover ? highlight : label }),
- close(),
- );
-}
-
-function row(ops: Op[], keys: KeyDef[], ctx: AppContext): void {
- ops.push(
- open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }),
- );
- for (let k of keys) {
- key(ops, k, ctx);
- }
- ops.push(close());
-}
-
-function spacer(ops: Op[], width: number): void {
- ops.push(
- open("", { layout: { width: fixed(width), height: grow() } }),
- close(),
- );
-}
-
-function mainKeys(ops: Op[], ctx: AppContext): void {
- ops.push(
- open("main-keys", { layout: { direction: "ttb", gap: GAP } }),
- );
-
- row(ops, [
- { label: "Esc", code: "Escape", width: 11 },
- { label: "F1", code: "F1" },
- { label: "F2", code: "F2" },
- { label: "F3", code: "F3" },
- { label: "F4", code: "F4" },
- { label: "F5", code: "F5" },
- { label: "F6", code: "F6" },
- { label: "F7", code: "F7" },
- { label: "F8", code: "F8" },
- { label: "F9", code: "F9" },
- { label: "F10", code: "F10" },
- { label: "F11", code: "F11" },
- { label: "F12", code: "F12" },
- ], ctx);
-
- row(ops, [
- { label: "`", code: "`" },
- { label: "1", code: "1" },
- { label: "2", code: "2" },
- { label: "3", code: "3" },
- { label: "4", code: "4" },
- { label: "5", code: "5" },
- { label: "6", code: "6" },
- { label: "7", code: "7" },
- { label: "8", code: "8" },
- { label: "9", code: "9" },
- { label: "0", code: "0" },
- { label: "-", code: "-" },
- { label: "=", code: "=" },
- { label: "Bksp", code: "Backspace", width: 9 },
- ], ctx);
-
- row(ops, [
- { label: "Tab", code: "Tab", width: 7 },
- { label: "Q", code: "q" },
- { label: "W", code: "w" },
- { label: "E", code: "e" },
- { label: "R", code: "r" },
- { label: "T", code: "t" },
- { label: "Y", code: "y" },
- { label: "U", code: "u" },
- { label: "I", code: "i" },
- { label: "O", code: "o" },
- { label: "P", code: "p" },
- { label: "[", code: "[" },
- { label: "]", code: "]" },
- { label: "\\", code: "\\", width: 7 },
- ], ctx);
-
- row(ops, [
- { label: "Caps", code: "CapsLock", width: 9 },
- { label: "A", code: "a" },
- { label: "S", code: "s" },
- { label: "D", code: "d" },
- { label: "F", code: "f" },
- { label: "G", code: "g" },
- { label: "H", code: "h" },
- { label: "J", code: "j" },
- { label: "K", code: "k" },
- { label: "L", code: "l" },
- { label: ";", code: ";" },
- { label: "'", code: "'" },
- { label: "Enter", code: "Enter", width: 10 },
- ], ctx);
-
- row(ops, [
- { label: "Shift", code: "ShiftLeft", width: 11 },
- { label: "Z", code: "z" },
- { label: "X", code: "x" },
- { label: "C", code: "c" },
- { label: "V", code: "v" },
- { label: "B", code: "b" },
- { label: "N", code: "n" },
- { label: "M", code: "m" },
- { label: ",", code: "," },
- { label: ".", code: "." },
- { label: "/", code: "/" },
- { label: "Shift", code: "ShiftRight", width: 13 },
- ], ctx);
-
- row(ops, [
- { label: "Ctrl", code: "ControlLeft", width: 7 },
- { label: "Win", code: "SuperLeft", width: 6 },
- { label: "Alt", code: "AltLeft", width: 6 },
- { label: "", code: " ", width: 33 },
- { label: "Alt", code: "AltRight", width: 6 },
- { label: "Win", code: "SuperRight", width: 6 },
- { label: "Menu", code: "Menu", width: 6 },
- { label: "Ctrl", code: "ControlRight", width: 7 },
- ], ctx);
-
- ops.push(close());
-}
-
-function navKeys(ops: Op[], ctx: AppContext): void {
- ops.push(
- open("nav-keys", { layout: { direction: "ttb", gap: GAP } }),
- );
-
- // top section: Ins/Home/PgUp, Del/End/PgDn
- row(ops, [
- { label: "Ins", code: "Insert", width: 6 },
- { label: "Home", code: "Home", width: 6 },
- { label: "PgUp", code: "PageUp", width: 6 },
- ], ctx);
-
- row(ops, [
- { label: "Del", code: "Delete", width: 6 },
- { label: "End", code: "End", width: 6 },
- { label: "PgDn", code: "PageDown", width: 6 },
- ], ctx);
-
- // gap before arrows
- ops.push(
- open("", { layout: { height: fixed(3) } }),
- close(),
- );
-
- // arrow up
- ops.push(
- open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }),
- );
- spacer(ops, 6);
- key(ops, { label: "\u2191", code: "ArrowUp", width: 6 }, ctx);
- spacer(ops, 6);
- ops.push(close());
-
- // arrow left/down/right
- row(ops, [
- { label: "\u2190", code: "ArrowLeft", width: 6 },
- { label: "\u2193", code: "ArrowDown", width: 6 },
- { label: "\u2192", code: "ArrowRight", width: 6 },
- ], ctx);
-
- ops.push(close());
-}
-
-function numpad(ops: Op[], ctx: AppContext): void {
- ops.push(
- open("numpad", { layout: { direction: "ttb", gap: GAP } }),
- );
-
- row(ops, [
- { label: "Num", code: "NumLock", width: 6 },
- { label: "/", code: "NumpadDivide", width: 6 },
- { label: "*", code: "NumpadMultiply", width: 6 },
- { label: "-", code: "NumpadSubtract", width: 6 },
- ], ctx);
-
- // rows 2-3 grouped horizontally so + spans both
- ops.push(
- open("", { layout: { direction: "ltr", gap: GAP } }),
- );
-
- // left side: 7-8-9 and 4-5-6 stacked
- ops.push(
- open("", { layout: { direction: "ttb", gap: GAP } }),
- );
- row(ops, [
- { label: "7", code: "Numpad7", width: 6 },
- { label: "8", code: "Numpad8", width: 6 },
- { label: "9", code: "Numpad9", width: 6 },
- ], ctx);
- row(ops, [
- { label: "4", code: "Numpad4", width: 6 },
- { label: "5", code: "Numpad5", width: 6 },
- { label: "6", code: "Numpad6", width: 6 },
- ], ctx);
- ops.push(close());
-
- // + spanning both rows
- key(ops, { label: "+", code: "NumpadAdd" }, ctx);
-
- ops.push(close());
-
- // rows 4-5 grouped horizontally so Enter spans both
- ops.push(
- open("", { layout: { direction: "ltr", gap: GAP } }),
- );
-
- // left side: 1-2-3 and 0-. stacked
- ops.push(
- open("", { layout: { direction: "ttb", gap: GAP } }),
- );
- row(ops, [
- { label: "1", code: "Numpad1", width: 6 },
- { label: "2", code: "Numpad2", width: 6 },
- { label: "3", code: "Numpad3", width: 6 },
- ], ctx);
- row(ops, [
- { label: "0", code: "Numpad0", width: 13 },
- { label: ".", code: "NumpadDecimal", width: 6 },
- ], ctx);
- ops.push(close());
-
- // Enter spanning both rows
- key(ops, { label: "Ent", code: "NumpadEnter" }, ctx);
-
- ops.push(close());
-
- ops.push(close());
-}
-
-function toggle(ops: Op[], enabled: boolean, name: string): void {
- let indicator = enabled
- ? "\u25cf\u2500\u2500\u2500"
- : "\u2500\u2500\u2500\u25cb";
- ops.push(
- open("", {
- layout: {
- direction: "ltr",
- height: fixed(1),
- gap: 1,
- },
- }),
- text(indicator, { color: enabled ? on : dim }),
- text(name, { color: enabled ? label : dim }),
- close(),
- );
-}
-
-const flagNames:
- (keyof Omit)[] =
- [
- "Disambiguate escape codes",
- "Report event types",
- "Report alternate keys",
- "Report all keys as escapes",
- "Report associated text",
- ];
-
-const logEntries: { key: string; name: keyof EventFilter }[] = [
- { key: "a", name: "keydown" },
- { key: "b", name: "keyup" },
- { key: "c", name: "keyrepeat" },
- { key: "d", name: "mousedown" },
- { key: "e", name: "mouseup" },
- { key: "f", name: "mousemove" },
- { key: "g", name: "wheel" },
- { key: "h", name: "resize" },
- { key: "i", name: "pointerenter" },
- { key: "j", name: "pointerleave" },
- { key: "k", name: "pointerclick" },
-];
-
-function logToggle(
- ops: Op[],
- entries: typeof logEntries,
- ctx: AppContext,
-): void {
- for (let entry of entries) {
- ops.push(
- open(`log:${entry.name}`, {
- layout: { direction: "ltr", height: fixed(1), gap: 1 },
- }),
- );
- ops.push(text(`${entry.key}.`, { color: dim }));
- toggle(ops, ctx.log[entry.name], entry.name);
- ops.push(close());
- }
-}
-
-function configPanel(ops: Op[], ctx: AppContext): void {
- let color = ctx.mode === "config" ? active : rgba(0, 0, 0, 0);
- ops.push(open("config", {
- layout: {
- direction: "ltr",
- gap: 3,
- padding: { left: 1, right: 1, top: 1, bottom: 1 },
- },
- border: { color, left: 1, right: 1, top: 1, bottom: 1 },
- }));
-
- // keyboard protocol level column
- ops.push(open("protocol-level", { layout: { direction: "ttb", gap: 1 } }));
- ops.push(
- open("", { layout: { height: fixed(1) } }),
- text("Keyboard Protocol Level", { color: highlight }),
- close(),
- );
- for (let i = 0; i < flagNames.length; i++) {
- let name = flagNames[i];
- ops.push(
- open(`flag:${name}`, {
- layout: { direction: "ltr", height: fixed(1), gap: 1 },
- }),
- );
- ops.push(text(`${i + 1}.`, { color: dim }));
- toggle(ops, ctx[name], name);
- ops.push(close());
- }
- ops.push(close());
-
- // log events column 1
- let col1 = logEntries.slice(0, 6);
- let col2 = logEntries.slice(6);
-
- ops.push(open("log-events", { layout: { direction: "ttb", gap: 1 } }));
- ops.push(
- open("", { layout: { height: fixed(1) } }),
- text("Log Events", { color: highlight }),
- close(),
- );
- logToggle(ops, col1, ctx);
- ops.push(close());
-
- // log events column 2
- ops.push(open("log-events-2", { layout: { direction: "ttb", gap: 1 } }));
- ops.push(
- open("", { layout: { height: fixed(1) } }),
- close(),
- );
- logToggle(ops, col2, ctx);
- ops.push(close());
-
- ops.push(close());
-}
-
-function keyboard(ctx: AppContext): Op[] {
- let ops: Op[] = [];
-
- // root
- ops.push(
- open("root", {
- layout: {
- width: grow(),
- height: grow(),
- direction: "ttb",
- alignX: 2,
- alignY: 2,
- padding: { left: 2, top: 1 },
- },
- }),
- );
-
- // keyboard + toggles wrapper
- ops.push(
- open("", { layout: { direction: "ttb" } }),
- );
-
- // badges + config row
- ops.push(
- open("", {
- layout: {
- width: grow(),
- direction: "ltr",
- alignY: 0,
- padding: { bottom: 1 },
- },
- }),
- );
-
- // badges column (left, bottom-aligned)
- let badgeBg = ctx.mode === "input" ? rgba(40, 120, 200) : rgba(200, 120, 40);
- let badgeLabel = ctx.mode === "input" ? "input" : "config";
- let badgeHint = ctx.mode === "input"
- ? "Ctrl+X Ctrl+X to enter config"
- : "Set flags with keys [0-5], Enter to save";
- let mouseBg = ctx["Capture mouse events"]
- ? rgba(40, 180, 80)
- : rgba(80, 80, 80);
- let mouseLabel = ctx["Capture mouse events"] ? "capture" : "system";
- ops.push(
- open("badges", {
- layout: { direction: "ttb", gap: 1, padding: { top: 1 } },
- }),
- open("badge:mode", {
- layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } },
- }),
- open("", {
- layout: { padding: { left: 1, right: 1 } },
- bg: rgba(60, 60, 60),
- }),
- text("mode", { color: rgba(220, 220, 220) }),
- close(),
- open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }),
- text(badgeLabel, { color: rgba(255, 255, 255) }),
- close(),
- text(` ${badgeHint}`, { color: dim }),
- close(),
- open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }),
- open("", {
- layout: { padding: { left: 1, right: 1 } },
- bg: rgba(60, 60, 60),
- }),
- text("mouse", { color: rgba(220, 220, 220) }),
- close(),
- open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }),
- text(mouseLabel, { color: rgba(255, 255, 255) }),
- close(),
- text(" Ctrl+X Ctrl+M to toggle", { color: dim }),
- close(),
- close(),
- );
-
- // config panel (right)
- ops.push(
- open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } }),
- );
- configPanel(ops, ctx);
- ops.push(close());
-
- ops.push(close()); // badges + config row
-
- // three keyboard groups side by side, bottom-aligned
- let kbColor = ctx.mode === "input" ? active : rgba(0, 0, 0, 0);
- ops.push(
- open("keyboard", {
- layout: {
- direction: "ltr",
- gap: 3,
- alignY: 1,
- padding: { left: 1, right: 1, top: 1, bottom: 1 },
- },
- border: { color: kbColor, left: 1, right: 1, top: 1, bottom: 1 },
- }),
- );
-
- mainKeys(ops, ctx);
- navKeys(ops, ctx);
- numpad(ops, ctx);
-
- ops.push(close());
-
- ops.push(close()); // keyboard + toggles wrapper
-
- // raw event display
- ops.push(
- open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } }),
- text(ctx.logged ? JSON.stringify(ctx.logged) : "Press any key...", {
- color: highlight,
- }),
- close(),
- );
-
- ops.push(close());
-
- return ops;
-}
+ text,
+});
-function ttyFlags(ctx: AppContext): Setting {
- let parts: Setting[] = [];
- let bits = 0;
- if (ctx["Disambiguate escape codes"]) bits |= 1;
- if (ctx["Report event types"]) bits |= 2;
- if (ctx["Report alternate keys"]) bits |= 4;
- if (ctx["Report all keys as escapes"]) bits |= 8;
- if (ctx["Report associated text"]) bits |= 16;
- parts.push(progressiveInput(bits));
- if (ctx["Capture mouse events"]) {
- parts.push(mouseTracking());
+function writeAllSync(output: Uint8Array): void {
+ // Deno's stdout writes are not guaranteed to drain the full buffer in one
+ // call, so always loop until the entire frame chunk is written.
+ let offset = 0;
+ while (offset < output.length) {
+ let written = Deno.stdout.writeSync(output.subarray(offset));
+ if (written <= 0) {
+ // should never happen with stdout, but guard against infinite loop if it does
+ throw new Error("stdout write returned without making progress");
+ }
+ offset += written;
}
- return settings(...parts);
}
await main(function* () {
@@ -567,30 +47,35 @@ await main(function* () {
? Deno.consoleSize()
: { columns: 80, rows: 24 };
- Deno.stdin.setRaw(true);
+ if (Deno.stdin.isTerminal()) {
+ Deno.stdin.setRaw(true);
+ }
let stdin = yield* useStdin();
let input = useInput(stdin);
-
let term = yield* until(createTerm({ width: columns, height: rows }));
let tty = settings(alternateBuffer(), cursor(false));
- Deno.stdout.writeSync(tty.apply);
+ writeAllSync(tty.apply);
- let modality = recognizer();
+ let modality = demo.recognizer();
let context = modality.next().value;
- let flags = ttyFlags(context);
- Deno.stdout.writeSync(flags.apply);
+ let flags = demo.ttyFlags(context);
+ writeAllSync(flags.apply);
yield* ensure(() => {
- Deno.stdout.writeSync(flags.revert);
- Deno.stdout.writeSync(tty.revert);
+ // Restore so Backspace and normal shell editing work after exit.
+ if (Deno.stdin.isTerminal()) {
+ Deno.stdin.setRaw(false);
+ }
+ writeAllSync(flags.revert);
+ writeAllSync(tty.revert);
});
- let { output } = term.render(keyboard(context));
+ let { output } = term.render(demo.keyboard(context));
- Deno.stdout.writeSync(output);
+ writeAllSync(output);
let pointer = {
events: createChannel(),
@@ -616,9 +101,9 @@ await main(function* () {
context = { ...context, logged: prev };
}
- Deno.stdout.writeSync(flags.revert);
- flags = ttyFlags(context);
- Deno.stdout.writeSync(flags.apply);
+ writeAllSync(flags.revert);
+ flags = demo.ttyFlags(context);
+ writeAllSync(flags.apply);
if (context["Capture mouse events"]) {
if ("x" in event) {
@@ -632,7 +117,7 @@ await main(function* () {
pointer.state = undefined;
}
- let { output, events } = term.render(keyboard(context), {
+ let { output, events } = term.render(demo.keyboard(context), {
pointer: pointer.state,
});
@@ -640,125 +125,12 @@ await main(function* () {
yield* pointer.events.send(event);
}
- Deno.stdout.writeSync(output);
+ writeAllSync(output);
yield* each.next();
}
});
-function* recognizer(): Iterator {
- let current: AppContext = {
- mode: "input",
- "Disambiguate escape codes": true,
- "Report event types": true,
- "Report alternate keys": true,
- "Report all keys as escapes": true,
- "Report associated text": true,
- "Capture mouse events": true,
- log: {
- keydown: true,
- keyrepeat: false,
- keyup: false,
- mousedown: false,
- mouseup: false,
- mousemove: false,
- wheel: true,
- resize: true,
- pointerenter: false,
- pointerleave: false,
- pointerclick: true,
- },
- entered: new Set(),
- event: null,
- logged: null,
- };
-
- let event = yield current;
-
- let mode = inputmode({ ...current, event });
-
- while (true) {
- mode = yield* mode;
- }
-}
-
-type Mode = Iterable;
-
-function* inputmode(context: AppContext): Mode {
- context = { ...context, mode: "input" };
- let event = context.event ? context.event : yield context;
- while (true) {
- context = { ...context, event };
- if (event.type === "keydown" && event.key === "x" && event.ctrl) {
- let next = yield context;
- while (next.type !== "keydown") {
- context = { ...context, event: next };
- next = yield context;
- }
- context = { ...context, event: next };
- if (next.key === "x" && next.ctrl) {
- return configmode({
- ...context,
- event: null,
- });
- } else if (next.key === "m" && next.ctrl) {
- context = {
- ...context,
- "Capture mouse events": !context["Capture mouse events"],
- event: null,
- };
- event = yield context;
- continue;
- }
- }
- event = yield context;
- }
-}
-
-function* configmode(context: AppContext): Mode {
- context = { ...context, mode: "config" };
- let event = yield context;
- while (true) {
- if (event.type === "keydown" && event.key === "Enter") {
- return inputmode({ ...context, event: null });
- }
- if (event.type === "keydown") {
- let k = (event as KeyEvent).key;
- let entry = logEntries.find((e) => e.key === k);
- if (entry) {
- context = {
- ...context,
- log: { ...context.log, [entry.name]: !context.log[entry.name] },
- };
- }
- if ("012345".indexOf(event.key) >= 0) {
- context = { ...context };
- context["Report associated text"] = false;
- context["Report all keys as escapes"] = false;
- context["Report alternate keys"] = false;
- context["Report event types"] = false;
- context["Disambiguate escape codes"] = false;
- switch (event.key) {
- case "5":
- context["Report associated text"] = true;
- case "4":
- context["Report all keys as escapes"] = true;
- case "3":
- context["Report alternate keys"] = true;
- case "2":
- context["Report event types"] = true;
- case "1":
- context["Disambiguate escape codes"] = true;
- break;
- case "0":
- break;
- }
- }
- }
- event = yield context;
- }
-}
-
function merge(
a: Stream,
b: Stream,
@@ -790,17 +162,3 @@ type EventFilter = {
pointerleave: boolean;
pointerclick: boolean;
};
-
-type AppContext = {
- mode: "input" | "config";
- event: InputEvent | PointerEvent | null;
- logged: InputEvent | PointerEvent | null;
- log: EventFilter;
- entered: Set;
- ["Disambiguate escape codes"]: boolean;
- ["Report event types"]: boolean;
- ["Report alternate keys"]: boolean;
- ["Report all keys as escapes"]: boolean;
- ["Report associated text"]: boolean;
- ["Capture mouse events"]: boolean;
-};
diff --git a/deno.lock b/deno.lock
index 7947a80..a25aa0d 100644
--- a/deno.lock
+++ b/deno.lock
@@ -22,6 +22,7 @@
"jsr:@ts-morph/common@0.27": "0.27.0",
"npm:@sinclair/typebox@*": "0.34.48",
"npm:@sinclair/typebox@0.34": "0.34.48",
+ "npm:@types/node@*": "25.9.1",
"npm:effection@^4.0.2": "4.0.2",
"npm:valrs@*": "0.1.0"
},
@@ -112,9 +113,18 @@
"@sinclair/typebox@0.34.48": {
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="
},
+ "@types/node@25.9.1": {
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "dependencies": [
+ "undici-types"
+ ]
+ },
"effection@4.0.2": {
"integrity": "sha512-O8WMGP10nPuJDwbNGILcaCNWS+CvDYjcdsUSD79nWZ+WtUQ8h1MEV7JJwCSZCSeKx8+TdEaZ/8r6qPTR2o/o8w=="
},
+ "undici-types@7.24.6": {
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="
+ },
"valrs@0.1.0": {
"integrity": "sha512-BqVkjx3qhsRLHerblLDoqEx0OEx7ms0DB6LPv40oWkMfFKUVKrqVuklaGdrPrHyubC5hSHYfEtUiQXrCkC6xHQ=="
}
diff --git a/input-native.ts b/input-native.ts
index 7ed6d1b..e66e405 100644
--- a/input-native.ts
+++ b/input-native.ts
@@ -177,7 +177,12 @@ export async function createInputNative(
let memory = new WebAssembly.Memory({ initial: 4 });
let instance = await WebAssembly.instantiate(compiled, {
- env: { memory },
+ env: {
+ memory,
+ debugLog(_ptr: number, _len: number) {
+ // no-op debug logger for wasm imports
+ },
+ },
clay: {
measureTextFunction() {},
queryScrollOffsetFunction(ret: number) {
diff --git a/term-native.ts b/term-native.ts
index 40e646d..5579680 100644
--- a/term-native.ts
+++ b/term-native.ts
@@ -41,7 +41,12 @@ export async function createTermNative(
let exports: Record = {};
let instance = await WebAssembly.instantiate(compiled, {
- env: { memory },
+ env: {
+ memory,
+ debugLog(_ptr: number, _len: number) {
+ // no-op debug logger for wasm imports
+ },
+ },
clay: {
measureTextFunction(
ret: number,
diff --git a/term.ts b/term.ts
index 12517d0..478c099 100644
--- a/term.ts
+++ b/term.ts
@@ -84,6 +84,7 @@ export async function createTerm(options: TermOptions): Promise {
let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength);
let mode = options?.mode === "line" ? 1 : 0;
let row = options?.row ?? 1;
+
native.reduce(statePtr, opsBuf, len, mode, row);
if (options?.pointer) {
@@ -91,7 +92,7 @@ export async function createTerm(options: TermOptions): Promise {
native.setPointer(x, y, down);
}
- let output = new Uint8Array(
+ let output: Uint8Array = new Uint8Array(
memory.buffer,
native.output(statePtr),
native.length(statePtr),