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),