[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 termcodes module and make CursorEvent 1-based

Rename csi.ts to termcodes.ts and add ESC(), SHOWCURSOR(),
HIDECURSOR(), ALTSCREEN(), and MAINSCREEN() helpers. Make
CursorEvent.row/column 1-based to match DSR native format.
Replace all raw escape sequences in the demo with termcodes.
Use DECSC/DECRC (ESC 7/8) for cursor save/restore instead of
SCO (CSI s/u).

Charles Lowell (Apr 10, 2026, 4:06 PM -0500) 7b801bb8 c09a67ed

+159 -55
+28 -22
demo/inline-region.ts
··· 13 13 close, 14 14 createInput, 15 15 createTerm, 16 + CSI, 16 17 type CursorEvent, 18 + DSR, 19 + ESC, 17 20 fixed, 18 21 grow, 19 22 type Op, 20 23 open, 21 24 rgba, 25 + SHOWCURSOR, 22 26 text, 23 27 } from "../mod.ts"; 24 28 import { cursor, settings } from "../settings.ts"; 25 29 import { validated } from "../validate.ts"; 26 30 27 - let write = (b: Uint8Array) => Deno.stdout.writeSync(b); 28 31 let encode = (s: string) => new TextEncoder().encode(s); 29 - let esc = (s: string) => write(encode(s)); 32 + let write = (b: Uint8Array) => Deno.stdout.writeSync(b); 30 33 31 34 let GREEN = rgba(80, 250, 123); 32 35 let GRAY = rgba(100, 100, 100); ··· 44 47 45 48 function* queryCursor(): Operation<CursorEvent> { 46 49 let parser = yield* until(createInput({ escLatency: 100 })); 47 - esc("\x1b[6n"); 50 + write(DSR()); 48 51 49 52 let buf = new Uint8Array(32); 50 53 while (true) { ··· 67 70 for (let i = 0; i < n; i++) { 68 71 if (buf[i] === 0x03) { 69 72 Deno.stdin.setRaw(false); 70 - esc("\x1b[?25h"); 73 + write(SHOWCURSOR()); 71 74 Deno.exit(0); 72 75 } 73 76 } ··· 111 114 ): Operation<void> { 112 115 let { columns } = Deno.consoleSize(); 113 116 114 - esc("\n".repeat(height)); 117 + write(encode("\n".repeat(height))); 115 118 116 119 let pos = yield* queryCursor(); 117 120 /** 1-based terminal row where the region starts */ 118 - let row = pos.top - height + 1; 121 + let row = pos.row - height + 1; 119 122 120 - esc("\x1b[s"); 123 + write(ESC("7")); 121 124 let tty = settings(cursor(false)); 122 125 write(tty.apply); 123 126 ··· 131 134 } 132 135 133 136 write(tty.revert); 134 - esc("\x1b[u"); 135 - esc("\n"); 137 + write(ESC("8")); 138 + write(encode("\n")); 136 139 } 137 140 138 141 function say(msg: string) { 139 - esc(msg + "\n"); 142 + write(encode(msg + "\n")); 140 143 } 141 144 142 145 function pause() { 143 146 waitKey(); 144 - esc("\n"); 147 + write(encode("\n")); 145 148 } 146 149 147 150 await main(function* () { ··· 157 160 say(""); 158 161 159 162 // Demo 1: Spinner box 160 - esc("\n\n\n"); 163 + write(encode("\n\n\n")); 161 164 162 165 let pos = yield* queryCursor(); 163 166 /** 1-based terminal row where the region starts */ 164 - let row = pos.top - 2; 167 + let row = pos.row - 2; 165 168 166 - esc("\x1b[s"); 169 + write(ESC("7")); 167 170 168 171 let frames = 30; 169 172 let term = validated( ··· 195 198 yield* sleep(80); 196 199 } 197 200 198 - esc("\x1b[u"); 199 - esc("\x1b[0m"); 200 - esc("\n"); 201 + write(ESC("8")); 202 + write(CSI("0m")); 203 + write(encode("\n")); 201 204 202 205 yield* sleep(500); 203 206 204 - esc( 205 - "\nRegions can be multi-line, but they can be a single line too. (continue...)", 207 + write( 208 + encode( 209 + "\nRegions can be multi-line, but they can be a single line too. (continue...)", 210 + ), 206 211 ); 207 212 pause(); 208 213 ··· 251 256 50, 252 257 ); 253 258 254 - esc("\x1b[0m"); 259 + write(CSI("0m")); 255 260 yield* sleep(500); 256 - esc("\nGoodbye sadness with limitless sky. (continue...)"); 261 + write(encode("\nGoodbye sadness with limitless sky. (continue...)")); 257 262 pause(); 258 263 259 264 // Demo 3: Nyan cat ··· 331 336 60, 332 337 ); 333 338 334 - esc("\x1b[0m\n"); 339 + write(CSI("0m")); 340 + write(encode("\n")); 335 341 write(tty.revert); 336 342 Deno.stdin.setRaw(false); 337 343 });
+5 -5
input.ts
··· 361 361 type: "cursor"; 362 362 363 363 /** 364 - * Cursor row (0-based). 364 + * Cursor row (1-based). Matches ECMA-48 DSR native format. 365 365 */ 366 - top: number; 366 + row: number; 367 367 368 368 /** 369 - * Cursor column (0-based). 369 + * Cursor column (1-based). Matches ECMA-48 DSR native format. 370 370 */ 371 - left: number; 371 + column: number; 372 372 } 373 373 374 374 import type { PointerEvent } from "./term.ts"; ··· 682 682 return { type: "resize", width: native.w, height: native.h }; 683 683 } 684 684 case EVENT_CURSOR: { 685 - return { type: "cursor", top: native.y, left: native.x }; 685 + return { type: "cursor", row: native.y, column: native.x }; 686 686 } 687 687 default: { 688 688 return mapKeyEvent(native);
+2
mod.ts
··· 1 1 export * from "./ops.ts"; 2 2 export * from "./term.ts"; 3 3 export * from "./input.ts"; 4 + export * from "./settings.ts"; 5 + export * from "./termcodes.ts";
+32 -20
settings.ts
··· 1 + import { 2 + ALTSCREEN, 3 + CSI, 4 + ESC, 5 + HIDECURSOR, 6 + MAINSCREEN, 7 + SHOWCURSOR, 8 + } from "./termcodes.ts"; 9 + 1 10 export interface Setting { 2 11 apply: Uint8Array; 3 12 revert: Uint8Array; ··· 12 21 13 22 export function alternateBuffer(): Setting { 14 23 return { 15 - apply: csi("?1049h"), 16 - revert: csi("?1049l"), 24 + apply: ALTSCREEN(), 25 + revert: MAINSCREEN(), 17 26 }; 18 27 } 19 28 20 29 export function cursor(visible: boolean): Setting { 21 30 if (visible) { 22 31 return { 23 - apply: csi("?25h"), 24 - revert: csi("?25l"), 32 + apply: SHOWCURSOR(), 33 + revert: HIDECURSOR(), 25 34 }; 26 35 } else { 27 36 return { 28 - apply: csi("?25l"), 29 - revert: csi("?25h"), 37 + apply: HIDECURSOR(), 38 + revert: SHOWCURSOR(), 30 39 }; 31 40 } 32 41 } 33 42 34 - export function progressiveInput(level: number): Setting { 43 + /** 44 + * Save and restore cursor position using DECSC (`ESC 7`) / DECRC (`ESC 8`). 45 + * 46 + * @see {@link https://vt100.net/docs/vt510-rm/DECSC.html | VT510 DECSC} 47 + * @see {@link https://vt100.net/docs/vt510-rm/DECRC.html | VT510 DECRC} 48 + */ 49 + export function saveCursorPosition(): Setting { 35 50 return { 36 - apply: csi(`>${level}u`), 37 - revert: csi("<u"), 51 + apply: ESC("7"), 52 + revert: ESC("8"), 38 53 }; 39 54 } 40 55 41 - export function mouseTracking(): Setting { 56 + export function progressiveInput(level: number): Setting { 42 57 return { 43 - apply: concat([csi("?1003h"), csi("?1006h")]), 44 - revert: concat([csi("?1006l"), csi("?1003l")]), 58 + apply: CSI(`>${level}u`), 59 + revert: CSI("<u"), 45 60 }; 46 61 } 47 62 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}`); 63 + export function mouseTracking(): Setting { 64 + return { 65 + apply: concat([CSI("?1003h"), CSI("?1006h")]), 66 + revert: concat([CSI("?1006l"), CSI("?1003l")]), 67 + }; 56 68 } 57 69 58 70 function concat(arrays: Uint8Array[]): Uint8Array {
+2 -2
src/input.c
··· 604 604 return PARSE_ERR; 605 605 i++; 606 606 ev->type = EVENT_CURSOR; 607 - ev->y = row - 1; 608 - ev->x = col - 1; 607 + ev->y = row; 608 + ev->x = col; 609 609 shift(st, i); 610 610 return PARSE_OK; 611 611 } else {
+84
termcodes.ts
··· 1 + /** 2 + * Encode a plain escape sequence. 3 + * 4 + * Prepends `ESC` (`\x1b`) to the given string and returns the result as bytes. 5 + * 6 + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} 7 + */ 8 + export function ESC(str: string): Uint8Array { 9 + return encode(`\x1b${str}`); 10 + } 11 + 12 + /** 13 + * Encode a Control Sequence Introducer (CSI) command. 14 + * 15 + * Prepends `ESC[` to the given string and returns the result as bytes. 16 + * 17 + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} 18 + */ 19 + export function CSI(str: string): Uint8Array { 20 + return ESC(`[${str}`); 21 + } 22 + 23 + /** 24 + * Request the cursor position via Device Status Report (DSR). 25 + * 26 + * Sends `CSI 6n`. The terminal responds with a Cursor Position Report 27 + * (`CSI row ; column R`) where row and column are 1-based. 28 + * 29 + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} 30 + */ 31 + export function DSR(): Uint8Array { 32 + return CSI("6n"); 33 + } 34 + 35 + /** 36 + * Show the cursor (DECTCEM set). 37 + * 38 + * DEC private mode 25. Not part of ECMA-48; originates from the VT220. 39 + * 40 + * @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM} 41 + */ 42 + export function SHOWCURSOR(): Uint8Array { 43 + return CSI("?25h"); 44 + } 45 + 46 + /** 47 + * Hide the cursor (DECTCEM reset). 48 + * 49 + * DEC private mode 25. Not part of ECMA-48; originates from the VT220. 50 + * 51 + * @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM} 52 + */ 53 + export function HIDECURSOR(): Uint8Array { 54 + return CSI("?25l"); 55 + } 56 + 57 + /** 58 + * Switch to the alternate screen buffer (xterm private mode 1049). 59 + * 60 + * Saves the cursor and switches to a clean alternate screen. Use 61 + * {@link MAINSCREEN} to switch back. 62 + * 63 + * @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences} 64 + */ 65 + export function ALTSCREEN(): Uint8Array { 66 + return CSI("?1049h"); 67 + } 68 + 69 + /** 70 + * Switch back to the main screen buffer (xterm private mode 1049). 71 + * 72 + * Restores the cursor and returns to the main screen with scrollback intact. 73 + * 74 + * @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences} 75 + */ 76 + export function MAINSCREEN(): Uint8Array { 77 + return CSI("?1049l"); 78 + } 79 + 80 + const encoder = new TextEncoder(); 81 + 82 + function encode(str: string): Uint8Array { 83 + return encoder.encode(str); 84 + }
+6 -6
test/input.test.ts
··· 687 687 expect(result.events.length).toBe(1); 688 688 expect(result.events[0]).toMatchObject({ 689 689 type: "cursor", 690 - top: 23, 691 - left: 79, 690 + row: 24, 691 + column: 80, 692 692 }); 693 693 }); 694 694 ··· 697 697 expect(result.events.length).toBe(1); 698 698 expect(result.events[0]).toMatchObject({ 699 699 type: "cursor", 700 - top: 0, 701 - left: 0, 700 + row: 1, 701 + column: 1, 702 702 }); 703 703 }); 704 704 ··· 708 708 expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" }); 709 709 expect(result.events[1]).toMatchObject({ 710 710 type: "cursor", 711 - top: 9, 712 - left: 4, 711 + row: 10, 712 + column: 5, 713 713 }); 714 714 expect(result.events[2]).toMatchObject({ type: "keydown", key: "b" }); 715 715 });