[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

node example, deno ensure all bytes write

Jacob Bolda (May 23, 2026, 1:23 AM -0500) 36fe005c 4940c9ef

+970 -714
+254
demo/keyboard-node.ts
··· 1 + /// <reference types="npm:@types/node" /> 2 + 3 + import { Buffer } from "node:buffer"; 4 + import { 5 + alternateBuffer, 6 + close, 7 + createInput, 8 + createTerm, 9 + cursor, 10 + fixed, 11 + grow, 12 + mouseTracking, 13 + open, 14 + progressiveInput, 15 + rgba, 16 + settings, 17 + text, 18 + } from "../build/npm/esm/mod.js"; 19 + import type { InputEvent, PointerEvent } from "../mod.ts"; 20 + import { createKeyboardDemo } from "./keyboard-shared.ts"; 21 + 22 + type PointerState = { 23 + x: number; 24 + y: number; 25 + down: boolean; 26 + }; 27 + 28 + type NodeInput = Awaited<ReturnType<typeof createInput>>; 29 + type NodeScanResult = { 30 + events: InputEvent[]; 31 + pending?: { delay: number }; 32 + }; 33 + 34 + let demo = createKeyboardDemo({ 35 + close, 36 + fixed, 37 + grow, 38 + mouseTracking, 39 + open, 40 + progressiveInput, 41 + rgba, 42 + settings, 43 + text, 44 + } as Parameters<typeof createKeyboardDemo>[0]); 45 + 46 + let diagnostics = { 47 + disableWindowsFullscreenHandling: process.argv.includes( 48 + "--no-windows-fullscreen-fix", 49 + ), 50 + }; 51 + 52 + (globalThis as typeof globalThis & { 53 + __claytermDiagnostics__?: { 54 + disableWindowsFullscreenHandling?: boolean; 55 + }; 56 + }).__claytermDiagnostics__ = { 57 + disableWindowsFullscreenHandling: diagnostics.disableWindowsFullscreenHandling, 58 + }; 59 + 60 + let term: Awaited<ReturnType<typeof createTerm>> | null = null; 61 + let input: NodeInput | null = null; 62 + let size = getSize(); 63 + let flushTimer: ReturnType<typeof setTimeout> | null = null; 64 + let tty = settings(alternateBuffer(), cursor(false)); 65 + let modality = demo.recognizer(); 66 + let context = modality.next().value; 67 + let flags = demo.ttyFlags(context); 68 + 69 + let pointer: { state: PointerState | undefined } = { 70 + state: undefined, 71 + }; 72 + 73 + function getSize() { 74 + return { 75 + width: process.stdout.columns || 80, 76 + height: process.stdout.rows || 24, 77 + }; 78 + } 79 + 80 + function write(bytes: Uint8Array): void { 81 + process.stdout.write(Buffer.from(bytes)); 82 + } 83 + 84 + function scan(chunk?: Uint8Array): NodeScanResult { 85 + let normalized = chunk ? new Uint8Array(chunk) : undefined; 86 + return input!.scan(normalized) as NodeScanResult; 87 + } 88 + 89 + function resetFlushTimer(delay: number): void { 90 + if (flushTimer !== null) { 91 + clearTimeout(flushTimer); 92 + } 93 + flushTimer = setTimeout(() => { 94 + flushTimer = null; 95 + let result = scan(); 96 + for (let event of result.events) { 97 + handleEvent(event); 98 + } 99 + }, delay); 100 + } 101 + 102 + function applyFlags(): void { 103 + write(flags.revert); 104 + flags = demo.ttyFlags(context); 105 + write(flags.apply); 106 + } 107 + 108 + function render(): PointerEvent[] { 109 + if (!term) { 110 + return []; 111 + } 112 + let result = term.render(demo.keyboard(context), { 113 + pointer: pointer.state, 114 + }); 115 + write(result.output); 116 + return result.events as PointerEvent[]; 117 + } 118 + 119 + function dispatchLogged(event: InputEvent | PointerEvent): void { 120 + let previous = context.logged; 121 + context = modality.next(event).value; 122 + if ( 123 + context.event && 124 + context.event.type in context.log && 125 + context.log[context.event.type as keyof typeof context.log] 126 + ) { 127 + context = { ...context, logged: context.event }; 128 + } else { 129 + context = { ...context, logged: previous }; 130 + } 131 + } 132 + 133 + function updatePointer(event: InputEvent | PointerEvent): void { 134 + if (!context["Capture mouse events"]) { 135 + pointer.state = undefined; 136 + return; 137 + } 138 + if (!("x" in event)) { 139 + return; 140 + } 141 + pointer.state = { 142 + x: event.x, 143 + y: event.y, 144 + down: event.type === "mousedown", 145 + }; 146 + } 147 + 148 + function handleEvent(event: InputEvent | PointerEvent): void { 149 + if (event.type === "keydown" && event.ctrl && event.key === "c") { 150 + cleanup(); 151 + process.exit(0); 152 + } 153 + 154 + if (event.type === "pointerenter") { 155 + context.entered.add(event.id); 156 + } 157 + if (event.type === "pointerleave") { 158 + context.entered.delete(event.id); 159 + } 160 + 161 + dispatchLogged(event); 162 + applyFlags(); 163 + updatePointer(event); 164 + 165 + let queue = render(); 166 + while (queue.length > 0) { 167 + let next = queue.shift(); 168 + if (!next) { 169 + continue; 170 + } 171 + if (next.type === "pointerenter") { 172 + context.entered.add(next.id); 173 + } 174 + if (next.type === "pointerleave") { 175 + context.entered.delete(next.id); 176 + } 177 + dispatchLogged(next); 178 + applyFlags(); 179 + let emitted = render(); 180 + queue.push(...emitted); 181 + } 182 + } 183 + 184 + async function resetTerm(): Promise<void> { 185 + size = getSize(); 186 + term = await createTerm(size); 187 + } 188 + 189 + function cleanup(): void { 190 + if (flushTimer !== null) { 191 + clearTimeout(flushTimer); 192 + flushTimer = null; 193 + } 194 + try { 195 + write(flags.revert); 196 + write(tty.revert); 197 + } catch { 198 + // ignore cleanup write failures during shutdown 199 + } 200 + if (process.stdin.isTTY) { 201 + process.stdin.setRawMode(false); 202 + } 203 + process.stdin.pause(); 204 + } 205 + 206 + try { 207 + input = await createInput(); 208 + await resetTerm(); 209 + 210 + write(tty.apply); 211 + write(flags.apply); 212 + render(); 213 + 214 + if (process.stdin.isTTY) { 215 + process.stdin.setRawMode(true); 216 + } 217 + process.stdin.resume(); 218 + process.stdin.on("data", (chunk: Uint8Array) => { 219 + let result = scan(new Uint8Array(chunk)); 220 + for (let event of result.events) { 221 + handleEvent(event); 222 + } 223 + if (result.pending) { 224 + resetFlushTimer(result.pending.delay); 225 + } 226 + }); 227 + 228 + if (process.stdout.isTTY) { 229 + process.stdout.on("resize", async () => { 230 + try { 231 + await resetTerm(); 232 + render(); 233 + } catch (error) { 234 + cleanup(); 235 + console.error(error); 236 + process.exit(1); 237 + } 238 + }); 239 + } 240 + 241 + process.on("SIGINT", () => { 242 + cleanup(); 243 + process.exit(0); 244 + }); 245 + process.on("SIGTERM", () => { 246 + cleanup(); 247 + process.exit(0); 248 + }); 249 + process.on("exit", cleanup); 250 + } catch (error) { 251 + cleanup(); 252 + console.error(error); 253 + process.exit(1); 254 + }
+640
demo/keyboard-shared.ts
··· 1 + import type { InputEvent, KeyEvent, Op, PointerEvent } from "../mod.ts"; 2 + import type { SizingAxis } from "../ops.ts"; 3 + import type { Setting } from "../settings.ts"; 4 + 5 + type EventFilter = { 6 + keydown: boolean; 7 + keyrepeat: boolean; 8 + keyup: boolean; 9 + mousedown: boolean; 10 + mouseup: boolean; 11 + mousemove: boolean; 12 + wheel: boolean; 13 + resize: boolean; 14 + pointerenter: boolean; 15 + pointerleave: boolean; 16 + pointerclick: boolean; 17 + }; 18 + 19 + type AppContext = { 20 + mode: "input" | "config"; 21 + event: InputEvent | PointerEvent | null; 22 + logged: InputEvent | PointerEvent | null; 23 + log: EventFilter; 24 + entered: Set<string>; 25 + ["Disambiguate escape codes"]: boolean; 26 + ["Report event types"]: boolean; 27 + ["Report alternate keys"]: boolean; 28 + ["Report all keys as escapes"]: boolean; 29 + ["Report associated text"]: boolean; 30 + ["Capture mouse events"]: boolean; 31 + }; 32 + 33 + type KeyDef = { 34 + label: string; 35 + code: string; 36 + width?: number; 37 + }; 38 + 39 + type KeyboardDemoApi = { 40 + close(): Op; 41 + fixed(value: number): SizingAxis; 42 + grow(min?: number, max?: number): SizingAxis; 43 + mouseTracking(): Setting; 44 + open(id: string, properties: Record<string, unknown>): Op; 45 + progressiveInput(bits: number): Setting; 46 + rgba(r: number, g: number, b: number, a?: number): number; 47 + settings(...parts: Setting[]): Setting; 48 + text(value: string, properties?: Record<string, unknown>): Op; 49 + }; 50 + 51 + type Mode = Iterable<AppContext, Mode, InputEvent | PointerEvent>; 52 + 53 + export function createKeyboardDemo(api: KeyboardDemoApi) { 54 + let { 55 + close, 56 + fixed, 57 + grow, 58 + mouseTracking, 59 + open, 60 + progressiveInput, 61 + rgba, 62 + settings, 63 + text, 64 + } = api; 65 + 66 + let active = rgba(60, 120, 220); 67 + let inactive = rgba(50, 50, 60); 68 + let on = rgba(40, 180, 80); 69 + let label = rgba(220, 220, 220); 70 + let dim = rgba(100, 100, 120); 71 + let highlight = rgba(255, 220, 80); 72 + let hovered = rgba(80, 80, 100); 73 + 74 + let KEY_W = 5; 75 + let GAP = 1; 76 + 77 + let flagNames: (keyof Omit< 78 + AppContext, 79 + "mode" | "event" | "logged" | "log" | "entered" 80 + >)[] = [ 81 + "Disambiguate escape codes", 82 + "Report event types", 83 + "Report alternate keys", 84 + "Report all keys as escapes", 85 + "Report associated text", 86 + ]; 87 + 88 + let logEntries: { key: string; name: keyof EventFilter }[] = [ 89 + { key: "a", name: "keydown" }, 90 + { key: "b", name: "keyup" }, 91 + { key: "c", name: "keyrepeat" }, 92 + { key: "d", name: "mousedown" }, 93 + { key: "e", name: "mouseup" }, 94 + { key: "f", name: "mousemove" }, 95 + { key: "g", name: "wheel" }, 96 + { key: "h", name: "resize" }, 97 + { key: "i", name: "pointerenter" }, 98 + { key: "j", name: "pointerleave" }, 99 + { key: "k", name: "pointerclick" }, 100 + ]; 101 + 102 + function isKeyEvent(event: InputEvent | PointerEvent): event is KeyEvent { 103 + return event.type === "keydown" || event.type === "keyrepeat" || 104 + event.type === "keyup"; 105 + } 106 + 107 + function matches(keyDef: KeyDef, event: InputEvent | PointerEvent): boolean { 108 + return isKeyEvent(event) && event.type === "keydown" && 109 + event.code.toUpperCase() === keyDef.code.toUpperCase(); 110 + } 111 + 112 + function key(ops: Op[], keyDef: KeyDef, context: AppContext): void { 113 + let pressed = context.event && matches(keyDef, context.event); 114 + let hover = context.entered.has(`key:${keyDef.code}`); 115 + let bg = pressed ? active : hover ? hovered : inactive; 116 + let width = keyDef.width ?? KEY_W; 117 + ops.push( 118 + open(`key:${keyDef.code}`, { 119 + layout: { 120 + width: fixed(width), 121 + height: grow(), 122 + padding: { left: 1, right: 1 }, 123 + alignX: 2, 124 + alignY: 2, 125 + }, 126 + bg, 127 + border: hover 128 + ? { color: highlight, left: 1, right: 1, top: 1, bottom: 1 } 129 + : undefined, 130 + }), 131 + text(keyDef.label, { color: hover ? highlight : label }), 132 + close(), 133 + ); 134 + } 135 + 136 + function row(ops: Op[], keys: KeyDef[], context: AppContext): void { 137 + ops.push( 138 + open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }), 139 + ); 140 + for (let keyDef of keys) { 141 + key(ops, keyDef, context); 142 + } 143 + ops.push(close()); 144 + } 145 + 146 + function spacer(ops: Op[], width: number): void { 147 + ops.push( 148 + open("", { layout: { width: fixed(width), height: grow() } }), 149 + close(), 150 + ); 151 + } 152 + 153 + function mainKeys(ops: Op[], context: AppContext): void { 154 + ops.push(open("main-keys", { layout: { direction: "ttb", gap: GAP } })); 155 + 156 + row(ops, [ 157 + { label: "Esc", code: "Escape", width: 11 }, 158 + { label: "F1", code: "F1" }, 159 + { label: "F2", code: "F2" }, 160 + { label: "F3", code: "F3" }, 161 + { label: "F4", code: "F4" }, 162 + { label: "F5", code: "F5" }, 163 + { label: "F6", code: "F6" }, 164 + { label: "F7", code: "F7" }, 165 + { label: "F8", code: "F8" }, 166 + { label: "F9", code: "F9" }, 167 + { label: "F10", code: "F10" }, 168 + { label: "F11", code: "F11" }, 169 + { label: "F12", code: "F12" }, 170 + ], context); 171 + 172 + row(ops, [ 173 + { label: "`", code: "`" }, 174 + { label: "1", code: "1" }, 175 + { label: "2", code: "2" }, 176 + { label: "3", code: "3" }, 177 + { label: "4", code: "4" }, 178 + { label: "5", code: "5" }, 179 + { label: "6", code: "6" }, 180 + { label: "7", code: "7" }, 181 + { label: "8", code: "8" }, 182 + { label: "9", code: "9" }, 183 + { label: "0", code: "0" }, 184 + { label: "-", code: "-" }, 185 + { label: "=", code: "=" }, 186 + { label: "Bksp", code: "Backspace", width: 9 }, 187 + ], context); 188 + 189 + row(ops, [ 190 + { label: "Tab", code: "Tab", width: 7 }, 191 + { label: "Q", code: "q" }, 192 + { label: "W", code: "w" }, 193 + { label: "E", code: "e" }, 194 + { label: "R", code: "r" }, 195 + { label: "T", code: "t" }, 196 + { label: "Y", code: "y" }, 197 + { label: "U", code: "u" }, 198 + { label: "I", code: "i" }, 199 + { label: "O", code: "o" }, 200 + { label: "P", code: "p" }, 201 + { label: "[", code: "[" }, 202 + { label: "]", code: "]" }, 203 + { label: "\\", code: "\\", width: 7 }, 204 + ], context); 205 + 206 + row(ops, [ 207 + { label: "Caps", code: "CapsLock", width: 9 }, 208 + { label: "A", code: "a" }, 209 + { label: "S", code: "s" }, 210 + { label: "D", code: "d" }, 211 + { label: "F", code: "f" }, 212 + { label: "G", code: "g" }, 213 + { label: "H", code: "h" }, 214 + { label: "J", code: "j" }, 215 + { label: "K", code: "k" }, 216 + { label: "L", code: "l" }, 217 + { label: ";", code: ";" }, 218 + { label: "'", code: "'" }, 219 + { label: "Enter", code: "Enter", width: 10 }, 220 + ], context); 221 + 222 + row(ops, [ 223 + { label: "Shift", code: "ShiftLeft", width: 11 }, 224 + { label: "Z", code: "z" }, 225 + { label: "X", code: "x" }, 226 + { label: "C", code: "c" }, 227 + { label: "V", code: "v" }, 228 + { label: "B", code: "b" }, 229 + { label: "N", code: "n" }, 230 + { label: "M", code: "m" }, 231 + { label: ",", code: "," }, 232 + { label: ".", code: "." }, 233 + { label: "/", code: "/" }, 234 + { label: "Shift", code: "ShiftRight", width: 13 }, 235 + ], context); 236 + 237 + row(ops, [ 238 + { label: "Ctrl", code: "ControlLeft", width: 7 }, 239 + { label: "Win", code: "SuperLeft", width: 6 }, 240 + { label: "Alt", code: "AltLeft", width: 6 }, 241 + { label: "", code: " ", width: 33 }, 242 + { label: "Alt", code: "AltRight", width: 6 }, 243 + { label: "Win", code: "SuperRight", width: 6 }, 244 + { label: "Menu", code: "Menu", width: 6 }, 245 + { label: "Ctrl", code: "ControlRight", width: 7 }, 246 + ], context); 247 + 248 + ops.push(close()); 249 + } 250 + 251 + function navKeys(ops: Op[], context: AppContext): void { 252 + ops.push(open("nav-keys", { layout: { direction: "ttb", gap: GAP } })); 253 + 254 + row(ops, [ 255 + { label: "Ins", code: "Insert", width: 6 }, 256 + { label: "Home", code: "Home", width: 6 }, 257 + { label: "PgUp", code: "PageUp", width: 6 }, 258 + ], context); 259 + 260 + row(ops, [ 261 + { label: "Del", code: "Delete", width: 6 }, 262 + { label: "End", code: "End", width: 6 }, 263 + { label: "PgDn", code: "PageDown", width: 6 }, 264 + ], context); 265 + 266 + ops.push(open("", { layout: { height: fixed(3) } }), close()); 267 + 268 + ops.push( 269 + open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }), 270 + ); 271 + spacer(ops, 6); 272 + key(ops, { label: "↑", code: "ArrowUp", width: 6 }, context); 273 + spacer(ops, 6); 274 + ops.push(close()); 275 + 276 + row(ops, [ 277 + { label: "←", code: "ArrowLeft", width: 6 }, 278 + { label: "↓", code: "ArrowDown", width: 6 }, 279 + { label: "→", code: "ArrowRight", width: 6 }, 280 + ], context); 281 + 282 + ops.push(close()); 283 + } 284 + 285 + function numpad(ops: Op[], context: AppContext): void { 286 + ops.push(open("numpad", { layout: { direction: "ttb", gap: GAP } })); 287 + 288 + row(ops, [ 289 + { label: "Num", code: "NumLock", width: 6 }, 290 + { label: "/", code: "NumpadDivide", width: 6 }, 291 + { label: "*", code: "NumpadMultiply", width: 6 }, 292 + { label: "-", code: "NumpadSubtract", width: 6 }, 293 + ], context); 294 + 295 + ops.push(open("", { layout: { direction: "ltr", gap: GAP } })); 296 + ops.push(open("", { layout: { direction: "ttb", gap: GAP } })); 297 + row(ops, [ 298 + { label: "7", code: "Numpad7", width: 6 }, 299 + { label: "8", code: "Numpad8", width: 6 }, 300 + { label: "9", code: "Numpad9", width: 6 }, 301 + ], context); 302 + row(ops, [ 303 + { label: "4", code: "Numpad4", width: 6 }, 304 + { label: "5", code: "Numpad5", width: 6 }, 305 + { label: "6", code: "Numpad6", width: 6 }, 306 + ], context); 307 + ops.push(close()); 308 + 309 + key(ops, { label: "+", code: "NumpadAdd" }, context); 310 + ops.push(close()); 311 + 312 + ops.push(open("", { layout: { direction: "ltr", gap: GAP } })); 313 + ops.push(open("", { layout: { direction: "ttb", gap: GAP } })); 314 + row(ops, [ 315 + { label: "1", code: "Numpad1", width: 6 }, 316 + { label: "2", code: "Numpad2", width: 6 }, 317 + { label: "3", code: "Numpad3", width: 6 }, 318 + ], context); 319 + row(ops, [ 320 + { label: "0", code: "Numpad0", width: 13 }, 321 + { label: ".", code: "NumpadDecimal", width: 6 }, 322 + ], context); 323 + ops.push(close()); 324 + 325 + key(ops, { label: "Ent", code: "NumpadEnter" }, context); 326 + ops.push(close()); 327 + ops.push(close()); 328 + } 329 + 330 + function toggle(ops: Op[], enabled: boolean, name: string): void { 331 + let indicator = enabled ? "●───" : "───○"; 332 + ops.push( 333 + open("", { layout: { direction: "ltr", height: fixed(1), gap: 1 } }), 334 + text(indicator, { color: enabled ? on : dim }), 335 + text(name, { color: enabled ? label : dim }), 336 + close(), 337 + ); 338 + } 339 + 340 + function logToggle( 341 + ops: Op[], 342 + entries: typeof logEntries, 343 + context: AppContext, 344 + ): void { 345 + for (let entry of entries) { 346 + ops.push( 347 + open(`log:${entry.name}`, { 348 + layout: { direction: "ltr", height: fixed(1), gap: 1 }, 349 + }), 350 + ); 351 + ops.push(text(`${entry.key}.`, { color: dim })); 352 + toggle(ops, context.log[entry.name], entry.name); 353 + ops.push(close()); 354 + } 355 + } 356 + 357 + function configPanel(ops: Op[], context: AppContext): void { 358 + let color = context.mode === "config" ? active : rgba(0, 0, 0, 0); 359 + ops.push(open("config", { 360 + layout: { 361 + direction: "ltr", 362 + gap: 3, 363 + padding: { left: 1, right: 1, top: 1, bottom: 1 }, 364 + }, 365 + border: { color, left: 1, right: 1, top: 1, bottom: 1 }, 366 + })); 367 + 368 + ops.push(open("protocol-level", { layout: { direction: "ttb", gap: 1 } })); 369 + ops.push( 370 + open("", { layout: { height: fixed(1) } }), 371 + text("Keyboard Protocol Level", { color: highlight }), 372 + close(), 373 + ); 374 + for (let index = 0; index < flagNames.length; index++) { 375 + let name = flagNames[index]; 376 + ops.push( 377 + open(`flag:${name}`, { 378 + layout: { direction: "ltr", height: fixed(1), gap: 1 }, 379 + }), 380 + ); 381 + ops.push(text(`${index + 1}.`, { color: dim })); 382 + toggle(ops, context[name], name); 383 + ops.push(close()); 384 + } 385 + ops.push(close()); 386 + 387 + let col1 = logEntries.slice(0, 6); 388 + let col2 = logEntries.slice(6); 389 + 390 + ops.push(open("log-events", { layout: { direction: "ttb", gap: 1 } })); 391 + ops.push( 392 + open("", { layout: { height: fixed(1) } }), 393 + text("Log Events", { color: highlight }), 394 + close(), 395 + ); 396 + logToggle(ops, col1, context); 397 + ops.push(close()); 398 + 399 + ops.push(open("log-events-2", { layout: { direction: "ttb", gap: 1 } })); 400 + ops.push(open("", { layout: { height: fixed(1) } }), close()); 401 + logToggle(ops, col2, context); 402 + ops.push(close()); 403 + 404 + ops.push(close()); 405 + } 406 + 407 + function keyboard(context: AppContext): Op[] { 408 + let ops: Op[] = []; 409 + 410 + ops.push(open("root", { 411 + layout: { 412 + width: grow(), 413 + height: grow(), 414 + direction: "ttb", 415 + alignX: 2, 416 + alignY: 2, 417 + padding: { left: 2, top: 1 }, 418 + }, 419 + })); 420 + 421 + ops.push(open("", { layout: { direction: "ttb" } })); 422 + 423 + ops.push(open("", { 424 + layout: { 425 + width: grow(), 426 + direction: "ltr", 427 + alignY: 0, 428 + padding: { bottom: 1 }, 429 + }, 430 + })); 431 + 432 + let badgeBg = context.mode === "input" 433 + ? rgba(40, 120, 200) 434 + : rgba(200, 120, 40); 435 + let badgeLabel = context.mode === "input" ? "input" : "config"; 436 + let badgeHint = context.mode === "input" 437 + ? "Ctrl+X Ctrl+X to enter config" 438 + : "Set flags with keys [0-5], Enter to save"; 439 + let mouseBg = context["Capture mouse events"] 440 + ? rgba(40, 180, 80) 441 + : rgba(80, 80, 80); 442 + let mouseLabel = context["Capture mouse events"] ? "capture" : "system"; 443 + 444 + ops.push( 445 + open("badges", { layout: { direction: "ttb", gap: 1, padding: { top: 1 } } }), 446 + open("badge:mode", { 447 + layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } }, 448 + }), 449 + open("", { 450 + layout: { padding: { left: 1, right: 1 } }, 451 + bg: rgba(60, 60, 60), 452 + }), 453 + text("mode", { color: rgba(220, 220, 220) }), 454 + close(), 455 + open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }), 456 + text(badgeLabel, { color: rgba(255, 255, 255) }), 457 + close(), 458 + text(` ${badgeHint}`, { color: dim }), 459 + close(), 460 + open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }), 461 + open("", { 462 + layout: { padding: { left: 1, right: 1 } }, 463 + bg: rgba(60, 60, 60), 464 + }), 465 + text("mouse", { color: rgba(220, 220, 220) }), 466 + close(), 467 + open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }), 468 + text(mouseLabel, { color: rgba(255, 255, 255) }), 469 + close(), 470 + text(" Ctrl+X Ctrl+M to toggle", { color: dim }), 471 + close(), 472 + close(), 473 + ); 474 + 475 + ops.push(open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } })); 476 + configPanel(ops, context); 477 + ops.push(close()); 478 + ops.push(close()); 479 + 480 + let keyboardColor = context.mode === "input" ? active : rgba(0, 0, 0, 0); 481 + ops.push(open("keyboard", { 482 + layout: { 483 + direction: "ltr", 484 + gap: 3, 485 + alignY: 1, 486 + padding: { left: 1, right: 1, top: 1, bottom: 1 }, 487 + }, 488 + border: { color: keyboardColor, left: 1, right: 1, top: 1, bottom: 1 }, 489 + })); 490 + 491 + mainKeys(ops, context); 492 + navKeys(ops, context); 493 + numpad(ops, context); 494 + 495 + ops.push(close()); 496 + ops.push(close()); 497 + 498 + ops.push(open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } })); 499 + ops.push( 500 + text(context.logged ? JSON.stringify(context.logged) : "Press any key...", { 501 + color: highlight, 502 + }), 503 + ); 504 + ops.push(close()); 505 + ops.push(close()); 506 + 507 + return ops; 508 + } 509 + 510 + function ttyFlags(context: AppContext): Setting { 511 + let parts: Setting[] = []; 512 + let bits = 0; 513 + if (context["Disambiguate escape codes"]) bits |= 1; 514 + if (context["Report event types"]) bits |= 2; 515 + if (context["Report alternate keys"]) bits |= 4; 516 + if (context["Report all keys as escapes"]) bits |= 8; 517 + if (context["Report associated text"]) bits |= 16; 518 + parts.push(progressiveInput(bits)); 519 + if (context["Capture mouse events"]) { 520 + parts.push(mouseTracking()); 521 + } 522 + return settings(...parts); 523 + } 524 + 525 + function createInitialContext(): AppContext { 526 + return { 527 + mode: "input", 528 + "Disambiguate escape codes": true, 529 + "Report event types": true, 530 + "Report alternate keys": true, 531 + "Report all keys as escapes": true, 532 + "Report associated text": true, 533 + "Capture mouse events": true, 534 + log: { 535 + keydown: true, 536 + keyrepeat: false, 537 + keyup: false, 538 + mousedown: false, 539 + mouseup: false, 540 + mousemove: false, 541 + wheel: true, 542 + resize: true, 543 + pointerenter: false, 544 + pointerleave: false, 545 + pointerclick: true, 546 + }, 547 + entered: new Set(), 548 + event: null, 549 + logged: null, 550 + }; 551 + } 552 + 553 + function* recognizer(): Iterator<AppContext, never, InputEvent | PointerEvent> { 554 + let current = createInitialContext(); 555 + let event = yield current; 556 + let mode = inputmode({ ...current, event }); 557 + 558 + while (true) { 559 + mode = yield* mode; 560 + } 561 + } 562 + 563 + function* inputmode(context: AppContext): Mode { 564 + context = { ...context, mode: "input" }; 565 + let event = context.event ? context.event : yield context; 566 + while (true) { 567 + context = { ...context, event }; 568 + if (event.type === "keydown" && event.key === "x" && event.ctrl) { 569 + let next = yield context; 570 + while (next.type !== "keydown") { 571 + context = { ...context, event: next }; 572 + next = yield context; 573 + } 574 + context = { ...context, event: next }; 575 + if (next.key === "x" && next.ctrl) { 576 + return configmode({ ...context, event: null }); 577 + } else if (next.key === "m" && next.ctrl) { 578 + context = { 579 + ...context, 580 + "Capture mouse events": !context["Capture mouse events"], 581 + event: null, 582 + }; 583 + event = yield context; 584 + continue; 585 + } 586 + } 587 + event = yield context; 588 + } 589 + } 590 + 591 + function* configmode(context: AppContext): Mode { 592 + context = { ...context, mode: "config" }; 593 + let event = yield context; 594 + while (true) { 595 + if (event.type === "keydown" && event.key === "Enter") { 596 + return inputmode({ ...context, event: null }); 597 + } 598 + if (event.type === "keydown") { 599 + let keyEvent = event as KeyEvent; 600 + let entry = logEntries.find((candidate) => candidate.key === keyEvent.key); 601 + if (entry) { 602 + context = { 603 + ...context, 604 + log: { ...context.log, [entry.name]: !context.log[entry.name] }, 605 + }; 606 + } 607 + if ("012345".indexOf(keyEvent.key) >= 0) { 608 + context = { ...context }; 609 + context["Report associated text"] = false; 610 + context["Report all keys as escapes"] = false; 611 + context["Report alternate keys"] = false; 612 + context["Report event types"] = false; 613 + context["Disambiguate escape codes"] = false; 614 + switch (keyEvent.key) { 615 + case "5": 616 + context["Report associated text"] = true; 617 + case "4": 618 + context["Report all keys as escapes"] = true; 619 + case "3": 620 + context["Report alternate keys"] = true; 621 + case "2": 622 + context["Report event types"] = true; 623 + case "1": 624 + context["Disambiguate escape codes"] = true; 625 + break; 626 + case "0": 627 + break; 628 + } 629 + } 630 + } 631 + event = yield context; 632 + } 633 + } 634 + 635 + return { 636 + keyboard, 637 + recognizer, 638 + ttyFlags, 639 + }; 640 + }
+49 -704
demo/keyboard.ts
··· 1 - // deno-lint-ignore-file no-fallthrough 2 1 import { 3 2 createChannel, 4 3 each, ··· 10 9 until, 11 10 } from "effection"; 12 11 import { 13 - close, 14 12 createTerm, 15 - fixed, 16 - grow, 17 - type InputEvent, 18 - type KeyEvent, 19 - type Op, 20 - open, 21 13 type PointerEvent, 22 - rgba, 23 - text, 24 14 } from "../mod.ts"; 25 15 import { 26 16 alternateBuffer, 27 - cursor, 28 - mouseTracking, 29 - progressiveInput, 30 - type Setting, 31 17 settings, 32 18 } from "../settings.ts"; 19 + import { close, fixed, grow, open, rgba, text } from "../mod.ts"; 20 + import { cursor, mouseTracking, progressiveInput } from "../settings.ts"; 21 + import { createKeyboardDemo } from "./keyboard-shared.ts"; 33 22 import { useInput } from "./use-input.ts"; 34 23 import { useStdin } from "./use-stdin.ts"; 35 24 36 - const active = rgba(60, 120, 220); 37 - const inactive = rgba(50, 50, 60); 38 - const on = rgba(40, 180, 80); 39 - const label = rgba(220, 220, 220); 40 - const dim = rgba(100, 100, 120); 41 - const highlight = rgba(255, 220, 80); 25 + const demo = createKeyboardDemo({ 26 + close, 27 + fixed, 28 + grow, 29 + mouseTracking, 30 + open, 31 + progressiveInput, 32 + rgba, 33 + settings, 34 + text, 35 + }); 42 36 43 - const KEY_W = 5; 44 - const GAP = 1; 45 - const WRITE_CHUNK_SIZE = 1024; 37 + let diagnostics = { 38 + disableWindowsFullscreenHandling: Deno.args.includes( 39 + "--no-windows-fullscreen-fix", 40 + ), 41 + }; 46 42 47 - interface KeyDef { 48 - label: string; 49 - code: string; 50 - width?: number; 51 - } 52 - 53 - function isKeyEvent(e: InputEvent | PointerEvent): e is KeyEvent { 54 - return e.type === "keydown" || e.type === "keyrepeat" || e.type === "keyup"; 55 - } 56 - 57 - function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean { 58 - return isKeyEvent(event) && event.type === "keydown" && 59 - event.code.toUpperCase() === k.code.toUpperCase(); 60 - } 43 + (globalThis as typeof globalThis & { 44 + __claytermDiagnostics__?: { 45 + disableWindowsFullscreenHandling?: boolean; 46 + }; 47 + }).__claytermDiagnostics__ = { 48 + disableWindowsFullscreenHandling: diagnostics.disableWindowsFullscreenHandling, 49 + }; 61 50 62 - const hovered = rgba(80, 80, 100); 63 - 64 - function writeOutput(output: Uint8Array): void { 65 - if (Deno.build.os !== "windows") { 66 - Deno.stdout.writeSync(output); 67 - return; 68 - } 69 - 70 - // VS Code's Windows terminal path can corrupt large fullscreen writes from 71 - // Deno, so flush complete rendered rows instead of one large write. 72 - let start = 0; 73 - let lastBreak = -1; 74 - 75 - for (let i = 0; i < output.length; i++) { 76 - if (output[i] === 0x0a) { 77 - lastBreak = i + 1; 51 + function writeAllSync(output: Uint8Array): void { 52 + // Deno's stdout writes are not guaranteed to drain the full buffer in one 53 + // call, so always loop until the entire frame chunk is written. 54 + let offset = 0; 55 + while (offset < output.length) { 56 + let written = Deno.stdout.writeSync(output.subarray(offset)); 57 + if (written <= 0) { 58 + // should never happen with stdout, but guard against infinite loop if it does 59 + throw new Error("stdout write returned without making progress"); 78 60 } 79 - 80 - if (i - start + 1 >= WRITE_CHUNK_SIZE && lastBreak > start) { 81 - Deno.stdout.writeSync(output.subarray(start, lastBreak)); 82 - start = lastBreak; 83 - } 84 - } 85 - 86 - if (start < output.length) { 87 - Deno.stdout.writeSync(output.subarray(start)); 61 + offset += written; 88 62 } 89 63 } 90 64 91 - function key(ops: Op[], k: KeyDef, ctx: AppContext): void { 92 - let pressed = ctx.event && matches(k, ctx.event); 93 - let hover = ctx.entered.has(`key:${k.code}`); 94 - let bg = pressed ? active : hover ? hovered : inactive; 95 - let w = k.width ?? KEY_W; 96 - ops.push( 97 - open(`key:${k.code}`, { 98 - layout: { 99 - width: fixed(w), 100 - height: grow(), 101 - padding: { left: 1, right: 1 }, 102 - alignX: 2, 103 - alignY: 2, 104 - }, 105 - bg, 106 - border: hover 107 - ? { color: highlight, left: 1, right: 1, top: 1, bottom: 1 } 108 - : undefined, 109 - }), 110 - text(k.label, { color: hover ? highlight : label }), 111 - close(), 112 - ); 113 - } 114 - 115 - function row(ops: Op[], keys: KeyDef[], ctx: AppContext): void { 116 - ops.push( 117 - open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }), 118 - ); 119 - for (let k of keys) { 120 - key(ops, k, ctx); 121 - } 122 - ops.push(close()); 123 - } 124 - 125 - function spacer(ops: Op[], width: number): void { 126 - ops.push( 127 - open("", { layout: { width: fixed(width), height: grow() } }), 128 - close(), 129 - ); 130 - } 131 - 132 - function mainKeys(ops: Op[], ctx: AppContext): void { 133 - ops.push( 134 - open("main-keys", { layout: { direction: "ttb", gap: GAP } }), 135 - ); 136 - 137 - row(ops, [ 138 - { label: "Esc", code: "Escape", width: 11 }, 139 - { label: "F1", code: "F1" }, 140 - { label: "F2", code: "F2" }, 141 - { label: "F3", code: "F3" }, 142 - { label: "F4", code: "F4" }, 143 - { label: "F5", code: "F5" }, 144 - { label: "F6", code: "F6" }, 145 - { label: "F7", code: "F7" }, 146 - { label: "F8", code: "F8" }, 147 - { label: "F9", code: "F9" }, 148 - { label: "F10", code: "F10" }, 149 - { label: "F11", code: "F11" }, 150 - { label: "F12", code: "F12" }, 151 - ], ctx); 152 - 153 - row(ops, [ 154 - { label: "`", code: "`" }, 155 - { label: "1", code: "1" }, 156 - { label: "2", code: "2" }, 157 - { label: "3", code: "3" }, 158 - { label: "4", code: "4" }, 159 - { label: "5", code: "5" }, 160 - { label: "6", code: "6" }, 161 - { label: "7", code: "7" }, 162 - { label: "8", code: "8" }, 163 - { label: "9", code: "9" }, 164 - { label: "0", code: "0" }, 165 - { label: "-", code: "-" }, 166 - { label: "=", code: "=" }, 167 - { label: "Bksp", code: "Backspace", width: 9 }, 168 - ], ctx); 169 - 170 - row(ops, [ 171 - { label: "Tab", code: "Tab", width: 7 }, 172 - { label: "Q", code: "q" }, 173 - { label: "W", code: "w" }, 174 - { label: "E", code: "e" }, 175 - { label: "R", code: "r" }, 176 - { label: "T", code: "t" }, 177 - { label: "Y", code: "y" }, 178 - { label: "U", code: "u" }, 179 - { label: "I", code: "i" }, 180 - { label: "O", code: "o" }, 181 - { label: "P", code: "p" }, 182 - { label: "[", code: "[" }, 183 - { label: "]", code: "]" }, 184 - { label: "\\", code: "\\", width: 7 }, 185 - ], ctx); 186 - 187 - row(ops, [ 188 - { label: "Caps", code: "CapsLock", width: 9 }, 189 - { label: "A", code: "a" }, 190 - { label: "S", code: "s" }, 191 - { label: "D", code: "d" }, 192 - { label: "F", code: "f" }, 193 - { label: "G", code: "g" }, 194 - { label: "H", code: "h" }, 195 - { label: "J", code: "j" }, 196 - { label: "K", code: "k" }, 197 - { label: "L", code: "l" }, 198 - { label: ";", code: ";" }, 199 - { label: "'", code: "'" }, 200 - { label: "Enter", code: "Enter", width: 10 }, 201 - ], ctx); 202 - 203 - row(ops, [ 204 - { label: "Shift", code: "ShiftLeft", width: 11 }, 205 - { label: "Z", code: "z" }, 206 - { label: "X", code: "x" }, 207 - { label: "C", code: "c" }, 208 - { label: "V", code: "v" }, 209 - { label: "B", code: "b" }, 210 - { label: "N", code: "n" }, 211 - { label: "M", code: "m" }, 212 - { label: ",", code: "," }, 213 - { label: ".", code: "." }, 214 - { label: "/", code: "/" }, 215 - { label: "Shift", code: "ShiftRight", width: 13 }, 216 - ], ctx); 217 - 218 - row(ops, [ 219 - { label: "Ctrl", code: "ControlLeft", width: 7 }, 220 - { label: "Win", code: "SuperLeft", width: 6 }, 221 - { label: "Alt", code: "AltLeft", width: 6 }, 222 - { label: "", code: " ", width: 33 }, 223 - { label: "Alt", code: "AltRight", width: 6 }, 224 - { label: "Win", code: "SuperRight", width: 6 }, 225 - { label: "Menu", code: "Menu", width: 6 }, 226 - { label: "Ctrl", code: "ControlRight", width: 7 }, 227 - ], ctx); 228 - 229 - ops.push(close()); 230 - } 231 - 232 - function navKeys(ops: Op[], ctx: AppContext): void { 233 - ops.push( 234 - open("nav-keys", { layout: { direction: "ttb", gap: GAP } }), 235 - ); 236 - 237 - // top section: Ins/Home/PgUp, Del/End/PgDn 238 - row(ops, [ 239 - { label: "Ins", code: "Insert", width: 6 }, 240 - { label: "Home", code: "Home", width: 6 }, 241 - { label: "PgUp", code: "PageUp", width: 6 }, 242 - ], ctx); 243 - 244 - row(ops, [ 245 - { label: "Del", code: "Delete", width: 6 }, 246 - { label: "End", code: "End", width: 6 }, 247 - { label: "PgDn", code: "PageDown", width: 6 }, 248 - ], ctx); 249 - 250 - // gap before arrows 251 - ops.push( 252 - open("", { layout: { height: fixed(3) } }), 253 - close(), 254 - ); 255 - 256 - // arrow up 257 - ops.push( 258 - open("", { layout: { direction: "ltr", gap: GAP, height: fixed(3) } }), 259 - ); 260 - spacer(ops, 6); 261 - key(ops, { label: "\u2191", code: "ArrowUp", width: 6 }, ctx); 262 - spacer(ops, 6); 263 - ops.push(close()); 264 - 265 - // arrow left/down/right 266 - row(ops, [ 267 - { label: "\u2190", code: "ArrowLeft", width: 6 }, 268 - { label: "\u2193", code: "ArrowDown", width: 6 }, 269 - { label: "\u2192", code: "ArrowRight", width: 6 }, 270 - ], ctx); 271 - 272 - ops.push(close()); 273 - } 274 - 275 - function numpad(ops: Op[], ctx: AppContext): void { 276 - ops.push( 277 - open("numpad", { layout: { direction: "ttb", gap: GAP } }), 278 - ); 279 - 280 - row(ops, [ 281 - { label: "Num", code: "NumLock", width: 6 }, 282 - { label: "/", code: "NumpadDivide", width: 6 }, 283 - { label: "*", code: "NumpadMultiply", width: 6 }, 284 - { label: "-", code: "NumpadSubtract", width: 6 }, 285 - ], ctx); 286 - 287 - // rows 2-3 grouped horizontally so + spans both 288 - ops.push( 289 - open("", { layout: { direction: "ltr", gap: GAP } }), 290 - ); 291 - 292 - // left side: 7-8-9 and 4-5-6 stacked 293 - ops.push( 294 - open("", { layout: { direction: "ttb", gap: GAP } }), 295 - ); 296 - row(ops, [ 297 - { label: "7", code: "Numpad7", width: 6 }, 298 - { label: "8", code: "Numpad8", width: 6 }, 299 - { label: "9", code: "Numpad9", width: 6 }, 300 - ], ctx); 301 - row(ops, [ 302 - { label: "4", code: "Numpad4", width: 6 }, 303 - { label: "5", code: "Numpad5", width: 6 }, 304 - { label: "6", code: "Numpad6", width: 6 }, 305 - ], ctx); 306 - ops.push(close()); 307 - 308 - // + spanning both rows 309 - key(ops, { label: "+", code: "NumpadAdd" }, ctx); 310 - 311 - ops.push(close()); 312 - 313 - // rows 4-5 grouped horizontally so Enter spans both 314 - ops.push( 315 - open("", { layout: { direction: "ltr", gap: GAP } }), 316 - ); 317 - 318 - // left side: 1-2-3 and 0-. stacked 319 - ops.push( 320 - open("", { layout: { direction: "ttb", gap: GAP } }), 321 - ); 322 - row(ops, [ 323 - { label: "1", code: "Numpad1", width: 6 }, 324 - { label: "2", code: "Numpad2", width: 6 }, 325 - { label: "3", code: "Numpad3", width: 6 }, 326 - ], ctx); 327 - row(ops, [ 328 - { label: "0", code: "Numpad0", width: 13 }, 329 - { label: ".", code: "NumpadDecimal", width: 6 }, 330 - ], ctx); 331 - ops.push(close()); 332 - 333 - // Enter spanning both rows 334 - key(ops, { label: "Ent", code: "NumpadEnter" }, ctx); 335 - 336 - ops.push(close()); 337 - 338 - ops.push(close()); 339 - } 340 - 341 - function toggle(ops: Op[], enabled: boolean, name: string): void { 342 - let indicator = enabled 343 - ? "\u25cf\u2500\u2500\u2500" 344 - : "\u2500\u2500\u2500\u25cb"; 345 - ops.push( 346 - open("", { 347 - layout: { 348 - direction: "ltr", 349 - height: fixed(1), 350 - gap: 1, 351 - }, 352 - }), 353 - text(indicator, { color: enabled ? on : dim }), 354 - text(name, { color: enabled ? label : dim }), 355 - close(), 356 - ); 357 - } 358 - 359 - const flagNames: 360 - (keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] = 361 - [ 362 - "Disambiguate escape codes", 363 - "Report event types", 364 - "Report alternate keys", 365 - "Report all keys as escapes", 366 - "Report associated text", 367 - ]; 368 - 369 - const logEntries: { key: string; name: keyof EventFilter }[] = [ 370 - { key: "a", name: "keydown" }, 371 - { key: "b", name: "keyup" }, 372 - { key: "c", name: "keyrepeat" }, 373 - { key: "d", name: "mousedown" }, 374 - { key: "e", name: "mouseup" }, 375 - { key: "f", name: "mousemove" }, 376 - { key: "g", name: "wheel" }, 377 - { key: "h", name: "resize" }, 378 - { key: "i", name: "pointerenter" }, 379 - { key: "j", name: "pointerleave" }, 380 - { key: "k", name: "pointerclick" }, 381 - ]; 382 - 383 - function logToggle( 384 - ops: Op[], 385 - entries: typeof logEntries, 386 - ctx: AppContext, 387 - ): void { 388 - for (let entry of entries) { 389 - ops.push( 390 - open(`log:${entry.name}`, { 391 - layout: { direction: "ltr", height: fixed(1), gap: 1 }, 392 - }), 393 - ); 394 - ops.push(text(`${entry.key}.`, { color: dim })); 395 - toggle(ops, ctx.log[entry.name], entry.name); 396 - ops.push(close()); 397 - } 398 - } 399 - 400 - function configPanel(ops: Op[], ctx: AppContext): void { 401 - let color = ctx.mode === "config" ? active : rgba(0, 0, 0, 0); 402 - ops.push(open("config", { 403 - layout: { 404 - direction: "ltr", 405 - gap: 3, 406 - padding: { left: 1, right: 1, top: 1, bottom: 1 }, 407 - }, 408 - border: { color, left: 1, right: 1, top: 1, bottom: 1 }, 409 - })); 410 - 411 - // keyboard protocol level column 412 - ops.push(open("protocol-level", { layout: { direction: "ttb", gap: 1 } })); 413 - ops.push( 414 - open("", { layout: { height: fixed(1) } }), 415 - text("Keyboard Protocol Level", { color: highlight }), 416 - close(), 417 - ); 418 - for (let i = 0; i < flagNames.length; i++) { 419 - let name = flagNames[i]; 420 - ops.push( 421 - open(`flag:${name}`, { 422 - layout: { direction: "ltr", height: fixed(1), gap: 1 }, 423 - }), 424 - ); 425 - ops.push(text(`${i + 1}.`, { color: dim })); 426 - toggle(ops, ctx[name], name); 427 - ops.push(close()); 428 - } 429 - ops.push(close()); 430 - 431 - // log events column 1 432 - let col1 = logEntries.slice(0, 6); 433 - let col2 = logEntries.slice(6); 434 - 435 - ops.push(open("log-events", { layout: { direction: "ttb", gap: 1 } })); 436 - ops.push( 437 - open("", { layout: { height: fixed(1) } }), 438 - text("Log Events", { color: highlight }), 439 - close(), 440 - ); 441 - logToggle(ops, col1, ctx); 442 - ops.push(close()); 443 - 444 - // log events column 2 445 - ops.push(open("log-events-2", { layout: { direction: "ttb", gap: 1 } })); 446 - ops.push( 447 - open("", { layout: { height: fixed(1) } }), 448 - close(), 449 - ); 450 - logToggle(ops, col2, ctx); 451 - ops.push(close()); 452 - 453 - ops.push(close()); 454 - } 455 - 456 - function keyboard(ctx: AppContext): Op[] { 457 - let ops: Op[] = []; 458 - 459 - // root 460 - ops.push( 461 - open("root", { 462 - layout: { 463 - width: grow(), 464 - height: grow(), 465 - direction: "ttb", 466 - alignX: 2, 467 - alignY: 2, 468 - padding: { left: 2, top: 1 }, 469 - }, 470 - }), 471 - ); 472 - 473 - // keyboard + toggles wrapper 474 - ops.push( 475 - open("", { layout: { direction: "ttb" } }), 476 - ); 477 - 478 - // badges + config row 479 - ops.push( 480 - open("", { 481 - layout: { 482 - width: grow(), 483 - direction: "ltr", 484 - alignY: 0, 485 - padding: { bottom: 1 }, 486 - }, 487 - }), 488 - ); 489 - 490 - // badges column (left, bottom-aligned) 491 - let badgeBg = ctx.mode === "input" ? rgba(40, 120, 200) : rgba(200, 120, 40); 492 - let badgeLabel = ctx.mode === "input" ? "input" : "config"; 493 - let badgeHint = ctx.mode === "input" 494 - ? "Ctrl+X Ctrl+X to enter config" 495 - : "Set flags with keys [0-5], Enter to save"; 496 - let mouseBg = ctx["Capture mouse events"] 497 - ? rgba(40, 180, 80) 498 - : rgba(80, 80, 80); 499 - let mouseLabel = ctx["Capture mouse events"] ? "capture" : "system"; 500 - ops.push( 501 - open("badges", { 502 - layout: { direction: "ttb", gap: 1, padding: { top: 1 } }, 503 - }), 504 - open("badge:mode", { 505 - layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } }, 506 - }), 507 - open("", { 508 - layout: { padding: { left: 1, right: 1 } }, 509 - bg: rgba(60, 60, 60), 510 - }), 511 - text("mode", { color: rgba(220, 220, 220) }), 512 - close(), 513 - open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }), 514 - text(badgeLabel, { color: rgba(255, 255, 255) }), 515 - close(), 516 - text(` ${badgeHint}`, { color: dim }), 517 - close(), 518 - open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }), 519 - open("", { 520 - layout: { padding: { left: 1, right: 1 } }, 521 - bg: rgba(60, 60, 60), 522 - }), 523 - text("mouse", { color: rgba(220, 220, 220) }), 524 - close(), 525 - open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }), 526 - text(mouseLabel, { color: rgba(255, 255, 255) }), 527 - close(), 528 - text(" Ctrl+X Ctrl+M to toggle", { color: dim }), 529 - close(), 530 - close(), 531 - ); 532 - 533 - // config panel (right) 534 - ops.push( 535 - open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } }), 536 - ); 537 - configPanel(ops, ctx); 538 - ops.push(close()); 539 - 540 - ops.push(close()); // badges + config row 541 - 542 - // three keyboard groups side by side, bottom-aligned 543 - let kbColor = ctx.mode === "input" ? active : rgba(0, 0, 0, 0); 544 - ops.push( 545 - open("keyboard", { 546 - layout: { 547 - direction: "ltr", 548 - gap: 3, 549 - alignY: 1, 550 - padding: { left: 1, right: 1, top: 1, bottom: 1 }, 551 - }, 552 - border: { color: kbColor, left: 1, right: 1, top: 1, bottom: 1 }, 553 - }), 554 - ); 555 - 556 - mainKeys(ops, ctx); 557 - navKeys(ops, ctx); 558 - numpad(ops, ctx); 559 - 560 - ops.push(close()); 561 - 562 - ops.push(close()); // keyboard + toggles wrapper 563 - 564 - // raw event display 565 - ops.push( 566 - open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } }), 567 - text(ctx.logged ? JSON.stringify(ctx.logged) : "Press any key...", { 568 - color: highlight, 569 - }), 570 - close(), 571 - ); 572 - 573 - ops.push(close()); 574 - 575 - return ops; 576 - } 577 - 578 - function ttyFlags(ctx: AppContext): Setting { 579 - let parts: Setting[] = []; 580 - let bits = 0; 581 - if (ctx["Disambiguate escape codes"]) bits |= 1; 582 - if (ctx["Report event types"]) bits |= 2; 583 - if (ctx["Report alternate keys"]) bits |= 4; 584 - if (ctx["Report all keys as escapes"]) bits |= 8; 585 - if (ctx["Report associated text"]) bits |= 16; 586 - parts.push(progressiveInput(bits)); 587 - if (ctx["Capture mouse events"]) { 588 - parts.push(mouseTracking()); 589 - } 590 - return settings(...parts); 591 - } 592 - 593 65 await main(function* () { 594 66 let { columns, rows } = Deno.stdout.isTerminal() 595 67 ? Deno.consoleSize() ··· 604 76 let term = yield* until(createTerm({ width: columns, height: rows })); 605 77 606 78 let tty = settings(alternateBuffer(), cursor(false)); 607 - Deno.stdout.writeSync(tty.apply); 79 + writeAllSync(tty.apply); 608 80 609 - let modality = recognizer(); 81 + let modality = demo.recognizer(); 610 82 let context = modality.next().value; 611 83 612 - let flags = ttyFlags(context); 613 - Deno.stdout.writeSync(flags.apply); 84 + let flags = demo.ttyFlags(context); 85 + writeAllSync(flags.apply); 614 86 615 87 yield* ensure(() => { 616 88 // Restore so Backspace and normal shell editing work after exit. 617 89 if (Deno.stdin.isTerminal()) { 618 90 Deno.stdin.setRaw(false); 619 91 } 620 - Deno.stdout.writeSync(flags.revert); 621 - Deno.stdout.writeSync(tty.revert); 92 + writeAllSync(flags.revert); 93 + writeAllSync(tty.revert); 622 94 }); 623 95 624 - let { output } = term.render(keyboard(context)); 96 + let { output } = term.render(demo.keyboard(context)); 625 97 626 - writeOutput(output); 98 + writeAllSync(output); 627 99 628 100 let pointer = { 629 101 events: createChannel<PointerEvent, void>(), ··· 649 121 context = { ...context, logged: prev }; 650 122 } 651 123 652 - Deno.stdout.writeSync(flags.revert); 653 - flags = ttyFlags(context); 654 - Deno.stdout.writeSync(flags.apply); 124 + writeAllSync(flags.revert); 125 + flags = demo.ttyFlags(context); 126 + writeAllSync(flags.apply); 655 127 656 128 if (context["Capture mouse events"]) { 657 129 if ("x" in event) { ··· 665 137 pointer.state = undefined; 666 138 } 667 139 668 - let { output, events } = term.render(keyboard(context), { 140 + let { output, events } = term.render(demo.keyboard(context), { 669 141 pointer: pointer.state, 670 142 }); 671 143 ··· 673 145 yield* pointer.events.send(event); 674 146 } 675 147 676 - writeOutput(output); 148 + writeAllSync(output); 677 149 678 150 yield* each.next(); 679 151 } 680 152 }); 681 153 682 - function* recognizer(): Iterator<AppContext, never, InputEvent | PointerEvent> { 683 - let current: AppContext = { 684 - mode: "input", 685 - "Disambiguate escape codes": true, 686 - "Report event types": true, 687 - "Report alternate keys": true, 688 - "Report all keys as escapes": true, 689 - "Report associated text": true, 690 - "Capture mouse events": true, 691 - log: { 692 - keydown: true, 693 - keyrepeat: false, 694 - keyup: false, 695 - mousedown: false, 696 - mouseup: false, 697 - mousemove: false, 698 - wheel: true, 699 - resize: true, 700 - pointerenter: false, 701 - pointerleave: false, 702 - pointerclick: true, 703 - }, 704 - entered: new Set(), 705 - event: null, 706 - logged: null, 707 - }; 708 - 709 - let event = yield current; 710 - 711 - let mode = inputmode({ ...current, event }); 712 - 713 - while (true) { 714 - mode = yield* mode; 715 - } 716 - } 717 - 718 - type Mode = Iterable<AppContext, Mode, InputEvent | PointerEvent>; 719 - 720 - function* inputmode(context: AppContext): Mode { 721 - context = { ...context, mode: "input" }; 722 - let event = context.event ? context.event : yield context; 723 - while (true) { 724 - context = { ...context, event }; 725 - if (event.type === "keydown" && event.key === "x" && event.ctrl) { 726 - let next = yield context; 727 - while (next.type !== "keydown") { 728 - context = { ...context, event: next }; 729 - next = yield context; 730 - } 731 - context = { ...context, event: next }; 732 - if (next.key === "x" && next.ctrl) { 733 - return configmode({ 734 - ...context, 735 - event: null, 736 - }); 737 - } else if (next.key === "m" && next.ctrl) { 738 - context = { 739 - ...context, 740 - "Capture mouse events": !context["Capture mouse events"], 741 - event: null, 742 - }; 743 - event = yield context; 744 - continue; 745 - } 746 - } 747 - event = yield context; 748 - } 749 - } 750 - 751 - function* configmode(context: AppContext): Mode { 752 - context = { ...context, mode: "config" }; 753 - let event = yield context; 754 - while (true) { 755 - if (event.type === "keydown" && event.key === "Enter") { 756 - return inputmode({ ...context, event: null }); 757 - } 758 - if (event.type === "keydown") { 759 - let k = (event as KeyEvent).key; 760 - let entry = logEntries.find((e) => e.key === k); 761 - if (entry) { 762 - context = { 763 - ...context, 764 - log: { ...context.log, [entry.name]: !context.log[entry.name] }, 765 - }; 766 - } 767 - if ("012345".indexOf(event.key) >= 0) { 768 - context = { ...context }; 769 - context["Report associated text"] = false; 770 - context["Report all keys as escapes"] = false; 771 - context["Report alternate keys"] = false; 772 - context["Report event types"] = false; 773 - context["Disambiguate escape codes"] = false; 774 - switch (event.key) { 775 - case "5": 776 - context["Report associated text"] = true; 777 - case "4": 778 - context["Report all keys as escapes"] = true; 779 - case "3": 780 - context["Report alternate keys"] = true; 781 - case "2": 782 - context["Report event types"] = true; 783 - case "1": 784 - context["Disambiguate escape codes"] = true; 785 - break; 786 - case "0": 787 - break; 788 - } 789 - } 790 - } 791 - event = yield context; 792 - } 793 - } 794 - 795 154 function merge<A, B, TClose>( 796 155 a: Stream<A, TClose>, 797 156 b: Stream<B, TClose>, ··· 823 182 pointerleave: boolean; 824 183 pointerclick: boolean; 825 184 }; 826 - 827 - type AppContext = { 828 - mode: "input" | "config"; 829 - event: InputEvent | PointerEvent | null; 830 - logged: InputEvent | PointerEvent | null; 831 - log: EventFilter; 832 - entered: Set<string>; 833 - ["Disambiguate escape codes"]: boolean; 834 - ["Report event types"]: boolean; 835 - ["Report alternate keys"]: boolean; 836 - ["Report all keys as escapes"]: boolean; 837 - ["Report associated text"]: boolean; 838 - ["Capture mouse events"]: boolean; 839 - };
+27 -10
term.ts
··· 41 41 const WINDOWS_WRAP_DISABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x6c]); 42 42 const WINDOWS_WRAP_ENABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x68]); 43 43 44 + function windowsFullscreenHandlingDisabled(): boolean { 45 + let diagnostics = (globalThis as typeof globalThis & { 46 + __claytermDiagnostics__?: { 47 + disableWindowsFullscreenHandling?: boolean; 48 + }; 49 + }).__claytermDiagnostics__; 50 + 51 + return diagnostics?.disableWindowsFullscreenHandling === true; 52 + } 53 + 44 54 function normalizeWindowsLineOutput(output: Uint8Array): Uint8Array { 45 - // Windows fullscreen line-mode output needs an explicit home cursor move and 46 - // CRLF row separators; bare LF can leave the cursor in the wrong column and 47 - // visually clip later rows in some terminal stacks. 55 + // Empirically, Deno-on-Windows fullscreen redraws were more reliable when 56 + // line-mode output began with a home cursor move and used CRLF row 57 + // separators. Bare LF left later rows visually clipped in the tested 58 + // terminal stack, while localized redraws could still reveal the content. 48 59 let extra = 0; 49 60 for (let i = 0; i < output.length; i++) { 50 61 if (output[i] === 0x0a && (i === 0 || output[i - 1] !== 0x0d)) { ··· 79 90 } 80 91 81 92 function wrapWindowsFullscreenOutput(output: Uint8Array): Uint8Array { 82 - // Disabling autowrap around a fullscreen frame avoids Windows terminal 83 - // redraw quirks observed at the right edge. 93 + // Preserve an explicit wrap-state boundary around fullscreen frames on 94 + // Windows. In the tested Deno path this avoided redraw glitches at the right 95 + // edge even though the same scene rendered cleanly in Node. 84 96 // xterm defines CSI ? 7 h / CSI ? 7 l as auto-wrap on/off. 85 97 let wrapped = new Uint8Array( 86 98 WINDOWS_WRAP_DISABLE.length + output.length + WINDOWS_WRAP_ENABLE.length, ··· 123 135 render(ops: Op[], options?: RenderOptions): RenderResult; 124 136 } 125 137 138 + let runtimeIsWindows = (globalThis as typeof globalThis & { 139 + Deno?: { build?: { os?: string } }; 140 + }).Deno?.build?.os === "windows"; 141 + 126 142 export async function createTerm(options: TermOptions): Promise<Term> { 127 143 let { width, height } = options; 128 144 let native = await createTermNative(width, height); ··· 138 154 let mode = options?.mode === "line" ? 1 : 0; 139 155 let row = options?.row ?? 1; 140 156 let autoLineMode = false; 141 - let windowsFullscreen = row === 1 && Deno.build.os === "windows"; 157 + let windowsFullscreen = row === 1 && runtimeIsWindows && 158 + !windowsFullscreenHandlingDisabled(); 142 159 143 - // Windows terminals have historically been less reliable with many 144 - // absolute cursor CUP updates in full-screen diff mode. Use the 145 - // line-oriented render path by default on Windows for fullscreen 146 - // layouts to improve redraw reliability. 160 + // Use the line-oriented render path by default for Windows fullscreen 161 + // renders. This was required for Deno to avoid 162 + // clipped rows during fullscreen redraw, while the equivalent Node path 163 + // did not show the same failure. 147 164 if (mode === 0 && options?.mode === undefined && windowsFullscreen) { 148 165 mode = 1; 149 166 autoLineMode = true;