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

๐Ÿ’„ format codebase and fix validate test render result

Charles Lowell (Mar 30, 2026, 6:08 PM -0500) b555a1bf 10c686f7

+353 -168
+22 -17
README.md
··· 1 1 # clayterm 2 2 3 - A low-level, platform-independent terminal renderer and event parser for JavaScript. You can use clayterm directly, or as the foundation for your own framework. 3 + A low-level, platform-independent terminal renderer and event parser for 4 + JavaScript. You can use clayterm directly, or as the foundation for your own 5 + framework. 4 6 5 7 ## Features 6 8 7 - **Declarative terminal UI** โ€” Build terminal interfaces the same way you'd 8 - build a web page. Clayterm uses [Clay](https://github.com/nicbarker/clay) under 9 - the hood, giving you flexbox-like layout, pointer detection, and scroll 10 - containers โ€” all rendered to the terminal as box-drawing characters and ANSI 11 - escape sequences. 9 + **Declarative terminal UI** โ€” Build terminal interfaces the same way you'd build 10 + a web page. Clayterm uses [Clay](https://github.com/nicbarker/clay) under the 11 + hood, giving you flexbox-like layout, pointer detection, and scroll containers โ€” 12 + all rendered to the terminal as box-drawing characters and ANSI escape 13 + sequences. 12 14 13 15 **Zero I/O** โ€” Clayterm never reads stdin or writes stdout. You feed it bytes 14 16 and get bytes back. This makes it trivially embeddable in any framework, any 15 17 runtime, any event loop. There are no opinions about how you do I/O, just pure 16 18 computation. 17 19 18 - **Runs everywhere** โ€” The entire engine is compiled to WebAssembly, so 19 - clayterm will run anywhere JavaScript runs with no native 20 - dependencies, and no build step for consumers. 20 + **Runs everywhere** โ€” The entire engine is compiled to WebAssembly, so clayterm 21 + will run anywhere JavaScript runs with no native dependencies, and no build step 22 + for consumers. 21 23 22 24 ### Demo 23 25 ··· 25 27 26 28 #### Keyboard Events 27 29 28 - The input parser decodes raw terminal bytes into structured events. 29 - Here you can see each key event as the string "hello world" is typed. 30 + The input parser decodes raw terminal bytes into structured events. Here you can 31 + see each key event as the string "hello world" is typed. 30 32 31 33 ![Keyboard events demo](demo/keyboard-key-events.gif) 32 34 33 35 #### Pointer Events 34 36 35 - Here we see hover styles applied to UI elements in response to the 36 - pointer state. Clay drives the hit testing; no manual coordinate math 37 - required. 37 + Here we see hover styles applied to UI elements in response to the pointer 38 + state. Clay drives the hit testing; no manual coordinate math required. 38 39 39 40 ![Pointer events demo](demo/keyboard-pointer-events.gif) 40 - 41 41 42 42 ## Architecture 43 43 44 - Clayterm does not do any I/O itself. On the ouput side, it converts UI elements into a raw sequence of bytes and pointer events, and on the input side, it converts a stream of raw bytes into structured events. 44 + Clayterm does not do any I/O itself. On the ouput side, it converts UI elements 45 + into a raw sequence of bytes and pointer events, and on the input side, it 46 + converts a stream of raw bytes into structured events. 45 47 46 48 ### Output 47 49 ··· 126 128 layout: { padding: { left: 1, right: 1 } }, 127 129 border: { 128 130 color: rgba(0, 255, 0), 129 - left: 1, right: 1, top: 1, bottom: 1, 131 + left: 1, 132 + right: 1, 133 + top: 1, 134 + bottom: 1, 130 135 }, 131 136 cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 132 137 }),
+95 -54
demo/keyboard.ts
··· 1 1 // deno-lint-ignore-file no-fallthrough 2 - import { createChannel, each, ensure, main, race, resource, type Stream, until } from "effection"; 2 + import { 3 + createChannel, 4 + each, 5 + ensure, 6 + main, 7 + race, 8 + resource, 9 + type Stream, 10 + until, 11 + } from "effection"; 3 12 import { 4 13 close, 5 14 createTerm, ··· 37 46 } 38 47 39 48 function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean { 40 - return isKeyEvent(event) && event.type === "keydown" && event.code.toUpperCase() === k.code.toUpperCase(); 49 + return isKeyEvent(event) && event.type === "keydown" && 50 + event.code.toUpperCase() === k.code.toUpperCase(); 41 51 } 42 52 43 53 let hovered = rgba(80, 80, 100); ··· 310 320 ); 311 321 } 312 322 313 - let flagNames: (keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] = [ 314 - "Disambiguate escape codes", 315 - "Report event types", 316 - "Report alternate keys", 317 - "Report all keys as escapes", 318 - "Report associated text", 319 - ]; 323 + let flagNames: 324 + (keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] = 325 + [ 326 + "Disambiguate escape codes", 327 + "Report event types", 328 + "Report alternate keys", 329 + "Report all keys as escapes", 330 + "Report associated text", 331 + ]; 320 332 321 333 let logEntries: { key: string; name: keyof EventFilter }[] = [ 322 334 { key: "a", name: "keydown" }, ··· 332 344 { key: "k", name: "pointerclick" }, 333 345 ]; 334 346 335 - function logToggle(ops: Op[], entries: typeof logEntries, ctx: AppContext): void { 347 + function logToggle( 348 + ops: Op[], 349 + entries: typeof logEntries, 350 + ctx: AppContext, 351 + ): void { 336 352 for (let entry of entries) { 337 353 ops.push( 338 - open(`log:${entry.name}`, { layout: { direction: "ltr", height: fixed(1), gap: 1 } }), 354 + open(`log:${entry.name}`, { 355 + layout: { direction: "ltr", height: fixed(1), gap: 1 }, 356 + }), 339 357 ); 340 358 ops.push(text(`${entry.key}.`, { color: dim })); 341 359 toggle(ops, ctx.log[entry.name], entry.name); ··· 364 382 for (let i = 0; i < flagNames.length; i++) { 365 383 let name = flagNames[i]; 366 384 ops.push( 367 - open(`flag:${name}`, { layout: { direction: "ltr", height: fixed(1), gap: 1 } }), 385 + open(`flag:${name}`, { 386 + layout: { direction: "ltr", height: fixed(1), gap: 1 }, 387 + }), 368 388 ); 369 389 ops.push(text(`${i + 1}.`, { color: dim })); 370 390 toggle(ops, ctx[name], name); ··· 437 457 let badgeHint = ctx.mode === "input" 438 458 ? "Ctrl+X Ctrl+X to enter config" 439 459 : "Set flags with keys [0-5], Enter to save"; 440 - let mouseBg = ctx["Capture mouse events"] ? rgba(40, 180, 80) : rgba(80, 80, 80); 460 + let mouseBg = ctx["Capture mouse events"] 461 + ? rgba(40, 180, 80) 462 + : rgba(80, 80, 80); 441 463 let mouseLabel = ctx["Capture mouse events"] ? "capture" : "system"; 442 464 ops.push( 443 - open("badges", { layout: { direction: "ttb", gap: 1, padding: { top: 1 } } }), 444 - open("badge:mode", { layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } } }), 445 - open("", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }), 465 + open("badges", { 466 + layout: { direction: "ttb", gap: 1, padding: { top: 1 } }, 467 + }), 468 + open("badge:mode", { 469 + layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } }, 470 + }), 471 + open("", { 472 + layout: { padding: { left: 1, right: 1 } }, 473 + bg: rgba(60, 60, 60), 474 + }), 446 475 text("mode", { color: rgba(220, 220, 220) }), 447 476 close(), 448 477 open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }), ··· 451 480 text(` ${badgeHint}`, { color: dim }), 452 481 close(), 453 482 open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }), 454 - open("", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }), 483 + open("", { 484 + layout: { padding: { left: 1, right: 1 } }, 485 + bg: rgba(60, 60, 60), 486 + }), 455 487 text("mouse", { color: rgba(220, 220, 220) }), 456 488 close(), 457 489 open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }), ··· 552 584 553 585 let pointer = { 554 586 events: createChannel<PointerEvent, void>(), 555 - state: undefined as { x: number; y: number; down: boolean} | undefined, 587 + state: undefined as { x: number; y: number; down: boolean } | undefined, 556 588 }; 557 589 558 590 for (let event of yield* each(merge(input, pointer.events))) { ··· 578 610 579 611 if (context["Capture mouse events"]) { 580 612 if ("x" in event) { 581 - pointer.state = { x: event.x, y: event.y, down: event.type === "mousedown" }; 613 + pointer.state = { 614 + x: event.x, 615 + y: event.y, 616 + down: event.type === "mousedown", 617 + }; 582 618 } 583 619 } else { 584 620 pointer.state = undefined; 585 621 } 586 622 587 - let { output, events } = term.render(keyboard(context), { pointer: pointer.state }); 623 + let { output, events } = term.render(keyboard(context), { 624 + pointer: pointer.state, 625 + }); 588 626 589 627 for (let event of events) { 590 628 yield* pointer.events.send(event); 591 629 } 592 - 630 + 593 631 Deno.stdout.writeSync(output); 594 632 595 633 yield* each.next(); ··· 623 661 logged: null, 624 662 }; 625 663 626 - let event= yield current; 664 + let event = yield current; 627 665 628 666 let mode = inputmode({ ...current, event }); 629 667 ··· 670 708 let event = yield context; 671 709 while (true) { 672 710 if (event.type === "keydown" && event.key === "Enter") { 673 - return inputmode({...context, event: null }); 711 + return inputmode({ ...context, event: null }); 674 712 } 675 713 if (event.type === "keydown") { 676 714 let k = (event as KeyEvent).key; 677 715 let entry = logEntries.find((e) => e.key === k); 678 716 if (entry) { 679 - context = { ...context, log: { ...context.log, [entry.name]: !context.log[entry.name] } }; 717 + context = { 718 + ...context, 719 + log: { ...context.log, [entry.name]: !context.log[entry.name] }, 720 + }; 680 721 } 681 722 if ("012345".indexOf(event.key) >= 0) { 682 - context = { ...context }; 683 - context["Report associated text"] = false; 684 - context["Report all keys as escapes"] = false; 685 - context["Report alternate keys"] = false; 686 - context["Report event types"] = false; 687 - context["Disambiguate escape codes"] = false; 688 - switch (event.key) { 689 - case "5": 690 - context["Report associated text"] = true; 691 - case "4": 692 - context["Report all keys as escapes"] = true; 693 - case "3": 694 - context["Report alternate keys"] = true; 695 - case "2": 696 - context["Report event types"] = true; 697 - case "1": 698 - context["Disambiguate escape codes"] = true; 699 - break; 700 - case "0": 701 - break; 702 - } 723 + context = { ...context }; 724 + context["Report associated text"] = false; 725 + context["Report all keys as escapes"] = false; 726 + context["Report alternate keys"] = false; 727 + context["Report event types"] = false; 728 + context["Disambiguate escape codes"] = false; 729 + switch (event.key) { 730 + case "5": 731 + context["Report associated text"] = true; 732 + case "4": 733 + context["Report all keys as escapes"] = true; 734 + case "3": 735 + context["Report alternate keys"] = true; 736 + case "2": 737 + context["Report event types"] = true; 738 + case "1": 739 + context["Disambiguate escape codes"] = true; 740 + break; 741 + case "0": 742 + break; 743 + } 703 744 } 704 745 } 705 746 event = yield context; 706 747 } 707 748 } 708 749 709 - 710 - function merge<A,B, TClose>(a: Stream<A,TClose>, b: Stream<B,TClose>): Stream<A|B, TClose> { 711 - return resource(function*(provide) { 750 + function merge<A, B, TClose>( 751 + a: Stream<A, TClose>, 752 + b: Stream<B, TClose>, 753 + ): Stream<A | B, TClose> { 754 + return resource(function* (provide) { 712 755 let subscription = { 713 756 a: yield* a, 714 757 b: yield* b, 715 - } 758 + }; 716 759 717 760 return yield* provide({ 718 761 *next() { 719 - return yield* race([subscription.a.next(),subscription.b.next()]); 762 + return yield* race([subscription.a.next(), subscription.b.next()]); 720 763 }, 721 - }) 722 - }); 764 + }); 765 + }); 723 766 } 724 - 725 767 726 768 type EventFilter = { 727 769 keydown: boolean; ··· 750 792 ["Report associated text"]: boolean; 751 793 ["Capture mouse events"]: boolean; 752 794 }; 753 -
+9 -1
input-native.ts
··· 77 77 export const KEY_BACKSPACE = 0x7F; 78 78 export const KEY_SPACE = 0x20; 79 79 80 - import { array, int32, offsets, struct, uint8, uint16, uint32 } from "./typedef.ts"; 80 + import { 81 + array, 82 + int32, 83 + offsets, 84 + struct, 85 + uint16, 86 + uint32, 87 + uint8, 88 + } from "./typedef.ts"; 81 89 82 90 const MAX_TEXT_CODEPOINTS = 8; 83 91
+127 -43
input.ts
··· 11 11 EVENT_KEY, 12 12 EVENT_MOUSE, 13 13 EVENT_RESIZE, 14 + KEY_ALT_LEFT, 15 + KEY_ALT_RIGHT, 14 16 KEY_ARROW_DOWN, 15 17 KEY_ARROW_LEFT, 16 18 KEY_ARROW_RIGHT, 17 19 KEY_ARROW_UP, 18 20 KEY_BACKSPACE, 19 21 KEY_BACKTAB, 22 + KEY_CAPS_LOCK, 23 + KEY_CONTROL_LEFT, 24 + KEY_CONTROL_RIGHT, 20 25 KEY_DELETE, 21 26 KEY_END, 22 27 KEY_ENTER, ··· 34 39 KEY_F8, 35 40 KEY_F9, 36 41 KEY_HOME, 42 + KEY_HYPER_LEFT, 43 + KEY_HYPER_RIGHT, 37 44 KEY_INSERT, 45 + KEY_META_LEFT, 46 + KEY_META_RIGHT, 47 + KEY_MOUSE_LEFT, 48 + KEY_MOUSE_MIDDLE, 49 + KEY_MOUSE_RELEASE, 50 + KEY_MOUSE_RIGHT, 51 + KEY_MOUSE_WHEEL_DOWN, 52 + KEY_MOUSE_WHEEL_UP, 53 + KEY_NUM_LOCK, 38 54 KEY_NUMPAD_0, 39 55 KEY_NUMPAD_1, 40 56 KEY_NUMPAD_2, ··· 52 68 KEY_NUMPAD_EQUAL, 53 69 KEY_NUMPAD_MULTIPLY, 54 70 KEY_NUMPAD_SUBTRACT, 55 - KEY_META_LEFT, 56 - KEY_META_RIGHT, 57 - KEY_NUM_LOCK, 71 + KEY_PGDN, 72 + KEY_PGUP, 58 73 KEY_SCROLL_LOCK, 59 74 KEY_SHIFT_LEFT, 60 75 KEY_SHIFT_RIGHT, 61 - KEY_CONTROL_LEFT, 62 - KEY_CONTROL_RIGHT, 63 - KEY_ALT_LEFT, 64 - KEY_ALT_RIGHT, 65 76 KEY_SUPER_LEFT, 66 77 KEY_SUPER_RIGHT, 67 - KEY_HYPER_LEFT, 68 - KEY_HYPER_RIGHT, 69 - KEY_CAPS_LOCK, 70 - KEY_MOUSE_LEFT, 71 - KEY_MOUSE_MIDDLE, 72 - KEY_MOUSE_RELEASE, 73 - KEY_MOUSE_RIGHT, 74 - KEY_MOUSE_WHEEL_DOWN, 75 - KEY_MOUSE_WHEEL_UP, 76 - KEY_PGDN, 77 - KEY_PGUP, 78 78 KEY_TAB, 79 79 MAX_TERMINFO, 80 80 MOD_ALT, ··· 100 100 * Physical key identity on a US PC-101 layout. 101 101 */ 102 102 export type KeyCode = 103 - | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" 104 - | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" 105 - | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" 106 - | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" 107 - | "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | "," | "." | "/" | " " 108 - | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" 109 - | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" 110 - | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" 111 - | "Home" | "End" | "Insert" | "Delete" 112 - | "PageUp" | "PageDown" | "Backtab" 113 - | "Backspace" | "Tab" | "Enter" | "Escape" 114 - | "Numpad0" | "Numpad1" | "Numpad2" | "Numpad3" | "Numpad4" 115 - | "Numpad5" | "Numpad6" | "Numpad7" | "Numpad8" | "Numpad9" 116 - | "NumpadDecimal" | "NumpadDivide" | "NumpadMultiply" 117 - | "NumpadSubtract" | "NumpadAdd" | "NumpadEnter" | "NumpadEqual" 118 - | "ShiftLeft" | "ShiftRight" 119 - | "ControlLeft" | "ControlRight" 120 - | "AltLeft" | "AltRight" 121 - | "SuperLeft" | "SuperRight" 122 - | "HyperLeft" | "HyperRight" 123 - | "MetaLeft" | "MetaRight" 124 - | "CapsLock" | "NumLock" | "ScrollLock"; 103 + | "a" 104 + | "b" 105 + | "c" 106 + | "d" 107 + | "e" 108 + | "f" 109 + | "g" 110 + | "h" 111 + | "i" 112 + | "j" 113 + | "k" 114 + | "l" 115 + | "m" 116 + | "n" 117 + | "o" 118 + | "p" 119 + | "q" 120 + | "r" 121 + | "s" 122 + | "t" 123 + | "u" 124 + | "v" 125 + | "w" 126 + | "x" 127 + | "y" 128 + | "z" 129 + | "0" 130 + | "1" 131 + | "2" 132 + | "3" 133 + | "4" 134 + | "5" 135 + | "6" 136 + | "7" 137 + | "8" 138 + | "9" 139 + | "`" 140 + | "-" 141 + | "=" 142 + | "[" 143 + | "]" 144 + | "\\" 145 + | ";" 146 + | "'" 147 + | "," 148 + | "." 149 + | "/" 150 + | " " 151 + | "F1" 152 + | "F2" 153 + | "F3" 154 + | "F4" 155 + | "F5" 156 + | "F6" 157 + | "F7" 158 + | "F8" 159 + | "F9" 160 + | "F10" 161 + | "F11" 162 + | "F12" 163 + | "ArrowUp" 164 + | "ArrowDown" 165 + | "ArrowLeft" 166 + | "ArrowRight" 167 + | "Home" 168 + | "End" 169 + | "Insert" 170 + | "Delete" 171 + | "PageUp" 172 + | "PageDown" 173 + | "Backtab" 174 + | "Backspace" 175 + | "Tab" 176 + | "Enter" 177 + | "Escape" 178 + | "Numpad0" 179 + | "Numpad1" 180 + | "Numpad2" 181 + | "Numpad3" 182 + | "Numpad4" 183 + | "Numpad5" 184 + | "Numpad6" 185 + | "Numpad7" 186 + | "Numpad8" 187 + | "Numpad9" 188 + | "NumpadDecimal" 189 + | "NumpadDivide" 190 + | "NumpadMultiply" 191 + | "NumpadSubtract" 192 + | "NumpadAdd" 193 + | "NumpadEnter" 194 + | "NumpadEqual" 195 + | "ShiftLeft" 196 + | "ShiftRight" 197 + | "ControlLeft" 198 + | "ControlRight" 199 + | "AltLeft" 200 + | "AltRight" 201 + | "SuperLeft" 202 + | "SuperRight" 203 + | "HyperLeft" 204 + | "HyperRight" 205 + | "MetaLeft" 206 + | "MetaRight" 207 + | "CapsLock" 208 + | "NumLock" 209 + | "ScrollLock"; 125 210 126 211 /** 127 212 * Shared key information present on all keyboard events. ··· 499 584 500 585 function mapKeyEvent(native: NativeInputEvent): KeyEvent { 501 586 let key = keyName(native); 502 - let code = (native.base > 0 503 - ? String.fromCodePoint(native.base) 504 - : key) as KeyCode; 587 + let code = 588 + (native.base > 0 ? String.fromCodePoint(native.base) : key) as KeyCode; 505 589 let m = mods(native); 506 590 let isChar = !KEY_NAMES.has(native.key); 507 591 let text = textFromNative(native);
+41 -10
test/input.test.ts
··· 59 59 it("parses 0x7f as backspace", () => { 60 60 let result = input.scan(bytes(0x7f)); 61 61 expect(result.events.length).toBe(1); 62 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "Backspace" }); 62 + expect(result.events[0]).toMatchObject({ 63 + type: "keydown", 64 + key: "Backspace", 65 + }); 63 66 }); 64 67 }); 65 68 ··· 67 70 it("parses arrow up (CSI)", () => { 68 71 let result = input.scan(bytes(0x1b, 0x5b, 0x41)); 69 72 expect(result.events.length).toBe(1); 70 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); 73 + expect(result.events[0]).toMatchObject({ 74 + type: "keydown", 75 + key: "ArrowUp", 76 + }); 71 77 }); 72 78 73 79 it("parses arrow down (SS3)", () => { 74 80 let result = input.scan(bytes(0x1b, 0x4f, 0x42)); 75 81 expect(result.events.length).toBe(1); 76 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowDown" }); 82 + expect(result.events[0]).toMatchObject({ 83 + type: "keydown", 84 + key: "ArrowDown", 85 + }); 77 86 }); 78 87 }); 79 88 ··· 141 150 it("parses Delete", () => { 142 151 let result = input.scan(str("\x1b[3~")); 143 152 expect(result.events.length).toBe(1); 144 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "Delete" }); 153 + expect(result.events[0]).toMatchObject({ 154 + type: "keydown", 155 + key: "Delete", 156 + }); 145 157 }); 146 158 147 159 it("parses PgDn with Ctrl", () => { ··· 313 325 await new Promise((r) => setTimeout(r, 30)); 314 326 let result = input.scan(new Uint8Array(0)); 315 327 expect(result.events.length).toBe(1); 316 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "Escape" }); 328 + expect(result.events[0]).toMatchObject({ 329 + type: "keydown", 330 + key: "Escape", 331 + }); 317 332 expect(result.pending).toBeUndefined(); 318 333 }); 319 334 ··· 335 350 // "x" arrives โ€” ESC + x = Alt+x 336 351 let r2 = input.scan(bytes(0x78)); // 'x' 337 352 expect(r2.events.length).toBe(1); 338 - expect(r2.events[0]).toMatchObject({ type: "keydown", key: "x", alt: true }); 353 + expect(r2.events[0]).toMatchObject({ 354 + type: "keydown", 355 + key: "x", 356 + alt: true, 357 + }); 339 358 expect(r2.pending).toBeUndefined(); 340 359 }); 341 360 ··· 373 392 bytes(0x1b, 0x5b, 0x41, 0x1b, 0x5b, 0x42, 0x68, 0x69), 374 393 ); 375 394 expect(result.events.length).toBe(4); 376 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); 377 - expect(result.events[1]).toMatchObject({ type: "keydown", key: "ArrowDown" }); 395 + expect(result.events[0]).toMatchObject({ 396 + type: "keydown", 397 + key: "ArrowUp", 398 + }); 399 + expect(result.events[1]).toMatchObject({ 400 + type: "keydown", 401 + key: "ArrowDown", 402 + }); 378 403 expect(result.events[2]).toMatchObject({ type: "keydown", key: "h" }); 379 404 expect(result.events[3]).toMatchObject({ type: "keydown", key: "i" }); 380 405 }); ··· 660 685 it("parses 2-byte UTF-8 (รฉ)", () => { 661 686 let result = input.scan(bytes(0xc3, 0xa9)); 662 687 expect(result.events.length).toBe(1); 663 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "\u00e9" }); 688 + expect(result.events[0]).toMatchObject({ 689 + type: "keydown", 690 + key: "\u00e9", 691 + }); 664 692 }); 665 693 666 694 it("parses 3-byte UTF-8 (ไธญ)", () => { 667 695 let result = input.scan(bytes(0xe4, 0xb8, 0xad)); 668 696 expect(result.events.length).toBe(1); 669 - expect(result.events[0]).toMatchObject({ type: "keydown", key: "\u4e2d" }); 697 + expect(result.events[0]).toMatchObject({ 698 + type: "keydown", 699 + key: "\u4e2d", 700 + }); 670 701 }); 671 702 672 703 it("parses 4-byte UTF-8 (emoji ๐ŸŽ‰)", () => {
+41 -35
test/term.test.ts
··· 14 14 15 15 it("renders hello world", () => { 16 16 let out = print( 17 - decode(term.render([ 18 - open("root", { 19 - layout: { width: grow(), height: grow(), direction: "ttb" }, 20 - }), 21 - text("Hello, World!"), 22 - close(), 23 - ]).output), 17 + decode( 18 + term.render([ 19 + open("root", { 20 + layout: { width: grow(), height: grow(), direction: "ttb" }, 21 + }), 22 + text("Hello, World!"), 23 + close(), 24 + ]).output, 25 + ), 24 26 40, 25 27 10, 26 28 ); ··· 29 31 }); 30 32 31 33 it("text inherits parent background", () => { 32 - let ansi = decode(term.render([ 33 - open("root", { 34 - layout: { width: grow(), height: grow(), direction: "ttb" }, 35 - bg: rgba(255, 0, 0), 36 - }), 37 - text("hi"), 38 - close(), 39 - ]).output); 34 + let ansi = decode( 35 + term.render([ 36 + open("root", { 37 + layout: { width: grow(), height: grow(), direction: "ttb" }, 38 + bg: rgba(255, 0, 0), 39 + }), 40 + text("hi"), 41 + close(), 42 + ]).output, 43 + ); 40 44 41 45 // the SGR active when "h" is emitted should include the 42 46 // parent's red background (48;2;255;0;0), not terminal default ··· 46 50 47 51 it("renders borders and padding", () => { 48 52 let out = print( 49 - decode(term.render([ 50 - open("box", { 51 - layout: { 52 - width: grow(), 53 - height: grow(), 54 - padding: { left: 5, top: 5 }, 55 - direction: "ttb", 56 - }, 57 - border: { 58 - color: rgba(0, 255, 0), 59 - left: 1, 60 - right: 1, 61 - top: 1, 62 - bottom: 1, 63 - }, 64 - cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 65 - }), 66 - text("padded"), 67 - close(), 68 - ]).output), 53 + decode( 54 + term.render([ 55 + open("box", { 56 + layout: { 57 + width: grow(), 58 + height: grow(), 59 + padding: { left: 5, top: 5 }, 60 + direction: "ttb", 61 + }, 62 + border: { 63 + color: rgba(0, 255, 0), 64 + left: 1, 65 + right: 1, 66 + top: 1, 67 + bottom: 1, 68 + }, 69 + cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, 70 + }), 71 + text("padded"), 72 + close(), 73 + ]).output, 74 + ), 69 75 40, 70 76 10, 71 77 );
+9 -7
test/validate.test.ts
··· 89 89 90 90 it("renders valid ops normally", () => { 91 91 let out = print( 92 - decode(term.render([ 93 - open("root", { 94 - layout: { width: grow(), height: grow(), direction: "ttb" }, 95 - }), 96 - text("Hello, World!"), 97 - close(), 98 - ])), 92 + decode( 93 + term.render([ 94 + open("root", { 95 + layout: { width: grow(), height: grow(), direction: "ttb" }, 96 + }), 97 + text("Hello, World!"), 98 + close(), 99 + ]).output, 100 + ), 99 101 40, 100 102 10, 101 103 );
+9 -1
typedef.ts
··· 22 22 }; 23 23 24 24 export type Num<T> = { 25 - type: "i32" | "f32" | "f64" | "uint8" | "uint16" | "uint32" | "int16" | "int32"; 25 + type: 26 + | "i32" 27 + | "f32" 28 + | "f64" 29 + | "uint8" 30 + | "uint16" 31 + | "uint32" 32 + | "int16" 33 + | "int32"; 26 34 byteAlign: Alignment; 27 35 byteLength: number; 28 36 T?: T;