@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.
- package/dist/components/ansi-console.d.ts +32 -0
- package/dist/components/ansi-console.d.ts.map +1 -0
- package/dist/components/ansi-console.js +75 -0
- package/dist/components/ansi-console.test.d.ts +2 -0
- package/dist/components/ansi-console.test.d.ts.map +1 -0
- package/dist/components/ansi-console.test.js +36 -0
- package/dist/components/loader.d.ts +21 -1
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +39 -15
- package/dist/components/markdown.d.ts +5 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +47 -2
- package/dist/components/markdown.test.js +30 -1
- package/dist/components/select-list.d.ts +6 -0
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +18 -0
- package/dist/components/select-list.test.d.ts +2 -0
- package/dist/components/select-list.test.d.ts.map +1 -0
- package/dist/components/select-list.test.js +53 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/keybindings.d.ts +5 -0
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +7 -0
- package/dist/loader-sweep.test.d.ts +2 -0
- package/dist/loader-sweep.test.d.ts.map +1 -0
- package/dist/loader-sweep.test.js +67 -0
- package/dist/stdin-buffer.d.ts +1 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +19 -0
- package/dist/stdin-buffer.test.d.ts +2 -0
- package/dist/stdin-buffer.test.d.ts.map +1 -0
- package/dist/stdin-buffer.test.js +38 -0
- package/dist/terminal-reassert.test.d.ts +2 -0
- package/dist/terminal-reassert.test.d.ts.map +1 -0
- package/dist/terminal-reassert.test.js +18 -0
- package/dist/terminal.d.ts +14 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +18 -0
- package/dist/text-sizing.d.ts +7 -0
- package/dist/text-sizing.d.ts.map +1 -0
- package/dist/text-sizing.js +34 -0
- package/dist/text-sizing.test.d.ts +2 -0
- package/dist/text-sizing.test.d.ts.map +1 -0
- package/dist/text-sizing.test.js +43 -0
- package/dist/tui-scrollback.test.js +36 -0
- package/dist/tui.d.ts +8 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +73 -7
- package/dist/utils.d.ts +1 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +28 -0
- package/dist/virtual-terminal.d.ts +1 -0
- package/dist/virtual-terminal.d.ts.map +1 -1
- package/dist/virtual-terminal.js +3 -0
- 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 @@
|
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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 <
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
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 @@
|
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,4BAA4B,EAC5B,KAAK,YAAY,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EACL,MAAM,EACN,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,
|
|
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";
|