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

Configure Feed

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

✨ parse DSR cursor position and rename row offset to top

Add CursorEvent with 0-based top/left coords by parsing the terminal's
DSR response (\x1b[row;colR). Rename createTerm's row option to top
so cursor.top feeds directly into createTerm({ top: cursor.top }).

+124 -4
+1
input-native.ts
··· 1 1 export const EVENT_KEY = 1; 2 2 export const EVENT_MOUSE = 2; 3 3 export const EVENT_RESIZE = 3; 4 + export const EVENT_CURSOR = 4; 4 5 5 6 export const MOD_ALT = 1; 6 7 export const MOD_CTRL = 2;
+25
input.ts
··· 8 8 9 9 import { 10 10 createInputNative, 11 + EVENT_CURSOR, 11 12 EVENT_KEY, 12 13 EVENT_MOUSE, 13 14 EVENT_RESIZE, ··· 350 351 height: number; 351 352 } 352 353 354 + /** 355 + * Cursor position report (DSR response). 356 + * 357 + * Emitted when the terminal responds to a Device Status Report 358 + * query (`\x1b[6n`) with the current cursor position. 359 + */ 360 + export interface CursorEvent { 361 + type: "cursor"; 362 + 363 + /** 364 + * Cursor row (0-based). 365 + */ 366 + top: number; 367 + 368 + /** 369 + * Cursor column (0-based). 370 + */ 371 + left: number; 372 + } 373 + 353 374 import type { PointerEvent } from "./term.ts"; 354 375 355 376 export type InputEvent = ··· 359 380 | MouseMoveEvent 360 381 | WheelEvent 361 382 | ResizeEvent 383 + | CursorEvent 362 384 | PointerEvent; 363 385 364 386 /** ··· 658 680 } 659 681 case EVENT_RESIZE: { 660 682 return { type: "resize", width: native.w, height: native.h }; 683 + } 684 + case EVENT_CURSOR: { 685 + return { type: "cursor", top: native.y, left: native.x }; 661 686 } 662 687 default: { 663 688 return mapKeyEvent(native);
+59
src/input.c
··· 573 573 } 574 574 } 575 575 576 + /* Parse DSR cursor position response: CSI row ; col R */ 577 + static int parse_cursor(struct InputState *st, struct InputEvent *ev) { 578 + if (st->len < 2) 579 + return PARSE_NEED_MORE; 580 + if (st->buf[0] != '\x1b' || st->buf[1] != '[') 581 + return PARSE_ERR; 582 + if (st->len < 3) 583 + return PARSE_NEED_MORE; 584 + 585 + int row = 0; 586 + int col = 0; 587 + int param = 0; 588 + int i = 2; 589 + 590 + while (i < st->len) { 591 + char c = st->buf[i]; 592 + if (c >= '0' && c <= '9') { 593 + if (param == 0) { 594 + row = row * 10 + (c - '0'); 595 + } else { 596 + col = col * 10 + (c - '0'); 597 + } 598 + } else if (c == ';') { 599 + if (param > 0) 600 + return PARSE_ERR; 601 + param++; 602 + } else if (c == 'R') { 603 + if (param != 1) 604 + return PARSE_ERR; 605 + i++; 606 + ev->type = EVENT_CURSOR; 607 + ev->y = row - 1; 608 + ev->x = col - 1; 609 + shift(st, i); 610 + return PARSE_OK; 611 + } else { 612 + return PARSE_ERR; 613 + } 614 + i++; 615 + } 616 + return PARSE_NEED_MORE; 617 + } 618 + 576 619 /* Parse Kitty-enhanced legacy CSI sequences (non-u terminators). 577 620 * Format: CSI [number] [; mod[:action]] terminator 578 621 * Handles A-D, F, H, P, Q, S, ~ terminators with optional :action */ ··· 975 1018 struct InputEvent kev; 976 1019 memset(&kev, 0, sizeof(kev)); 977 1020 int rv = parse_csi_u(st, &kev); 1021 + if (rv == PARSE_OK) { 1022 + struct InputEvent *ev = emit(st); 1023 + *ev = kev; 1024 + st->esc_time = 0; 1025 + continue; 1026 + } 1027 + if (rv == PARSE_NEED_MORE) { 1028 + return accepted; 1029 + } 1030 + } 1031 + 1032 + /* try DSR cursor position response */ 1033 + { 1034 + struct InputEvent kev; 1035 + memset(&kev, 0, sizeof(kev)); 1036 + int rv = parse_cursor(st, &kev); 978 1037 if (rv == PARSE_OK) { 979 1038 struct InputEvent *ev = emit(st); 980 1039 *ev = kev;
+1
src/input.h
··· 58 58 #define EVENT_KEY 1 59 59 #define EVENT_MOUSE 2 60 60 #define EVENT_RESIZE 3 61 + #define EVENT_CURSOR 4 61 62 62 63 /* ── Modifier flags (bitwise) ─────────────────────────────────────── */ 63 64
+3 -3
term.ts
··· 4 4 export interface TermOptions { 5 5 height: number; 6 6 width: number; 7 - row?: number; 7 + top?: number; 8 8 } 9 9 10 10 export interface RenderOptions { ··· 30 30 } 31 31 32 32 export async function createTerm(options: TermOptions): Promise<Term> { 33 - let { width, height, row = 0 } = options; 34 - let native = await createTermNative(width, height, row); 33 + let { width, height, top = 0 } = options; 34 + let native = await createTermNative(width, height, top); 35 35 let { memory, statePtr, opsBuf } = native; 36 36 37 37 let prev = new Set<string>();
+34
test/input.test.ts
··· 681 681 }); 682 682 }); 683 683 684 + describe("cursor position (DSR response)", () => { 685 + it("parses cursor position report", () => { 686 + let result = input.scan(str("\x1b[24;80R")); 687 + expect(result.events.length).toBe(1); 688 + expect(result.events[0]).toMatchObject({ 689 + type: "cursor", 690 + top: 23, 691 + left: 79, 692 + }); 693 + }); 694 + 695 + it("parses cursor at origin", () => { 696 + let result = input.scan(str("\x1b[1;1R")); 697 + expect(result.events.length).toBe(1); 698 + expect(result.events[0]).toMatchObject({ 699 + type: "cursor", 700 + top: 0, 701 + left: 0, 702 + }); 703 + }); 704 + 705 + it("parses cursor interleaved with other input", () => { 706 + let result = input.scan(str("a\x1b[10;5Rb")); 707 + expect(result.events.length).toBe(3); 708 + expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" }); 709 + expect(result.events[1]).toMatchObject({ 710 + type: "cursor", 711 + top: 9, 712 + left: 4, 713 + }); 714 + expect(result.events[2]).toMatchObject({ type: "keydown", key: "b" }); 715 + }); 716 + }); 717 + 684 718 describe("UTF-8", () => { 685 719 it("parses 2-byte UTF-8 (é)", () => { 686 720 let result = input.scan(bytes(0xc3, 0xa9));
+1 -1
test/term.test.ts
··· 91 91 92 92 describe("row offset", () => { 93 93 it("renders two frames at the offset position", async () => { 94 - let term = await createTerm({ width: 20, height: 5, row: 5 }); 94 + let term = await createTerm({ width: 20, height: 5, top: 5 }); 95 95 let box = (msg: string) => [ 96 96 open("root", { 97 97 layout: { width: grow(), height: grow(), direction: "ttb" },