@chengchenccc/tui 0.1.1-rc.3 → 0.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/components/ansi-console.d.ts +32 -0
  2. package/dist/components/ansi-console.d.ts.map +1 -0
  3. package/dist/components/ansi-console.js +75 -0
  4. package/dist/components/ansi-console.test.d.ts +2 -0
  5. package/dist/components/ansi-console.test.d.ts.map +1 -0
  6. package/dist/components/ansi-console.test.js +36 -0
  7. package/dist/components/loader.d.ts +21 -1
  8. package/dist/components/loader.d.ts.map +1 -1
  9. package/dist/components/loader.js +39 -15
  10. package/dist/components/markdown.d.ts +5 -0
  11. package/dist/components/markdown.d.ts.map +1 -1
  12. package/dist/components/markdown.js +47 -2
  13. package/dist/components/markdown.test.js +30 -1
  14. package/dist/components/select-list.d.ts +6 -0
  15. package/dist/components/select-list.d.ts.map +1 -1
  16. package/dist/components/select-list.js +18 -0
  17. package/dist/components/select-list.test.d.ts +2 -0
  18. package/dist/components/select-list.test.d.ts.map +1 -0
  19. package/dist/components/select-list.test.js +53 -0
  20. package/dist/index.d.ts +4 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -2
  23. package/dist/keybindings.d.ts +5 -0
  24. package/dist/keybindings.d.ts.map +1 -1
  25. package/dist/keybindings.js +7 -0
  26. package/dist/loader-sweep.test.d.ts +2 -0
  27. package/dist/loader-sweep.test.d.ts.map +1 -0
  28. package/dist/loader-sweep.test.js +67 -0
  29. package/dist/stdin-buffer.d.ts +1 -0
  30. package/dist/stdin-buffer.d.ts.map +1 -1
  31. package/dist/stdin-buffer.js +19 -0
  32. package/dist/stdin-buffer.test.d.ts +2 -0
  33. package/dist/stdin-buffer.test.d.ts.map +1 -0
  34. package/dist/stdin-buffer.test.js +38 -0
  35. package/dist/terminal-reassert.test.d.ts +2 -0
  36. package/dist/terminal-reassert.test.d.ts.map +1 -0
  37. package/dist/terminal-reassert.test.js +18 -0
  38. package/dist/terminal.d.ts +14 -0
  39. package/dist/terminal.d.ts.map +1 -1
  40. package/dist/terminal.js +18 -0
  41. package/dist/text-sizing.d.ts +7 -0
  42. package/dist/text-sizing.d.ts.map +1 -0
  43. package/dist/text-sizing.js +34 -0
  44. package/dist/text-sizing.test.d.ts +2 -0
  45. package/dist/text-sizing.test.d.ts.map +1 -0
  46. package/dist/text-sizing.test.js +43 -0
  47. package/dist/tui-scrollback.test.js +36 -0
  48. package/dist/tui.d.ts +8 -0
  49. package/dist/tui.d.ts.map +1 -1
  50. package/dist/tui.js +73 -7
  51. package/dist/utils.d.ts +1 -3
  52. package/dist/utils.d.ts.map +1 -1
  53. package/dist/utils.js +28 -0
  54. package/dist/virtual-terminal.d.ts +1 -0
  55. package/dist/virtual-terminal.d.ts.map +1 -1
  56. package/dist/virtual-terminal.js +3 -0
  57. package/package.json +2 -2
@@ -0,0 +1,32 @@
1
+ import type { Component } from "../tui.ts";
2
+ /** Live ANSI console pane backed by a headless xterm VT.
3
+ *
4
+ * Feed it raw program output (`write`); it parses cursor control, colors,
5
+ * and screen addressing into a bounded virtual screen, and `render()`
6
+ * emits the visible rows as styled ANSI lines for the host TUI. When
7
+ * focused, `handleInput` forwards raw key bytes to `onInput` (the pty
8
+ * master); a lone Esc calls `onExitKey` instead (kill semantics).
9
+ *
10
+ * This is the rendering half of an embedded console (run an interactive
11
+ * command inside a TUI without leaving it). Transport (pty bridge) and
12
+ * lifecycle live with the caller. */
13
+ export declare class AnsiConsole implements Component {
14
+ private readonly rows;
15
+ private readonly onInput;
16
+ private readonly onExitKey;
17
+ private readonly onRequestRender;
18
+ private readonly vt;
19
+ private dirty;
20
+ private cached;
21
+ private cachedWidth;
22
+ constructor(columns: number, rows: number, onInput: (data: string) => void, onExitKey: () => void, onRequestRender?: () => void);
23
+ /** Raw program output (ANSI allowed) into the virtual screen. */
24
+ write(data: string): void;
25
+ /** Resolves after every queued write has been parsed into the buffer
26
+ * (xterm processes writes asynchronously). Await before render(). */
27
+ flush(): Promise<void>;
28
+ render(width: number): string[];
29
+ handleInput(data: string): void;
30
+ invalidate(): void;
31
+ }
32
+ //# sourceMappingURL=ansi-console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-console.d.ts","sourceRoot":"","sources":["../../src/components/ansi-console.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAK3C;;;;;;;;;;sCAUsC;AACtC,qBAAa,WAAY,YAAW,SAAS;IAQzC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAVlC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAqC;IACxD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,WAAW,CAAM;gBAGvB,OAAO,EAAE,MAAM,EACE,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC/B,SAAS,EAAE,MAAM,IAAI,EACrB,eAAe,GAAE,MAAM,IAAe;IAUzD,iEAAiE;IACjE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzB;0EACsE;IACtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAc/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAU/B,UAAU,IAAI,IAAI;CAInB"}
@@ -0,0 +1,75 @@
1
+ import xterm from "@xterm/headless";
2
+ import { lineToAnsi } from "../virtual-terminal.js";
3
+ const XtermTerminal = xterm.Terminal;
4
+ /** Live ANSI console pane backed by a headless xterm VT.
5
+ *
6
+ * Feed it raw program output (`write`); it parses cursor control, colors,
7
+ * and screen addressing into a bounded virtual screen, and `render()`
8
+ * emits the visible rows as styled ANSI lines for the host TUI. When
9
+ * focused, `handleInput` forwards raw key bytes to `onInput` (the pty
10
+ * master); a lone Esc calls `onExitKey` instead (kill semantics).
11
+ *
12
+ * This is the rendering half of an embedded console (run an interactive
13
+ * command inside a TUI without leaving it). Transport (pty bridge) and
14
+ * lifecycle live with the caller. */
15
+ export class AnsiConsole {
16
+ rows;
17
+ onInput;
18
+ onExitKey;
19
+ onRequestRender;
20
+ vt;
21
+ dirty = true;
22
+ cached = [];
23
+ cachedWidth = -1;
24
+ constructor(columns, rows, onInput, onExitKey, onRequestRender = () => { }) {
25
+ this.rows = rows;
26
+ this.onInput = onInput;
27
+ this.onExitKey = onExitKey;
28
+ this.onRequestRender = onRequestRender;
29
+ this.vt = new XtermTerminal({
30
+ cols: columns,
31
+ rows,
32
+ disableStdin: true,
33
+ allowProposedApi: true,
34
+ });
35
+ }
36
+ /** Raw program output (ANSI allowed) into the virtual screen. */
37
+ write(data) {
38
+ this.vt.write(data);
39
+ this.dirty = true;
40
+ this.onRequestRender();
41
+ }
42
+ /** Resolves after every queued write has been parsed into the buffer
43
+ * (xterm processes writes asynchronously). Await before render(). */
44
+ flush() {
45
+ const { promise, resolve } = Promise.withResolvers();
46
+ this.vt.write("", () => resolve());
47
+ return promise;
48
+ }
49
+ render(width) {
50
+ if (this.dirty || this.cachedWidth !== width) {
51
+ this.cached = [];
52
+ const buffer = this.vt.buffer.active;
53
+ for (let row = 0; row < this.rows; row++) {
54
+ const line = buffer.getLine(buffer.viewportY + row);
55
+ this.cached.push(line ? lineToAnsi(line) : "");
56
+ }
57
+ this.cachedWidth = width;
58
+ this.dirty = false;
59
+ }
60
+ return this.cached.map((line) => line.slice(0, width));
61
+ }
62
+ handleInput(data) {
63
+ // A lone Esc terminates the console (pi semantics); escape sequences
64
+ // (ESC followed by more bytes) are forwarded untouched.
65
+ if (data === "\x1b") {
66
+ this.onExitKey();
67
+ return;
68
+ }
69
+ this.onInput(data);
70
+ }
71
+ invalidate() {
72
+ this.dirty = true;
73
+ this.cached = [];
74
+ }
75
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ansi-console.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-console.test.d.ts","sourceRoot":"","sources":["../../src/components/ansi-console.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { AnsiConsole } from "./ansi-console.js";
3
+ describe("AnsiConsole", () => {
4
+ test("renders raw ANSI output into bounded rows", async () => {
5
+ const pane = new AnsiConsole(20, 3, () => { }, () => { });
6
+ pane.write("hello \x1b[32mgreen\x1b[0m\r\nsecond line");
7
+ await pane.flush();
8
+ const lines = pane.render(20);
9
+ expect(lines.length).toBe(3);
10
+ expect(lines[0]).toContain("hello");
11
+ expect(lines[0]).toContain("green");
12
+ expect(lines[1]).toContain("second line");
13
+ });
14
+ test("handleInput forwards bytes; lone Esc triggers exit key", () => {
15
+ const forwarded = [];
16
+ let exited = false;
17
+ const pane = new AnsiConsole(20, 3, (data) => forwarded.push(data), () => {
18
+ exited = true;
19
+ });
20
+ pane.handleInput("a");
21
+ pane.handleInput("\x1b[B"); // arrow key escape sequence: forwarded
22
+ pane.handleInput("\x1b"); // lone Esc: exit
23
+ expect(forwarded).toEqual(["a", "\x1b[B"]);
24
+ expect(exited).toBe(true);
25
+ });
26
+ test("output beyond the visible rows scrolls the virtual screen", async () => {
27
+ const pane = new AnsiConsole(20, 3, () => { }, () => { });
28
+ pane.write("l1\nl2\nl3\nl4\nl5");
29
+ await pane.flush();
30
+ const lines = pane.render(20);
31
+ expect(lines.length).toBe(3);
32
+ expect(lines[2]).toContain("l5");
33
+ expect(lines.join("\n")).not.toContain("l1");
34
+ expect(lines.join("\n")).not.toContain("l2");
35
+ });
36
+ });
@@ -9,10 +9,27 @@ export interface LoaderIndicatorOptions {
9
9
  /**
10
10
  * Loader component that updates with an optional spinning animation.
11
11
  */
12
+ /** The one light-sweep palette. The loader's message band and every
13
+ * consumer's shimmer (oma's chrome rows, tool bodies) read from here, so the
14
+ * "working" animations cannot drift into different colors: cyan is the
15
+ * spinner accent, so band and spinner read as one animation. */
16
+ export declare const SHIMMER_TIER_OPEN: {
17
+ /** Base tier: dim, the resting color of an unsettled line. */
18
+ readonly low: "\u001B[2m";
19
+ /** Mid band: the spinner's own cyan. */
20
+ readonly mid: "\u001B[36m";
21
+ /** Crest: bold cyan. */
22
+ readonly high: "\u001B[1m\u001B[36m";
23
+ };
24
+ export type ShimmerTier = keyof typeof SHIMMER_TIER_OPEN;
12
25
  export declare class Loader extends Text {
13
26
  private frames;
14
27
  private intervalMs;
15
28
  private currentFrame;
29
+ /** Sweep position, INDEPENDENT of currentFrame: the spinner index wraps at
30
+ * frames.length (10), so reusing it capped the light band to the first ~10
31
+ * cells of the message instead of sweeping the whole line. */
32
+ private sweepStep;
16
33
  private intervalId;
17
34
  private ui;
18
35
  private renderIndicatorVerbatim;
@@ -26,7 +43,10 @@ export declare class Loader extends Text {
26
43
  setMessage(message: string): void;
27
44
  setIndicator(indicator?: LoaderIndicatorOptions): void;
28
45
  private restartAnimation;
29
- /** Moving light sweep across the message (omp shimmer-lite). */
46
+ /** Moving light sweep across the message (omp classic-shimmer shape: a
47
+ * cosine band ~2x BAND_HALF cells wide, three tiers, entering and leaving
48
+ * from outside the text). The rest stays in the base message color so the
49
+ * line stays legible while the band travels. */
30
50
  private animateMessage;
31
51
  private updateDisplay;
32
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD;;GAEG;AACH,qBAAa,MAAO,SAAQ,IAAI;IAC9B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,OAAO,CAAwB;gBAGrC,EAAE,EAAE,GAAG,EACP,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,OAAO,GAAE,MAAqB,EAC9B,SAAS,CAAC,EAAE,sBAAsB;IAU3B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAOxC,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAOZ,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMjC,YAAY,CAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,IAAI;IAWtD,OAAO,CAAC,gBAAgB;IAWxB,gEAAgE;IAChE,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,aAAa;CAStB"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD;;GAEG;AACH;;;iEAGiE;AACjE,eAAO,MAAM,iBAAiB;IAC5B,8DAA8D;;IAE9D,wCAAwC;;IAExC,wBAAwB;;CAEhB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAEzD,qBAAa,MAAO,SAAQ,IAAI;IAC9B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,YAAY,CAAK;IACzB;;mEAE+D;IAC/D,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,OAAO,CAAwB;gBAGrC,EAAE,EAAE,GAAG,EACP,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,OAAO,GAAE,MAAqB,EAC9B,SAAS,CAAC,EAAE,sBAAsB;IAU3B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAOxC,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAOZ,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMjC,YAAY,CAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,IAAI;IAYtD,OAAO,CAAC,gBAAgB;IAYxB;;;qDAGiD;IACjD,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,aAAa;CAStB"}
@@ -4,10 +4,26 @@ const DEFAULT_INTERVAL_MS = 80;
4
4
  /**
5
5
  * Loader component that updates with an optional spinning animation.
6
6
  */
7
+ /** The one light-sweep palette. The loader's message band and every
8
+ * consumer's shimmer (oma's chrome rows, tool bodies) read from here, so the
9
+ * "working" animations cannot drift into different colors: cyan is the
10
+ * spinner accent, so band and spinner read as one animation. */
11
+ export const SHIMMER_TIER_OPEN = {
12
+ /** Base tier: dim, the resting color of an unsettled line. */
13
+ low: "\u001b[2m",
14
+ /** Mid band: the spinner's own cyan. */
15
+ mid: "\u001b[36m",
16
+ /** Crest: bold cyan. */
17
+ high: "\u001b[1m\u001b[36m",
18
+ };
7
19
  export class Loader extends Text {
8
20
  frames = [...DEFAULT_FRAMES];
9
21
  intervalMs = DEFAULT_INTERVAL_MS;
10
22
  currentFrame = 0;
23
+ /** Sweep position, INDEPENDENT of currentFrame: the spinner index wraps at
24
+ * frames.length (10), so reusing it capped the light band to the first ~10
25
+ * cells of the message instead of sweeping the whole line. */
26
+ sweepStep = 0;
11
27
  intervalId = null;
12
28
  ui = null;
13
29
  renderIndicatorVerbatim = false;
@@ -52,6 +68,7 @@ export class Loader extends Text {
52
68
  ? indicator.intervalMs
53
69
  : DEFAULT_INTERVAL_MS;
54
70
  this.currentFrame = 0;
71
+ this.sweepStep = 0;
55
72
  this.start();
56
73
  }
57
74
  restartAnimation() {
@@ -61,30 +78,37 @@ export class Loader extends Text {
61
78
  }
62
79
  this.intervalId = setInterval(() => {
63
80
  this.currentFrame = (this.currentFrame + 1) % this.frames.length;
81
+ this.sweepStep += 1;
64
82
  this.updateDisplay();
65
83
  }, this.intervalMs);
66
84
  }
67
- /** Moving light sweep across the message (omp shimmer-lite). */
85
+ /** Moving light sweep across the message (omp classic-shimmer shape: a
86
+ * cosine band ~2x BAND_HALF cells wide, three tiers, entering and leaving
87
+ * from outside the text). The rest stays in the base message color so the
88
+ * line stays legible while the band travels. */
68
89
  animateMessage(message) {
69
90
  if (!message)
70
91
  return "";
71
- // Sweep a short accent band across the text; the rest stays in the
72
- // base message color so the line stays legible while it moves.
92
+ const chars = Array.from(message);
93
+ const BAND_HALF = 6;
94
+ const period = chars.length + BAND_HALF * 2;
95
+ const pos = this.sweepStep % period;
73
96
  const base = this.messageColorFn;
74
- const period = message.length + 4;
75
- const start = this.currentFrame % period;
76
97
  let out = "";
77
- for (let i = 0; i < message.length; i++) {
78
- const dist = Math.abs(i - start);
79
- if (dist < 1) {
80
- out += `\u001b[1m\u001b[36m${message[i]}\u001b[0m`;
81
- }
82
- else if (dist < 2) {
83
- out += `\u001b[36m${message[i]}\u001b[0m`;
84
- }
85
- else {
86
- out += base(message[i]);
98
+ for (let i = 0; i < chars.length; i++) {
99
+ const ch = chars[i];
100
+ const dist = Math.abs(i + BAND_HALF - pos);
101
+ if (dist >= BAND_HALF) {
102
+ out += base(ch);
103
+ continue;
87
104
  }
105
+ const intensity = 0.5 * (1 + Math.cos((Math.PI * dist) / BAND_HALF));
106
+ if (intensity >= 0.65)
107
+ out += `${SHIMMER_TIER_OPEN.high}${ch}\u001b[0m`;
108
+ else if (intensity >= 0.22)
109
+ out += `${SHIMMER_TIER_OPEN.mid}${ch}\u001b[0m`;
110
+ else
111
+ out += base(ch);
88
112
  }
89
113
  return out;
90
114
  }
@@ -23,6 +23,11 @@ export interface DefaultTextStyle {
23
23
  */
24
24
  export interface MarkdownTheme {
25
25
  heading: (text: string) => string;
26
+ /** Style for the `###`-run that marks a level-3+ heading. Optional: absent
27
+ * means the marker rides the heading style (omp's behaviour). A theme can
28
+ * dim it instead, so the level stays readable without shouting over the
29
+ * heading text. */
30
+ headingMarker?: (text: string) => string;
26
31
  link: (text: string) => string;
27
32
  linkUrl: (text: string) => string;
28
33
  code: (text: string) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAqI3C;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,gCAAgC;IAChC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,gBAAgB;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kBAAkB;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yBAAyB;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC1D,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAOD,qBAAa,QAAS,YAAW,SAAS;IACxC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,kBAAkB,CAAC,CAAS;IAGpC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAW;IAO/B,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,kBAAkB,CAAC,CAAU;gBAGnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,aAAa,EACpB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,OAAO,CAAC,EAAE,eAAe;IAU3B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAY3B,UAAU,IAAI,IAAI;IAMlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IA8F/B;;;2BAGuB;IACvB,OAAO,CAAC,SAAS;IAoBjB;;qCAEiC;IACjC,OAAO,CAAC,kBAAkB;IA0B1B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,qBAAqB;IAkC7B,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,WAAW;IA2MnB,OAAO,CAAC,kBAAkB;IAiG1B,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,sBAAsB;IAK9B;;OAEG;IACH,OAAO,CAAC,UAAU;IAyDlB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,OAAO,CAAC,WAAW;CAgLpB"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { Marked, Tokenizer } from "marked";
2
2
  import { resolveMermaidAscii } from "../mermaid.js";
3
3
  import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
4
- import { applyBackgroundToLine, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "../utils.js";
4
+ import { textSizingEnabled } from "../text-sizing.js";
5
+ import { applyBackgroundToLine, encodeTextSized, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "../utils.js";
5
6
  const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
6
7
  class StrictStrikethroughTokenizer extends Tokenizer {
7
8
  del(src) {
@@ -386,7 +387,27 @@ export class Markdown {
386
387
  stylePrefix: this.getStylePrefix(headingStyleFn),
387
388
  };
388
389
  const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);
389
- const styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;
390
+ // h1 at double size (omp parity, OSC 66): the terminal draws the glyph
391
+ // twice as large over two rows, so the line is emitted only when the
392
+ // doubled width still fits and the second row is reserved as a blank.
393
+ // Off by default — see text-sizing.ts for why an unsupported terminal
394
+ // must never receive the sequence.
395
+ if (headingLevel === 1 && textSizingEnabled() && !token.tokens?.some(needsSizing)) {
396
+ const plain = plainInlineText(token.tokens || []);
397
+ const plainWidth = visibleWidth(plain);
398
+ if (plainWidth > 0 && 2 * plainWidth <= width) {
399
+ lines.push(headingStyleFn(encodeTextSized(plain, 2)));
400
+ lines.push("");
401
+ if (nextTokenType && nextTokenType !== "space") {
402
+ lines.push("");
403
+ }
404
+ break;
405
+ }
406
+ }
407
+ const markerFn = this.theme.headingMarker;
408
+ const styledHeading = headingLevel >= 3
409
+ ? (markerFn ? markerFn(headingPrefix) : headingStyleFn(headingPrefix)) + headingText
410
+ : headingText;
390
411
  lines.push(styledHeading);
391
412
  if (nextTokenType && nextTokenType !== "space") {
392
413
  lines.push(""); // Add spacing after headings (unless space token follows)
@@ -845,3 +866,27 @@ export class Markdown {
845
866
  return lines;
846
867
  }
847
868
  }
869
+ /** Plain text of an inline token run. An OSC 66 payload must be plain: any
870
+ * styling inside it would leak escape bytes into the sized span, so callers
871
+ * only size runs where every child is text. */
872
+ function plainInlineText(tokens) {
873
+ let out = "";
874
+ for (const token of tokens) {
875
+ if ("text" in token && typeof token.text === "string") {
876
+ out += token.text;
877
+ continue;
878
+ }
879
+ if ("tokens" in token && Array.isArray(token.tokens)) {
880
+ out += plainInlineText(token.tokens);
881
+ continue;
882
+ }
883
+ if ("raw" in token && typeof token.raw === "string")
884
+ out += token.raw;
885
+ }
886
+ return out.replace(/\s+/g, " ").trim();
887
+ }
888
+ /** A child that must keep its own styling (code, emphasis, links) — a heading
889
+ * containing one stays at normal size. */
890
+ function needsSizing(token) {
891
+ return token.type !== "text";
892
+ }
@@ -1,5 +1,6 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { resolveMermaidAscii } from "../mermaid.js";
3
+ import { setTextSizingEnabled } from "../text-sizing.js";
3
4
  import { visibleWidth } from "../utils.js";
4
5
  import { Markdown } from "./markdown.js";
5
6
  const theme = {
@@ -79,3 +80,31 @@ describe("markdown streaming lex cache", () => {
79
80
  expect(stream.render(80)).toEqual(expected);
80
81
  });
81
82
  });
83
+ describe("heading hierarchy (omp parity)", () => {
84
+ afterEach(() => setTextSizingEnabled(undefined));
85
+ test("a level-3+ marker goes through headingMarker when the theme has one", () => {
86
+ const marked = { ...theme, headingMarker: (t) => `<${t}>` };
87
+ const rows = new Markdown("### Layer one\n\nbody", 0, 0, marked).render(40);
88
+ expect(rows[0]).toContain("<### >");
89
+ // Level 1 and 2 carry no marker at all.
90
+ const h2 = new Markdown("## Section\n\nbody", 0, 0, marked).render(40);
91
+ expect(h2[0]).not.toContain("<");
92
+ });
93
+ test("h1 is sized only when the capability is on and the doubled width fits", () => {
94
+ setTextSizingEnabled(false);
95
+ const plain = new Markdown("# Report\n\nbody", 0, 0, theme).render(40);
96
+ expect(plain.join("\n")).not.toContain("]66;");
97
+ setTextSizingEnabled(true);
98
+ const sized = new Markdown("# Report\n\nbody", 0, 0, theme).render(40);
99
+ expect(sized.join("\n")).toContain("]66;s=2;");
100
+ // The doubled glyph occupies two rows: the renderer reserves the second
101
+ // (padded to the container width like every other row).
102
+ expect(sized[1]?.trim()).toBe("");
103
+ // Body text is untouched.
104
+ expect(sized.join("\n")).toContain("body");
105
+ // A heading too wide to double stays at normal size instead of wrapping.
106
+ const wide = `# ${"x".repeat(30)}`;
107
+ const narrow = new Markdown(`${wide}\n\nbody`, 0, 0, theme).render(40);
108
+ expect(narrow.join("\n")).not.toContain("]66;");
109
+ });
110
+ });
@@ -33,7 +33,13 @@ export declare class SelectList implements Component {
33
33
  onSelect?: (item: SelectItem) => void;
34
34
  onCancel?: () => void;
35
35
  onSelectionChange?: (item: SelectItem) => void;
36
+ /** Fired on the delete binding. The owner removes the item and calls
37
+ * setItems — the list does not mutate its collection behind its back. */
38
+ onDelete?: (item: SelectItem) => void;
36
39
  constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme, layout?: SelectListLayoutOptions);
40
+ /** Replace the collection (after a delete, a reload). Selection is clamped
41
+ * so it never points past the new end. */
42
+ setItems(items: readonly SelectItem[]): void;
37
43
  setFilter(filter: string): void;
38
44
  setSelectedIndex(index: number): void;
39
45
  invalidate(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"select-list.d.ts","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAW3C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,gCAAgC,KAAK,MAAM,CAAC;CACzE;AAED,qBAAa,UAAW,YAAW,SAAS;IAC1C,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,MAAM,CAA0B;IAEjC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;gBAGpD,KAAK,EAAE,UAAU,EAAE,EACnB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,eAAe,EACtB,MAAM,GAAE,uBAA4B;IAStC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQ/B,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IA6C/B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IA6BlC,OAAO,CAAC,UAAU;IA+ClB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,qBAAqB;IAO7B,eAAe,IAAI,UAAU,GAAG,IAAI;CAIrC"}
1
+ {"version":3,"file":"select-list.d.ts","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAW3C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,gCAAgC,KAAK,MAAM,CAAC;CACzE;AAED,qBAAa,UAAW,YAAW,SAAS;IAC1C,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,MAAM,CAA0B;IAEjC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACtD;8EAC0E;IACnE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;gBAG3C,KAAK,EAAE,UAAU,EAAE,EACnB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,eAAe,EACtB,MAAM,GAAE,uBAA4B;IAStC;+CAC2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI;IAO5C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQ/B,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IA6C/B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAoClC,OAAO,CAAC,UAAU;IA+ClB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,qBAAqB;IAO7B,eAAe,IAAI,UAAU,GAAG,IAAI;CAIrC"}
@@ -15,6 +15,9 @@ export class SelectList {
15
15
  onSelect;
16
16
  onCancel;
17
17
  onSelectionChange;
18
+ /** Fired on the delete binding. The owner removes the item and calls
19
+ * setItems — the list does not mutate its collection behind its back. */
20
+ onDelete;
18
21
  constructor(items, maxVisible, theme, layout = {}) {
19
22
  this.items = items;
20
23
  this.filteredItems = items;
@@ -22,6 +25,14 @@ export class SelectList {
22
25
  this.theme = theme;
23
26
  this.layout = layout;
24
27
  }
28
+ /** Replace the collection (after a delete, a reload). Selection is clamped
29
+ * so it never points past the new end. */
30
+ setItems(items) {
31
+ this.items = [...items];
32
+ this.filteredItems = this.items;
33
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.items.length - 1));
34
+ this.notifySelectionChange();
35
+ }
25
36
  setFilter(filter) {
26
37
  this.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));
27
38
  // Reset selection when filter changes
@@ -84,6 +95,13 @@ export class SelectList {
84
95
  this.onSelect(selectedItem);
85
96
  }
86
97
  }
98
+ // Delete
99
+ else if (kb.matches(keyData, "tui.select.delete")) {
100
+ const selectedItem = this.filteredItems[this.selectedIndex];
101
+ if (selectedItem && this.onDelete) {
102
+ this.onDelete(selectedItem);
103
+ }
104
+ }
87
105
  // Escape or Ctrl+C
88
106
  else if (kb.matches(keyData, "tui.select.cancel")) {
89
107
  if (this.onCancel) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=select-list.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-list.test.d.ts","sourceRoot":"","sources":["../../src/components/select-list.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { SelectList } from "./select-list.js";
3
+ const theme = {
4
+ selectedPrefix: (t) => t,
5
+ selectedText: (t) => t,
6
+ description: (t) => t,
7
+ scrollInfo: (t) => t,
8
+ noMatch: (t) => t,
9
+ };
10
+ const items = () => [
11
+ { value: "a", label: "09-15 10:00", description: "first" },
12
+ { value: "b", label: "09-15 09:00", description: "second" },
13
+ { value: "c", label: "09-15 08:00", description: "third" },
14
+ ];
15
+ /** Ctrl+D is the picker's delete chord (EOT in legacy terminals, CSI-u under
16
+ * the Kitty protocol). The list only REPORTS the deletion — the owner
17
+ * removes the item and calls setItems, so the collection never mutates
18
+ * behind the owner's back. */
19
+ describe("SelectList delete", () => {
20
+ test("ctrl+d reports the selected item", () => {
21
+ const list = new SelectList(items(), 10, theme);
22
+ const deleted = [];
23
+ list.onDelete = (item) => deleted.push(item.value);
24
+ list.handleInput("\u0004");
25
+ expect(deleted).toEqual(["a"]);
26
+ });
27
+ test("the delete chord only fires when a handler is attached", () => {
28
+ const list = new SelectList(items(), 10, theme);
29
+ // No throw and no selection change: an unhandled chord is a no-op.
30
+ expect(() => list.handleInput("\u0004")).not.toThrow();
31
+ });
32
+ test("setItems replaces the collection and clamps the selection", () => {
33
+ const list = new SelectList(items(), 10, theme);
34
+ list.handleInput("\u001b[B"); // down -> "b"
35
+ list.handleInput("\u001b[B"); // down -> "c" (last)
36
+ list.setItems([
37
+ { value: "a", label: "09-15 10:00", description: "first" },
38
+ { value: "b", label: "09-15 09:00", description: "second" },
39
+ ]);
40
+ const seen = [];
41
+ list.onDelete = (item) => seen.push(item.value);
42
+ // Selection was on "c" (index 2); after the shrink it must resolve to the
43
+ // new last row rather than pointing past the end.
44
+ list.handleInput("\u0004");
45
+ expect(seen).toEqual(["b"]);
46
+ });
47
+ test("an emptied list renders without throwing", () => {
48
+ const list = new SelectList(items(), 10, theme);
49
+ list.setItems([]);
50
+ expect(() => list.render(40)).not.toThrow();
51
+ expect(() => list.handleInput("\u0004")).not.toThrow();
52
+ });
53
+ });
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { type AutocompleteItem, type AutocompleteProvider, type AutocompleteSuggestions, CombinedAutocompleteProvider, type SlashCommand, } from "./autocomplete.ts";
2
+ export { AnsiConsole } from "./components/ansi-console.ts";
2
3
  export { Box } from "./components/box.ts";
3
4
  export { Card, type CardOptions } from "./components/card.ts";
4
5
  export { Editor, type EditorOptions, type EditorTheme, } from "./components/editor.ts";
5
6
  export { Input } from "./components/input.ts";
6
- export { Loader, type LoaderIndicatorOptions } from "./components/loader.ts";
7
+ export { Loader, type LoaderIndicatorOptions, SHIMMER_TIER_OPEN, type ShimmerTier, } from "./components/loader.ts";
7
8
  export { type DefaultTextStyle, Markdown, type MarkdownOptions, type MarkdownTheme, } from "./components/markdown.ts";
8
9
  export { CachedOutputBlock, type OutputBlockOptions, type OutputBlockSection, type OutputBlockState, outputBlockContentWidth, renderOutputBlock, } from "./components/output-block.ts";
9
10
  export { type SelectItem, SelectList, type SelectListLayoutOptions, type SelectListTheme, type SelectListTruncatePrimaryContext, } from "./components/select-list.ts";
@@ -16,9 +17,10 @@ export { getKeybindings, type Keybinding, type KeybindingConflict, type Keybindi
16
17
  export { decodeKittyPrintable, isKeyRelease, isKeyRepeat, isKittyProtocolActive, Key, type KeyEventType, type KeyId, matchesKey, parseKey, setKittyProtocolActive, } from "./keys.ts";
17
18
  export { parseSgrMouse, routeSgrMouseInput, type SgrMouseEvent, type SgrMouseHandler, } from "./mouse.ts";
18
19
  export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions, } from "./stdin-buffer.ts";
19
- export { ProcessTerminal, type Terminal } from "./terminal.ts";
20
+ export { KITTY_KEYBOARD_SET_FLAGS, ProcessTerminal, type Terminal, } from "./terminal.ts";
20
21
  export { parseOsc11BackgroundColor, parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme, } from "./terminal-colors.ts";
21
22
  export { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions, } from "./terminal-image.ts";
23
+ export { setTextSizingEnabled, textSizingEnabled, textSizingSupported, } from "./text-sizing.ts";
22
24
  export { type Component, Container, CURSOR_MARKER, type Focusable, type FramePlan, getNativeScrollbackLiveRegionStart, getRenderStablePrefixRows, type HistoryBatch, isFocusable, type NativeScrollbackCommittedRows, type NativeScrollbackLiveRegion, type NativeScrollbackReplay, type OverlayAnchor, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, prepareNativeScrollbackReplay, type RenderStablePrefix, type SizeValue, setNativeScrollbackCommittedRows, type TerminalFrameProvider, TUI, type ViewportTailProvider, } from "./tui.ts";
23
25
  export { applyBackgroundToLine, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "./utils.ts";
24
26
  export { VirtualTerminal } from "./virtual-terminal.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,11 +4,12 @@
4
4
  // Autocomplete support (editor dependency)
5
5
  export { CombinedAutocompleteProvider, } from "./autocomplete.js";
6
6
  // Components
7
+ export { AnsiConsole } from "./components/ansi-console.js";
7
8
  export { Box } from "./components/box.js";
8
9
  export { Card } from "./components/card.js";
9
10
  export { Editor, } from "./components/editor.js";
10
11
  export { Input } from "./components/input.js";
11
- export { Loader } from "./components/loader.js";
12
+ export { Loader, SHIMMER_TIER_OPEN, } from "./components/loader.js";
12
13
  export { Markdown, } from "./components/markdown.js";
13
14
  export { CachedOutputBlock, outputBlockContentWidth, renderOutputBlock, } from "./components/output-block.js";
14
15
  export { SelectList, } from "./components/select-list.js";
@@ -25,11 +26,12 @@ export { parseSgrMouse, routeSgrMouseInput, } from "./mouse.js";
25
26
  // Input buffering for batch splitting
26
27
  export { StdinBuffer, } from "./stdin-buffer.js";
27
28
  // Terminal interface and implementations
28
- export { ProcessTerminal } from "./terminal.js";
29
+ export { KITTY_KEYBOARD_SET_FLAGS, ProcessTerminal, } from "./terminal.js";
29
30
  // Terminal colors
30
31
  export { parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
31
32
  // Terminal image helpers (used internally by tui.ts)
32
33
  export { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions, } from "./terminal-image.js";
34
+ export { setTextSizingEnabled, textSizingEnabled, textSizingSupported, } from "./text-sizing.js";
33
35
  export { Container, CURSOR_MARKER, getNativeScrollbackLiveRegionStart, getRenderStablePrefixRows, isFocusable, prepareNativeScrollbackReplay, setNativeScrollbackCommittedRows, TUI, } from "./tui.js";
34
36
  // Utilities
35
37
  export { applyBackgroundToLine, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "./utils.js";