[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.

fullscreen wrap not required

Jacob Bolda (May 23, 2026, 1:23 AM -0500) 4e2fb431 36fe005c

+52 -156
+14 -26
demo/keyboard-node.ts
··· 31 31 pending?: { delay: number }; 32 32 }; 33 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 - }; 34 + let demo = createKeyboardDemo( 35 + { 36 + close, 37 + fixed, 38 + grow, 39 + mouseTracking, 40 + open, 41 + progressiveInput, 42 + rgba, 43 + settings, 44 + text, 45 + } as Parameters<typeof createKeyboardDemo>[0], 46 + ); 59 47 60 48 let term: Awaited<ReturnType<typeof createTerm>> | null = null; 61 49 let input: NodeInput | null = null; ··· 251 239 cleanup(); 252 240 console.error(error); 253 241 process.exit(1); 254 - } 242 + }
+25 -9
demo/keyboard-shared.ts
··· 1 + // deno-lint-ignore-file no-fallthrough 1 2 import type { InputEvent, KeyEvent, Op, PointerEvent } from "../mod.ts"; 2 3 import type { SizingAxis } from "../ops.ts"; 3 4 import type { Setting } from "../settings.ts"; ··· 442 443 let mouseLabel = context["Capture mouse events"] ? "capture" : "system"; 443 444 444 445 ops.push( 445 - open("badges", { layout: { direction: "ttb", gap: 1, padding: { top: 1 } } }), 446 + open("badges", { 447 + layout: { direction: "ttb", gap: 1, padding: { top: 1 } }, 448 + }), 446 449 open("badge:mode", { 447 450 layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } }, 448 451 }), ··· 472 475 close(), 473 476 ); 474 477 475 - ops.push(open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } })); 478 + ops.push( 479 + open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } }), 480 + ); 476 481 configPanel(ops, context); 477 482 ops.push(close()); 478 483 ops.push(close()); ··· 495 500 ops.push(close()); 496 501 ops.push(close()); 497 502 498 - ops.push(open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } })); 503 + ops.push( 504 + open("event-log", { layout: { height: fixed(1), padding: { top: 1 } } }), 505 + ); 499 506 ops.push( 500 - text(context.logged ? JSON.stringify(context.logged) : "Press any key...", { 501 - color: highlight, 502 - }), 507 + text( 508 + context.logged ? JSON.stringify(context.logged) : "Press any key...", 509 + { 510 + color: highlight, 511 + }, 512 + ), 503 513 ); 504 514 ops.push(close()); 505 515 ops.push(close()); ··· 550 560 }; 551 561 } 552 562 553 - function* recognizer(): Iterator<AppContext, never, InputEvent | PointerEvent> { 563 + function* recognizer(): Iterator< 564 + AppContext, 565 + never, 566 + InputEvent | PointerEvent 567 + > { 554 568 let current = createInitialContext(); 555 569 let event = yield current; 556 570 let mode = inputmode({ ...current, event }); ··· 597 611 } 598 612 if (event.type === "keydown") { 599 613 let keyEvent = event as KeyEvent; 600 - let entry = logEntries.find((candidate) => candidate.key === keyEvent.key); 614 + let entry = logEntries.find((candidate) => 615 + candidate.key === keyEvent.key 616 + ); 601 617 if (entry) { 602 618 context = { 603 619 ...context, ··· 637 653 recognizer, 638 654 ttyFlags, 639 655 }; 640 - } 656 + }
+2 -22
demo/keyboard.ts
··· 8 8 type Stream, 9 9 until, 10 10 } from "effection"; 11 - import { 12 - createTerm, 13 - type PointerEvent, 14 - } from "../mod.ts"; 15 - import { 16 - alternateBuffer, 17 - settings, 18 - } from "../settings.ts"; 11 + import { createTerm, type PointerEvent } from "../mod.ts"; 12 + import { alternateBuffer, settings } from "../settings.ts"; 19 13 import { close, fixed, grow, open, rgba, text } from "../mod.ts"; 20 14 import { cursor, mouseTracking, progressiveInput } from "../settings.ts"; 21 15 import { createKeyboardDemo } from "./keyboard-shared.ts"; ··· 33 27 settings, 34 28 text, 35 29 }); 36 - 37 - let diagnostics = { 38 - disableWindowsFullscreenHandling: Deno.args.includes( 39 - "--no-windows-fullscreen-fix", 40 - ), 41 - }; 42 - 43 - (globalThis as typeof globalThis & { 44 - __claytermDiagnostics__?: { 45 - disableWindowsFullscreenHandling?: boolean; 46 - }; 47 - }).__claytermDiagnostics__ = { 48 - disableWindowsFullscreenHandling: diagnostics.disableWindowsFullscreenHandling, 49 - }; 50 30 51 31 function writeAllSync(output: Uint8Array): void { 52 32 // Deno's stdout writes are not guaranteed to drain the full buffer in one
+10
deno.lock
··· 22 22 "jsr:@ts-morph/common@0.27": "0.27.0", 23 23 "npm:@sinclair/typebox@*": "0.34.48", 24 24 "npm:@sinclair/typebox@0.34": "0.34.48", 25 + "npm:@types/node@*": "25.9.1", 25 26 "npm:effection@^4.0.2": "4.0.2", 26 27 "npm:valrs@*": "0.1.0" 27 28 }, ··· 112 113 "@sinclair/typebox@0.34.48": { 113 114 "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==" 114 115 }, 116 + "@types/node@25.9.1": { 117 + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", 118 + "dependencies": [ 119 + "undici-types" 120 + ] 121 + }, 115 122 "effection@4.0.2": { 116 123 "integrity": "sha512-O8WMGP10nPuJDwbNGILcaCNWS+CvDYjcdsUSD79nWZ+WtUQ8h1MEV7JJwCSZCSeKx8+TdEaZ/8r6qPTR2o/o8w==" 124 + }, 125 + "undici-types@7.24.6": { 126 + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==" 117 127 }, 118 128 "valrs@0.1.0": { 119 129 "integrity": "sha512-BqVkjx3qhsRLHerblLDoqEx0OEx7ms0DB6LPv40oWkMfFKUVKrqVuklaGdrPrHyubC5hSHYfEtUiQXrCkC6xHQ=="
-87
term.ts
··· 38 38 bounds: BoundingBox; 39 39 } 40 40 41 - const WINDOWS_WRAP_DISABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x6c]); 42 - const WINDOWS_WRAP_ENABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x68]); 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 - 54 - function normalizeWindowsLineOutput(output: Uint8Array): Uint8Array { 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. 59 - let extra = 0; 60 - for (let i = 0; i < output.length; i++) { 61 - if (output[i] === 0x0a && (i === 0 || output[i - 1] !== 0x0d)) { 62 - extra++; 63 - } 64 - } 65 - 66 - let prefix = new Uint8Array([ 67 - ...WINDOWS_WRAP_DISABLE, 68 - 0x1b, 69 - 0x5b, 70 - 0x48, 71 - ]); 72 - let suffix = WINDOWS_WRAP_ENABLE; 73 - let normalized = new Uint8Array( 74 - prefix.length + output.length + extra + suffix.length, 75 - ); 76 - normalized.set(prefix, 0); 77 - 78 - let offset = prefix.length; 79 - for (let i = 0; i < output.length; i++) { 80 - let byte = output[i]; 81 - if (byte === 0x0a && (i === 0 || output[i - 1] !== 0x0d)) { 82 - normalized[offset++] = 0x0d; 83 - } 84 - normalized[offset++] = byte; 85 - } 86 - 87 - normalized.set(suffix, offset); 88 - 89 - return normalized as Uint8Array; 90 - } 91 - 92 - function wrapWindowsFullscreenOutput(output: Uint8Array): Uint8Array { 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. 96 - // xterm defines CSI ? 7 h / CSI ? 7 l as auto-wrap on/off. 97 - let wrapped = new Uint8Array( 98 - WINDOWS_WRAP_DISABLE.length + output.length + WINDOWS_WRAP_ENABLE.length, 99 - ); 100 - wrapped.set(WINDOWS_WRAP_DISABLE, 0); 101 - wrapped.set(output, WINDOWS_WRAP_DISABLE.length); 102 - wrapped.set(WINDOWS_WRAP_ENABLE, WINDOWS_WRAP_DISABLE.length + output.length); 103 - return wrapped; 104 - } 105 - 106 41 const ERROR_TYPES = [ 107 42 "TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED", 108 43 "ARENA_CAPACITY_EXCEEDED", ··· 135 70 render(ops: Op[], options?: RenderOptions): RenderResult; 136 71 } 137 72 138 - let runtimeIsWindows = (globalThis as typeof globalThis & { 139 - Deno?: { build?: { os?: string } }; 140 - }).Deno?.build?.os === "windows"; 141 - 142 73 export async function createTerm(options: TermOptions): Promise<Term> { 143 74 let { width, height } = options; 144 75 let native = await createTermNative(width, height); ··· 153 84 let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength); 154 85 let mode = options?.mode === "line" ? 1 : 0; 155 86 let row = options?.row ?? 1; 156 - let autoLineMode = false; 157 - let windowsFullscreen = row === 1 && runtimeIsWindows && 158 - !windowsFullscreenHandlingDisabled(); 159 - 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. 164 - if (mode === 0 && options?.mode === undefined && windowsFullscreen) { 165 - mode = 1; 166 - autoLineMode = true; 167 - } 168 87 169 88 native.reduce(statePtr, opsBuf, len, mode, row); 170 89 ··· 178 97 native.output(statePtr), 179 98 native.length(statePtr), 180 99 ); 181 - 182 - if (autoLineMode) { 183 - output = normalizeWindowsLineOutput(output); 184 - } else if (windowsFullscreen) { 185 - output = wrapWindowsFullscreenOutput(output); 186 - } 187 100 188 101 let current = new Set( 189 102 options?.pointer ? native.getPointerOverIds() : [],
+1 -12
test/term.test.ts
··· 130 130 │ │ 131 131 │ │ 132 132 └──────────────────┘`.trim()); 133 - 134 - if (Deno.build.os === "windows") { 135 - expect(out.startsWith("\x1b[?7l")).toBe(true); 136 - expect(out.endsWith("\x1b[?7h")).toBe(true); 137 - } 138 133 }); 139 134 140 135 it("primes front buffer for subsequent diff render", async () => { ··· 152 147 │ │ 153 148 └──────────────────┘`.trim()); 154 149 155 - if (Deno.build.os === "windows") { 156 - expect(second.startsWith("\x1b[?7l\x1b[H")).toBe(true); 157 - expect(second).toContain("\r\n"); 158 - expect(second.endsWith("\x1b[?7h")).toBe(true); 159 - } else { 160 - expect(second.length).toBeLessThan(first.length); 161 - } 150 + expect(second.length).toBeLessThan(first.length); 162 151 }); 163 152 }); 164 153