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

✨ add composable terminal settings with automatic revert

Introduce a Setting type that pairs apply/revert escape sequences,
ensuring terminal state is always cleanly restored. Refactor the
keyboard demo to use settings() for alternate buffer, cursor, mouse
tracking, and progressive input.

Charles Lowell (Apr 3, 2026, 5:06 PM -0500) dde5727e b555a1bf

+168 -15
+27 -15
demo/keyboard.ts
··· 22 22 rgba, 23 23 text, 24 24 } from "../mod.ts"; 25 + import { 26 + alternateBuffer, 27 + cursor, 28 + mouseTracking, 29 + progressiveInput, 30 + type Setting, 31 + settings, 32 + } from "../settings.ts"; 25 33 import { useInput } from "./use-input.ts"; 26 34 import { useStdin } from "./use-stdin.ts"; 27 35 ··· 539 547 return ops; 540 548 } 541 549 542 - let encoder = new TextEncoder(); 543 - let esc = (s: string) => Deno.stdout.writeSync(encoder.encode(s)); 544 - 545 - function ttyFlags(ctx: AppContext): Uint8Array { 550 + function ttyFlags(ctx: AppContext): Setting { 551 + let parts: Setting[] = []; 546 552 let bits = 0; 547 553 if (ctx["Disambiguate escape codes"]) bits |= 1; 548 554 if (ctx["Report event types"]) bits |= 2; 549 555 if (ctx["Report alternate keys"]) bits |= 4; 550 556 if (ctx["Report all keys as escapes"]) bits |= 8; 551 557 if (ctx["Report associated text"]) bits |= 16; 552 - let mouse = ctx["Capture mouse events"] 553 - ? "\x1b[?1003h\x1b[?1006h" 554 - : "\x1b[?1003l\x1b[?1006l"; 555 - return encoder.encode(`\x1b[<u\x1b[>${bits}u${mouse}`); 558 + parts.push(progressiveInput(bits)); 559 + if (ctx["Capture mouse events"]) { 560 + parts.push(mouseTracking()); 561 + } 562 + return settings(...parts); 556 563 } 557 564 558 565 await main(function* () { ··· 567 574 568 575 let term = yield* until(createTerm({ width: columns, height: rows })); 569 576 570 - esc("\x1b[?1049h\x1b[?25l\x1b[>3u"); 571 - yield* ensure(() => { 572 - esc("\x1b[?1003l\x1b[?1006l\x1b[<u\x1b[?25h\x1b[?1049l"); 573 - }); 577 + let tty = settings(alternateBuffer(), cursor(false)); 578 + Deno.stdout.writeSync(tty.apply); 574 579 575 580 let modality = recognizer(); 581 + let context = modality.next().value; 576 582 577 - let context = modality.next().value; 583 + let flags = ttyFlags(context); 584 + Deno.stdout.writeSync(flags.apply); 578 585 579 - Deno.stdout.writeSync(ttyFlags(context)); 586 + yield* ensure(() => { 587 + Deno.stdout.writeSync(flags.revert); 588 + Deno.stdout.writeSync(tty.revert); 589 + }); 580 590 581 591 let { output } = term.render(keyboard(context)); 582 592 ··· 606 616 context = { ...context, logged: prev }; 607 617 } 608 618 609 - Deno.stdout.writeSync(ttyFlags(context)); 619 + Deno.stdout.writeSync(flags.revert); 620 + flags = ttyFlags(context); 621 + Deno.stdout.writeSync(flags.apply); 610 622 611 623 if (context["Capture mouse events"]) { 612 624 if ("x" in event) {
+67
settings.ts
··· 1 + export interface Setting { 2 + apply: Uint8Array; 3 + revert: Uint8Array; 4 + } 5 + 6 + export function settings(...sequence: Setting[]): Setting { 7 + return { 8 + apply: concat(sequence.map((s) => s.apply)), 9 + revert: concat(sequence.map((s) => s.revert).reverse()), 10 + }; 11 + } 12 + 13 + export function alternateBuffer(): Setting { 14 + return { 15 + apply: csi("?1049h"), 16 + revert: csi("?1049l"), 17 + }; 18 + } 19 + 20 + export function cursor(visible: boolean): Setting { 21 + if (visible) { 22 + return { 23 + apply: csi("?25h"), 24 + revert: csi("?25l"), 25 + }; 26 + } else { 27 + return { 28 + apply: csi("?25l"), 29 + revert: csi("?25h"), 30 + }; 31 + } 32 + } 33 + 34 + export function progressiveInput(level: number): Setting { 35 + return { 36 + apply: csi(`>${level}u`), 37 + revert: csi("<u"), 38 + }; 39 + } 40 + 41 + export function mouseTracking(): Setting { 42 + return { 43 + apply: concat([csi("?1003h"), csi("?1006h")]), 44 + revert: concat([csi("?1006l"), csi("?1003l")]), 45 + }; 46 + } 47 + 48 + let encoder = new TextEncoder(); 49 + 50 + function encode(str: string): Uint8Array { 51 + return encoder.encode(str); 52 + } 53 + 54 + function csi(str: string): Uint8Array { 55 + return encode(`\x1b[${str}`); 56 + } 57 + 58 + function concat(arrays: Uint8Array[]): Uint8Array { 59 + let length = arrays.reduce((sum, a) => sum + a.length, 0); 60 + let result = new Uint8Array(length); 61 + let offset = 0; 62 + for (let a of arrays) { 63 + result.set(a, offset); 64 + offset += a.length; 65 + } 66 + return result; 67 + }
+74
test/settings.test.ts
··· 1 + import { describe, expect, it } from "./suite.ts"; 2 + import { 3 + alternateBuffer, 4 + cursor, 5 + mouseTracking, 6 + progressiveInput, 7 + settings, 8 + } from "../settings.ts"; 9 + 10 + function str(bytes: Uint8Array): string { 11 + return new TextDecoder().decode(bytes); 12 + } 13 + 14 + describe("settings", () => { 15 + describe("alternateBuffer", () => { 16 + it("applies with enter and removes with leave", () => { 17 + let s = alternateBuffer(); 18 + expect(str(s.apply)).toBe("\x1b[?1049h"); 19 + expect(str(s.revert)).toBe("\x1b[?1049l"); 20 + }); 21 + }); 22 + 23 + describe("cursor", () => { 24 + it("hides on apply when visible is false", () => { 25 + let s = cursor(false); 26 + expect(str(s.apply)).toBe("\x1b[?25l"); 27 + expect(str(s.revert)).toBe("\x1b[?25h"); 28 + }); 29 + 30 + it("shows on apply when visible is true", () => { 31 + let s = cursor(true); 32 + expect(str(s.apply)).toBe("\x1b[?25h"); 33 + expect(str(s.revert)).toBe("\x1b[?25l"); 34 + }); 35 + }); 36 + 37 + describe("progressiveInput", () => { 38 + it("pushes the given level and pops on remove", () => { 39 + let s = progressiveInput(3); 40 + expect(str(s.apply)).toBe("\x1b[>3u"); 41 + expect(str(s.revert)).toBe("\x1b[<u"); 42 + }); 43 + }); 44 + 45 + describe("mouseTracking", () => { 46 + it("enables any-event tracking with SGR encoding", () => { 47 + let s = mouseTracking(); 48 + expect(str(s.apply)).toBe("\x1b[?1003h\x1b[?1006h"); 49 + expect(str(s.revert)).toBe("\x1b[?1006l\x1b[?1003l"); 50 + }); 51 + }); 52 + 53 + describe("settings()", () => { 54 + it("concatenates apply in order", () => { 55 + let s = settings(alternateBuffer(), cursor(false)); 56 + expect(str(s.apply)).toBe("\x1b[?1049h\x1b[?25l"); 57 + }); 58 + 59 + it("concatenates remove in reverse order", () => { 60 + let s = settings(alternateBuffer(), cursor(false)); 61 + expect(str(s.revert)).toBe("\x1b[?25h\x1b[?1049l"); 62 + }); 63 + 64 + it("composes multiple settings", () => { 65 + let s = settings( 66 + alternateBuffer(), 67 + cursor(false), 68 + progressiveInput(3), 69 + ); 70 + expect(str(s.apply)).toBe("\x1b[?1049h\x1b[?25l\x1b[>3u"); 71 + expect(str(s.revert)).toBe("\x1b[<u\x1b[?25h\x1b[?1049l"); 72 + }); 73 + }); 74 + });