@chengchenccc/tui 0.1.1-rc.1
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/autocomplete.d.ts +56 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +647 -0
- package/dist/components/box.d.ts +22 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +103 -0
- package/dist/components/card.d.ts +33 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +49 -0
- package/dist/components/editor.d.ts +257 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1980 -0
- package/dist/components/input.d.ts +37 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +389 -0
- package/dist/components/loader.d.ts +33 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +100 -0
- package/dist/components/markdown.d.ts +108 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +847 -0
- package/dist/components/markdown.test.d.ts +2 -0
- package/dist/components/markdown.test.d.ts.map +1 -0
- package/dist/components/markdown.test.js +81 -0
- package/dist/components/output-block.d.ts +44 -0
- package/dist/components/output-block.d.ts.map +1 -0
- package/dist/components/output-block.js +131 -0
- package/dist/components/select-list.d.ts +50 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +166 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +22 -0
- package/dist/components/statusline.d.ts +30 -0
- package/dist/components/statusline.d.ts.map +1 -0
- package/dist/components/statusline.js +46 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +90 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +50 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +109 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/keybindings.d.ts +193 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +191 -0
- package/dist/keys.d.ts +184 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1191 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +43 -0
- package/dist/mermaid.d.ts +23 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js +77 -0
- package/dist/mouse.d.ts +40 -0
- package/dist/mouse.d.ts.map +1 -0
- package/dist/mouse.js +39 -0
- package/dist/mouse.test.d.ts +2 -0
- package/dist/mouse.test.d.ts.map +1 -0
- package/dist/mouse.test.js +36 -0
- package/dist/native-modifiers.d.ts +3 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +52 -0
- package/dist/stdin-buffer.d.ts +50 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +360 -0
- package/dist/terminal-colors.d.ts +10 -0
- package/dist/terminal-colors.d.ts.map +1 -0
- package/dist/terminal-colors.js +58 -0
- package/dist/terminal-image.d.ts +90 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +375 -0
- package/dist/terminal.d.ts +110 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +432 -0
- package/dist/tui-scrollback.test.d.ts +2 -0
- package/dist/tui-scrollback.test.d.ts.map +1 -0
- package/dist/tui-scrollback.test.js +51 -0
- package/dist/tui.d.ts +330 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1568 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +24 -0
- package/dist/utils.d.ts +85 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1066 -0
- package/dist/vendor/mermaid-ascii/ascii/ansi.d.ts +42 -0
- package/dist/vendor/mermaid-ascii/ascii/ansi.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/ansi.js +361 -0
- package/dist/vendor/mermaid-ascii/ascii/canvas.d.ts +90 -0
- package/dist/vendor/mermaid-ascii/ascii/canvas.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/canvas.js +414 -0
- package/dist/vendor/mermaid-ascii/ascii/class-diagram.d.ts +8 -0
- package/dist/vendor/mermaid-ascii/ascii/class-diagram.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/class-diagram.js +623 -0
- package/dist/vendor/mermaid-ascii/ascii/converter.d.ts +13 -0
- package/dist/vendor/mermaid-ascii/ascii/converter.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/converter.js +233 -0
- package/dist/vendor/mermaid-ascii/ascii/draw.d.ts +67 -0
- package/dist/vendor/mermaid-ascii/ascii/draw.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/draw.js +1313 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +49 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-bundling.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-bundling.js +289 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-routing.d.ts +44 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-routing.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/edge-routing.js +302 -0
- package/dist/vendor/mermaid-ascii/ascii/er-diagram.d.ts +8 -0
- package/dist/vendor/mermaid-ascii/ascii/er-diagram.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/er-diagram.js +380 -0
- package/dist/vendor/mermaid-ascii/ascii/grid.d.ts +57 -0
- package/dist/vendor/mermaid-ascii/ascii/grid.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/grid.js +516 -0
- package/dist/vendor/mermaid-ascii/ascii/index.d.ts +66 -0
- package/dist/vendor/mermaid-ascii/ascii/index.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/index.js +133 -0
- package/dist/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +28 -0
- package/dist/vendor/mermaid-ascii/ascii/multiline-utils.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/multiline-utils.js +60 -0
- package/dist/vendor/mermaid-ascii/ascii/pathfinder.d.ts +18 -0
- package/dist/vendor/mermaid-ascii/ascii/pathfinder.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/pathfinder.js +224 -0
- package/dist/vendor/mermaid-ascii/ascii/sequence.d.ts +8 -0
- package/dist/vendor/mermaid-ascii/ascii/sequence.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/sequence.js +416 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +12 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/circle.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/circle.js +22 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +35 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/corners.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/corners.js +91 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +12 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/diamond.js +22 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +12 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/hexagon.js +22 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/index.d.ts +27 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/index.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/index.js +66 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +32 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rectangle.js +149 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +12 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/rounded.js +22 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/special.d.ts +60 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/special.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/special.js +253 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +18 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/stadium.js +103 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/state.d.ts +31 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/state.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/state.js +167 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/types.d.ts +56 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/shapes/types.js +4 -0
- package/dist/vendor/mermaid-ascii/ascii/types.d.ts +207 -0
- package/dist/vendor/mermaid-ascii/ascii/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/types.js +39 -0
- package/dist/vendor/mermaid-ascii/ascii/validate.d.ts +52 -0
- package/dist/vendor/mermaid-ascii/ascii/validate.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/validate.js +97 -0
- package/dist/vendor/mermaid-ascii/ascii/xychart.d.ts +3 -0
- package/dist/vendor/mermaid-ascii/ascii/xychart.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/ascii/xychart.js +719 -0
- package/dist/vendor/mermaid-ascii/class/parser.d.ts +7 -0
- package/dist/vendor/mermaid-ascii/class/parser.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/class/parser.js +265 -0
- package/dist/vendor/mermaid-ascii/class/types.d.ts +103 -0
- package/dist/vendor/mermaid-ascii/class/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/class/types.js +7 -0
- package/dist/vendor/mermaid-ascii/er/parser.d.ts +7 -0
- package/dist/vendor/mermaid-ascii/er/parser.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/er/parser.js +162 -0
- package/dist/vendor/mermaid-ascii/er/types.d.ts +77 -0
- package/dist/vendor/mermaid-ascii/er/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/er/types.js +7 -0
- package/dist/vendor/mermaid-ascii/index.d.ts +2 -0
- package/dist/vendor/mermaid-ascii/index.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/index.js +13 -0
- package/dist/vendor/mermaid-ascii/multiline-utils.d.ts +10 -0
- package/dist/vendor/mermaid-ascii/multiline-utils.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/multiline-utils.js +29 -0
- package/dist/vendor/mermaid-ascii/parser.d.ts +8 -0
- package/dist/vendor/mermaid-ascii/parser.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/parser.js +558 -0
- package/dist/vendor/mermaid-ascii/sequence/parser.d.ts +7 -0
- package/dist/vendor/mermaid-ascii/sequence/parser.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/sequence/parser.js +182 -0
- package/dist/vendor/mermaid-ascii/sequence/types.d.ts +131 -0
- package/dist/vendor/mermaid-ascii/sequence/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/sequence/types.js +7 -0
- package/dist/vendor/mermaid-ascii/text-metrics.d.ts +24 -0
- package/dist/vendor/mermaid-ascii/text-metrics.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/text-metrics.js +69 -0
- package/dist/vendor/mermaid-ascii/types.d.ts +115 -0
- package/dist/vendor/mermaid-ascii/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/types.js +4 -0
- package/dist/vendor/mermaid-ascii/xychart/colors.d.ts +26 -0
- package/dist/vendor/mermaid-ascii/xychart/colors.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/xychart/colors.js +145 -0
- package/dist/vendor/mermaid-ascii/xychart/parser.d.ts +7 -0
- package/dist/vendor/mermaid-ascii/xychart/parser.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/xychart/parser.js +105 -0
- package/dist/vendor/mermaid-ascii/xychart/types.d.ts +146 -0
- package/dist/vendor/mermaid-ascii/xychart/types.d.ts.map +1 -0
- package/dist/vendor/mermaid-ascii/xychart/types.js +8 -0
- package/dist/virtual-terminal.d.ts +80 -0
- package/dist/virtual-terminal.d.ts.map +1 -0
- package/dist/virtual-terminal.js +284 -0
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +97 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.test.d.ts","sourceRoot":"","sources":["../../src/components/markdown.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveMermaidAscii } from "../mermaid.js";
|
|
3
|
+
import { visibleWidth } from "../utils.js";
|
|
4
|
+
import { Markdown } from "./markdown.js";
|
|
5
|
+
const theme = {
|
|
6
|
+
heading: (s) => s,
|
|
7
|
+
link: (s) => s,
|
|
8
|
+
linkUrl: (s) => s,
|
|
9
|
+
code: (s) => s,
|
|
10
|
+
codeBlock: (s) => s,
|
|
11
|
+
codeBlockBorder: (s) => s,
|
|
12
|
+
quote: (s) => s,
|
|
13
|
+
quoteBorder: (s) => s,
|
|
14
|
+
hr: (s) => s,
|
|
15
|
+
listBullet: (s) => s,
|
|
16
|
+
bold: (s) => s,
|
|
17
|
+
italic: (s) => s,
|
|
18
|
+
strikethrough: (s) => s,
|
|
19
|
+
underline: (s) => s,
|
|
20
|
+
};
|
|
21
|
+
const wideDiagram = `graph LR
|
|
22
|
+
Start[Start Request] --> Auth{Auth Valid?}
|
|
23
|
+
Auth --No--> Deny[401 Unauthorized]
|
|
24
|
+
Auth --Yes--> Query[Query Service] --> DB[(Database)]
|
|
25
|
+
Query --> Build[Build Response] --> OK[200 OK]`;
|
|
26
|
+
describe("markdown mermaid", () => {
|
|
27
|
+
test("clips wide mermaid ASCII rows instead of wrapping them", () => {
|
|
28
|
+
const md = new Markdown(`\`\`\`mermaid\n${wideDiagram}\n\`\`\``, 0, 0, theme);
|
|
29
|
+
const resolved = resolveMermaidAscii(wideDiagram, { maxWidth: 40 });
|
|
30
|
+
expect(resolved).not.toBeNull();
|
|
31
|
+
const resolvedLines = resolved.split("\n");
|
|
32
|
+
// Re-fit can pick a taller TD layout when the base LR graph overflows.
|
|
33
|
+
const rendered = md.render(40);
|
|
34
|
+
// Every preformatted row stays exactly one row; the wrap pass must not
|
|
35
|
+
// fragment the box-drawing canvas.
|
|
36
|
+
expect(rendered.length).toBe(resolvedLines.length);
|
|
37
|
+
for (const line of rendered) {
|
|
38
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(40);
|
|
39
|
+
}
|
|
40
|
+
for (const line of rendered) {
|
|
41
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(40);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test("open mermaid fence stays a code fence until the closing fence arrives", () => {
|
|
45
|
+
const md = new Markdown(`\`\`\`mermaid\n${wideDiagram}`, 0, 0, theme);
|
|
46
|
+
const rendered = md.render(80).join("\n");
|
|
47
|
+
// No ASCII diagram while the fence is still streaming (would re-layout every
|
|
48
|
+
// chunk and stall the TUI); render the raw fence instead.
|
|
49
|
+
expect(rendered).toContain("```mermaid");
|
|
50
|
+
expect(rendered).not.toContain("┌");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("markdown streaming lex cache", () => {
|
|
54
|
+
test("append across blank-line boundaries matches one-shot render", () => {
|
|
55
|
+
const full = "# Title\n\nThis is a paragraph.\n\n- item one\n- item two";
|
|
56
|
+
const expected = new Markdown(full, 0, 0, theme).render(80);
|
|
57
|
+
const stream = new Markdown("", 0, 0, theme);
|
|
58
|
+
let acc = "";
|
|
59
|
+
for (const chunk of ["# Title", "\n\nThis is a paragraph.", "\n\n- item one\n- item two"]) {
|
|
60
|
+
acc += chunk;
|
|
61
|
+
stream.setText(acc);
|
|
62
|
+
const rendered = stream.render(80);
|
|
63
|
+
// Every intermediate render must already match the one-shot tail prefix.
|
|
64
|
+
if (acc === full) {
|
|
65
|
+
expect(rendered).toEqual(expected);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
expect(stream.render(80)).toEqual(expected);
|
|
69
|
+
});
|
|
70
|
+
test("single long paragraph without a blank line falls back safely", () => {
|
|
71
|
+
const long = "This is a long sentence. ".repeat(60).trim();
|
|
72
|
+
const expected = new Markdown(long, 0, 0, theme).render(80);
|
|
73
|
+
const stream = new Markdown("", 0, 0, theme);
|
|
74
|
+
let acc = "";
|
|
75
|
+
for (let i = 0; i < long.length; i += 47) {
|
|
76
|
+
acc += long.slice(i, i + 47);
|
|
77
|
+
stream.setText(acc);
|
|
78
|
+
}
|
|
79
|
+
expect(stream.render(80)).toEqual(expected);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type OutputBlockState = "pending" | "running" | "success" | "error" | "warning";
|
|
2
|
+
export interface OutputBlockSection {
|
|
3
|
+
/** Rendered as a label bar: ├── label ─────┤ */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Draw a plain divider bar instead of (or in addition to) the label. */
|
|
6
|
+
separator?: boolean;
|
|
7
|
+
/** Body lines; each may contain ANSI codes. */
|
|
8
|
+
lines: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface OutputBlockOptions {
|
|
11
|
+
/** Title-bar text (usually from renderToolHeader). */
|
|
12
|
+
header?: string;
|
|
13
|
+
/** Trailing meta appended to the header bar. */
|
|
14
|
+
headerMeta?: string;
|
|
15
|
+
state: OutputBlockState;
|
|
16
|
+
sections: readonly OutputBlockSection[];
|
|
17
|
+
width: number;
|
|
18
|
+
/** Override the state-derived border color (raw ANSI SGR). */
|
|
19
|
+
borderColor?: string;
|
|
20
|
+
/** Whole-block background tint. */
|
|
21
|
+
bg?: (line: string) => string;
|
|
22
|
+
applyBg?: boolean;
|
|
23
|
+
contentPaddingLeft?: number;
|
|
24
|
+
contentPaddingRight?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Inner content width for a given outer width and symmetric padding. */
|
|
27
|
+
export declare function outputBlockContentWidth(width: number, contentPaddingLeft?: number, contentPaddingRight?: number): number;
|
|
28
|
+
export declare function renderOutputBlock(options: OutputBlockOptions): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Cached wrapper around renderOutputBlock.
|
|
31
|
+
*
|
|
32
|
+
* Tool blocks are re-rendered every frame while the transcript is live. The
|
|
33
|
+
* key hashes every field that materially changes the produced lines, so a
|
|
34
|
+
* stable block avoids re-running visibleWidth / wrapTextWithAnsi on every
|
|
35
|
+
* keystroke.
|
|
36
|
+
*/
|
|
37
|
+
export declare class CachedOutputBlock {
|
|
38
|
+
private cachedKey;
|
|
39
|
+
private cachedLines;
|
|
40
|
+
render(options: OutputBlockOptions): readonly string[];
|
|
41
|
+
invalidate(): void;
|
|
42
|
+
private buildKey;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=output-block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-block.d.ts","sourceRoot":"","sources":["../../src/components/output-block.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvF,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAeD,yEAAyE;AACzE,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,kBAAkB,CAAC,EAAE,MAAM,EAC3B,mBAAmB,CAAC,EAAE,MAAM,GAC3B,MAAM,CAIR;AAwCD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,EAAE,CA+CvE;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,WAAW,CAAgC;IAEnD,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,SAAS,MAAM,EAAE;IAStD,UAAU,IAAI,IAAI;IAKlB,OAAO,CAAC,QAAQ;CAmBjB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bordered output container with optional header and labelled sections.
|
|
3
|
+
*
|
|
4
|
+
* This is the unified framed-box primitive for tool cards and any future
|
|
5
|
+
* rich result blocks. It replaces the ad-hoc Card + Text children hacks that
|
|
6
|
+
* made each tool renderer re-implement its own border/background rules.
|
|
7
|
+
*/
|
|
8
|
+
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
9
|
+
/** Default border colors, keyed by state (omp-ish semantics). */
|
|
10
|
+
const STATE_BORDER = {
|
|
11
|
+
pending: "\x1b[38;5;33m",
|
|
12
|
+
running: "\x1b[38;5;33m",
|
|
13
|
+
success: "\x1b[38;5;8m",
|
|
14
|
+
error: "\x1b[38;5;31m",
|
|
15
|
+
warning: "\x1b[38;5;11m",
|
|
16
|
+
};
|
|
17
|
+
function normalizePadding(value) {
|
|
18
|
+
return value === undefined ? 1 : Math.max(0, Math.trunc(value));
|
|
19
|
+
}
|
|
20
|
+
/** Inner content width for a given outer width and symmetric padding. */
|
|
21
|
+
export function outputBlockContentWidth(width, contentPaddingLeft, contentPaddingRight) {
|
|
22
|
+
const left = normalizePadding(contentPaddingLeft);
|
|
23
|
+
const right = normalizePadding(contentPaddingRight ?? left);
|
|
24
|
+
return Math.max(1, width - 2 - left - right);
|
|
25
|
+
}
|
|
26
|
+
/** Build a titled header bar: ┌─ title ─────────┐ */
|
|
27
|
+
function renderHeaderBar(leftChar, rightChar, label, meta, width, border) {
|
|
28
|
+
const left = leftChar + "─".repeat(3);
|
|
29
|
+
const right = rightChar;
|
|
30
|
+
const labelText = label === undefined ? "" : ` ${label}${meta ? ` · ${meta}` : ""} `;
|
|
31
|
+
const labelWidth = visibleWidth(labelText);
|
|
32
|
+
const fill = Math.max(0, width - visibleWidth(left) - labelWidth - visibleWidth(right));
|
|
33
|
+
return `${border(left)}${labelText}${border("─".repeat(fill))}${border(right)}`;
|
|
34
|
+
}
|
|
35
|
+
/** Build a plain divider/bar: ├─────────────────┤ */
|
|
36
|
+
function renderBar(leftChar, rightChar, width, border) {
|
|
37
|
+
const fill = Math.max(0, width - 2);
|
|
38
|
+
return `${border(leftChar)}${border("─".repeat(fill))}${border(rightChar)}`;
|
|
39
|
+
}
|
|
40
|
+
/** Build the bottom bar: └─────────────────┘ */
|
|
41
|
+
function renderBottomBar(leftChar, rightChar, width, border) {
|
|
42
|
+
return renderBar(leftChar, rightChar, width, border);
|
|
43
|
+
}
|
|
44
|
+
export function renderOutputBlock(options) {
|
|
45
|
+
const { header, headerMeta, state, sections, width } = options;
|
|
46
|
+
const borderColor = options.borderColor ?? STATE_BORDER[state];
|
|
47
|
+
const border = (s) => `${borderColor}${s}\x1b[0m`;
|
|
48
|
+
const bgFn = options.bg;
|
|
49
|
+
const applyBg = options.applyBg !== false && bgFn !== undefined;
|
|
50
|
+
const stabilize = (line) => {
|
|
51
|
+
if (!applyBg || bgFn === undefined)
|
|
52
|
+
return line;
|
|
53
|
+
return applyBackgroundToLine(line, width, bgFn);
|
|
54
|
+
};
|
|
55
|
+
const contentLeft = normalizePadding(options.contentPaddingLeft);
|
|
56
|
+
const contentRight = normalizePadding(options.contentPaddingRight ?? contentLeft);
|
|
57
|
+
const contentWidth = outputBlockContentWidth(width, contentLeft, contentRight);
|
|
58
|
+
const lines = [];
|
|
59
|
+
const pushBar = (left, right, label, meta) => {
|
|
60
|
+
lines.push(stabilize(renderHeaderBar(left, right, label, meta, width, border)));
|
|
61
|
+
};
|
|
62
|
+
pushBar("┌", "┐", header, headerMeta);
|
|
63
|
+
const normalizedSections = sections.length > 0 ? sections : [{ lines: [] }];
|
|
64
|
+
for (let i = 0; i < normalizedSections.length; i++) {
|
|
65
|
+
const section = normalizedSections[i];
|
|
66
|
+
if (section.label !== undefined) {
|
|
67
|
+
pushBar("├", "┤", section.label);
|
|
68
|
+
}
|
|
69
|
+
else if (section.separator && i > 0) {
|
|
70
|
+
lines.push(stabilize(renderBar("├", "┤", width, border)));
|
|
71
|
+
}
|
|
72
|
+
for (const rawLine of section.lines) {
|
|
73
|
+
for (const line of rawLine.split("\n")) {
|
|
74
|
+
const trimmed = line.trimEnd();
|
|
75
|
+
const wrapped = wrapTextWithAnsi(trimmed, contentWidth);
|
|
76
|
+
for (const w of wrapped) {
|
|
77
|
+
const pad = " ".repeat(Math.max(0, contentWidth - visibleWidth(w)));
|
|
78
|
+
const inner = `${" ".repeat(contentLeft)}${w}${" ".repeat(contentRight)}${pad}`;
|
|
79
|
+
const row = `${border("│")}${inner}${border("│")}`;
|
|
80
|
+
lines.push(stabilize(row));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push(stabilize(renderBottomBar("└", "┘", width, border)));
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cached wrapper around renderOutputBlock.
|
|
90
|
+
*
|
|
91
|
+
* Tool blocks are re-rendered every frame while the transcript is live. The
|
|
92
|
+
* key hashes every field that materially changes the produced lines, so a
|
|
93
|
+
* stable block avoids re-running visibleWidth / wrapTextWithAnsi on every
|
|
94
|
+
* keystroke.
|
|
95
|
+
*/
|
|
96
|
+
export class CachedOutputBlock {
|
|
97
|
+
cachedKey;
|
|
98
|
+
cachedLines;
|
|
99
|
+
render(options) {
|
|
100
|
+
const key = this.buildKey(options);
|
|
101
|
+
if (this.cachedKey === key)
|
|
102
|
+
return this.cachedLines ?? [];
|
|
103
|
+
const lines = renderOutputBlock(options);
|
|
104
|
+
this.cachedKey = key;
|
|
105
|
+
this.cachedLines = lines;
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
invalidate() {
|
|
109
|
+
this.cachedKey = undefined;
|
|
110
|
+
this.cachedLines = undefined;
|
|
111
|
+
}
|
|
112
|
+
buildKey(options) {
|
|
113
|
+
const parts = [
|
|
114
|
+
options.width,
|
|
115
|
+
options.state,
|
|
116
|
+
options.borderColor,
|
|
117
|
+
options.header,
|
|
118
|
+
options.headerMeta,
|
|
119
|
+
options.contentPaddingLeft,
|
|
120
|
+
options.contentPaddingRight,
|
|
121
|
+
options.applyBg,
|
|
122
|
+
// bg is a function reference; invalidate() forces rebuild on change.
|
|
123
|
+
options.bg === undefined ? "no-bg" : "bg",
|
|
124
|
+
];
|
|
125
|
+
for (const section of options.sections) {
|
|
126
|
+
parts.push(section.label, section.separator === true ? 1 : 0);
|
|
127
|
+
parts.push(...section.lines);
|
|
128
|
+
}
|
|
129
|
+
return JSON.stringify(parts);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Component } from "../tui.ts";
|
|
2
|
+
export interface SelectItem {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SelectListTheme {
|
|
8
|
+
selectedPrefix: (text: string) => string;
|
|
9
|
+
selectedText: (text: string) => string;
|
|
10
|
+
description: (text: string) => string;
|
|
11
|
+
scrollInfo: (text: string) => string;
|
|
12
|
+
noMatch: (text: string) => string;
|
|
13
|
+
}
|
|
14
|
+
export interface SelectListTruncatePrimaryContext {
|
|
15
|
+
text: string;
|
|
16
|
+
maxWidth: number;
|
|
17
|
+
columnWidth: number;
|
|
18
|
+
item: SelectItem;
|
|
19
|
+
isSelected: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface SelectListLayoutOptions {
|
|
22
|
+
minPrimaryColumnWidth?: number;
|
|
23
|
+
maxPrimaryColumnWidth?: number;
|
|
24
|
+
truncatePrimary?: (context: SelectListTruncatePrimaryContext) => string;
|
|
25
|
+
}
|
|
26
|
+
export declare class SelectList implements Component {
|
|
27
|
+
private items;
|
|
28
|
+
private filteredItems;
|
|
29
|
+
private selectedIndex;
|
|
30
|
+
private maxVisible;
|
|
31
|
+
private theme;
|
|
32
|
+
private layout;
|
|
33
|
+
onSelect?: (item: SelectItem) => void;
|
|
34
|
+
onCancel?: () => void;
|
|
35
|
+
onSelectionChange?: (item: SelectItem) => void;
|
|
36
|
+
constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme, layout?: SelectListLayoutOptions);
|
|
37
|
+
setFilter(filter: string): void;
|
|
38
|
+
setSelectedIndex(index: number): void;
|
|
39
|
+
invalidate(): void;
|
|
40
|
+
render(width: number): string[];
|
|
41
|
+
handleInput(keyData: string): void;
|
|
42
|
+
private renderItem;
|
|
43
|
+
private getPrimaryColumnWidth;
|
|
44
|
+
private getPrimaryColumnBounds;
|
|
45
|
+
private truncatePrimary;
|
|
46
|
+
private getDisplayValue;
|
|
47
|
+
private notifySelectionChange;
|
|
48
|
+
getSelectedItem(): SelectItem | null;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=select-list.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { getKeybindings } from "../keybindings.js";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "../utils.js";
|
|
3
|
+
const DEFAULT_PRIMARY_COLUMN_WIDTH = 32;
|
|
4
|
+
const PRIMARY_COLUMN_GAP = 2;
|
|
5
|
+
const MIN_DESCRIPTION_WIDTH = 10;
|
|
6
|
+
const normalizeToSingleLine = (text) => text.replace(/[\r\n]+/g, " ").trim();
|
|
7
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
8
|
+
export class SelectList {
|
|
9
|
+
items = [];
|
|
10
|
+
filteredItems = [];
|
|
11
|
+
selectedIndex = 0;
|
|
12
|
+
maxVisible = 5;
|
|
13
|
+
theme;
|
|
14
|
+
layout;
|
|
15
|
+
onSelect;
|
|
16
|
+
onCancel;
|
|
17
|
+
onSelectionChange;
|
|
18
|
+
constructor(items, maxVisible, theme, layout = {}) {
|
|
19
|
+
this.items = items;
|
|
20
|
+
this.filteredItems = items;
|
|
21
|
+
this.maxVisible = maxVisible;
|
|
22
|
+
this.theme = theme;
|
|
23
|
+
this.layout = layout;
|
|
24
|
+
}
|
|
25
|
+
setFilter(filter) {
|
|
26
|
+
this.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));
|
|
27
|
+
// Reset selection when filter changes
|
|
28
|
+
this.selectedIndex = 0;
|
|
29
|
+
}
|
|
30
|
+
setSelectedIndex(index) {
|
|
31
|
+
this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
|
|
32
|
+
}
|
|
33
|
+
invalidate() {
|
|
34
|
+
// No cached state to invalidate currently
|
|
35
|
+
}
|
|
36
|
+
render(width) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
// If no items match filter, show message
|
|
39
|
+
if (this.filteredItems.length === 0) {
|
|
40
|
+
lines.push(this.theme.noMatch(" No matching commands"));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
const primaryColumnWidth = this.getPrimaryColumnWidth();
|
|
44
|
+
// Calculate visible range with scrolling
|
|
45
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
|
|
46
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
|
|
47
|
+
// Render visible items
|
|
48
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
49
|
+
const item = this.filteredItems[i];
|
|
50
|
+
if (!item)
|
|
51
|
+
continue;
|
|
52
|
+
const isSelected = i === this.selectedIndex;
|
|
53
|
+
const descriptionSingleLine = item.description
|
|
54
|
+
? normalizeToSingleLine(item.description)
|
|
55
|
+
: undefined;
|
|
56
|
+
lines.push(this.renderItem(item, isSelected, width, descriptionSingleLine, primaryColumnWidth));
|
|
57
|
+
}
|
|
58
|
+
// Add scroll indicators if needed
|
|
59
|
+
if (startIndex > 0 || endIndex < this.filteredItems.length) {
|
|
60
|
+
const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;
|
|
61
|
+
// Truncate if too long for terminal
|
|
62
|
+
lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")));
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
handleInput(keyData) {
|
|
67
|
+
const kb = getKeybindings();
|
|
68
|
+
// Up arrow - wrap to bottom when at top
|
|
69
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
70
|
+
this.selectedIndex =
|
|
71
|
+
this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
|
|
72
|
+
this.notifySelectionChange();
|
|
73
|
+
}
|
|
74
|
+
// Down arrow - wrap to top when at bottom
|
|
75
|
+
else if (kb.matches(keyData, "tui.select.down")) {
|
|
76
|
+
this.selectedIndex =
|
|
77
|
+
this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
|
|
78
|
+
this.notifySelectionChange();
|
|
79
|
+
}
|
|
80
|
+
// Enter
|
|
81
|
+
else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
82
|
+
const selectedItem = this.filteredItems[this.selectedIndex];
|
|
83
|
+
if (selectedItem && this.onSelect) {
|
|
84
|
+
this.onSelect(selectedItem);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Escape or Ctrl+C
|
|
88
|
+
else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
89
|
+
if (this.onCancel) {
|
|
90
|
+
this.onCancel();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
renderItem(item, isSelected, width, descriptionSingleLine, primaryColumnWidth) {
|
|
95
|
+
const prefix = isSelected ? "→ " : " ";
|
|
96
|
+
const prefixWidth = visibleWidth(prefix);
|
|
97
|
+
if (descriptionSingleLine && width > 40) {
|
|
98
|
+
const effectivePrimaryColumnWidth = Math.max(1, Math.min(primaryColumnWidth, width - prefixWidth - 4));
|
|
99
|
+
const maxPrimaryWidth = Math.max(1, effectivePrimaryColumnWidth - PRIMARY_COLUMN_GAP);
|
|
100
|
+
const truncatedValue = this.truncatePrimary(item, isSelected, maxPrimaryWidth, effectivePrimaryColumnWidth);
|
|
101
|
+
const truncatedValueWidth = visibleWidth(truncatedValue);
|
|
102
|
+
const spacing = " ".repeat(Math.max(1, effectivePrimaryColumnWidth - truncatedValueWidth));
|
|
103
|
+
const descriptionStart = prefixWidth + truncatedValueWidth + spacing.length;
|
|
104
|
+
const remainingWidth = width - descriptionStart - 2; // -2 for safety
|
|
105
|
+
if (remainingWidth > MIN_DESCRIPTION_WIDTH) {
|
|
106
|
+
const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, "");
|
|
107
|
+
if (isSelected) {
|
|
108
|
+
return this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
|
|
109
|
+
}
|
|
110
|
+
const descText = this.theme.description(spacing + truncatedDesc);
|
|
111
|
+
return prefix + truncatedValue + descText;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const maxWidth = width - prefixWidth - 2;
|
|
115
|
+
const truncatedValue = this.truncatePrimary(item, isSelected, maxWidth, maxWidth);
|
|
116
|
+
if (isSelected) {
|
|
117
|
+
return this.theme.selectedText(`${prefix}${truncatedValue}`);
|
|
118
|
+
}
|
|
119
|
+
return prefix + truncatedValue;
|
|
120
|
+
}
|
|
121
|
+
getPrimaryColumnWidth() {
|
|
122
|
+
const { min, max } = this.getPrimaryColumnBounds();
|
|
123
|
+
const widestPrimary = this.filteredItems.reduce((widest, item) => {
|
|
124
|
+
return Math.max(widest, visibleWidth(this.getDisplayValue(item)) + PRIMARY_COLUMN_GAP);
|
|
125
|
+
}, 0);
|
|
126
|
+
return clamp(widestPrimary, min, max);
|
|
127
|
+
}
|
|
128
|
+
getPrimaryColumnBounds() {
|
|
129
|
+
const rawMin = this.layout.minPrimaryColumnWidth ??
|
|
130
|
+
this.layout.maxPrimaryColumnWidth ??
|
|
131
|
+
DEFAULT_PRIMARY_COLUMN_WIDTH;
|
|
132
|
+
const rawMax = this.layout.maxPrimaryColumnWidth ??
|
|
133
|
+
this.layout.minPrimaryColumnWidth ??
|
|
134
|
+
DEFAULT_PRIMARY_COLUMN_WIDTH;
|
|
135
|
+
return {
|
|
136
|
+
min: Math.max(1, Math.min(rawMin, rawMax)),
|
|
137
|
+
max: Math.max(1, Math.max(rawMin, rawMax)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
truncatePrimary(item, isSelected, maxWidth, columnWidth) {
|
|
141
|
+
const displayValue = this.getDisplayValue(item);
|
|
142
|
+
const truncatedValue = this.layout.truncatePrimary
|
|
143
|
+
? this.layout.truncatePrimary({
|
|
144
|
+
text: displayValue,
|
|
145
|
+
maxWidth,
|
|
146
|
+
columnWidth,
|
|
147
|
+
item,
|
|
148
|
+
isSelected,
|
|
149
|
+
})
|
|
150
|
+
: truncateToWidth(displayValue, maxWidth, "");
|
|
151
|
+
return truncateToWidth(truncatedValue, maxWidth, "");
|
|
152
|
+
}
|
|
153
|
+
getDisplayValue(item) {
|
|
154
|
+
return item.label || item.value;
|
|
155
|
+
}
|
|
156
|
+
notifySelectionChange() {
|
|
157
|
+
const selectedItem = this.filteredItems[this.selectedIndex];
|
|
158
|
+
if (selectedItem && this.onSelectionChange) {
|
|
159
|
+
this.onSelectionChange(selectedItem);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
getSelectedItem() {
|
|
163
|
+
const item = this.filteredItems[this.selectedIndex];
|
|
164
|
+
return item || null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Component } from "../tui.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Spacer component that renders empty lines
|
|
4
|
+
*/
|
|
5
|
+
export declare class Spacer implements Component {
|
|
6
|
+
private lines;
|
|
7
|
+
constructor(lines?: number);
|
|
8
|
+
setLines(lines: number): void;
|
|
9
|
+
invalidate(): void;
|
|
10
|
+
render(_width: number): string[];
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=spacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spacer.d.ts","sourceRoot":"","sources":["../../src/components/spacer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;GAEG;AACH,qBAAa,MAAO,YAAW,SAAS;IACtC,OAAO,CAAC,KAAK,CAAS;gBAEV,KAAK,GAAE,MAAU;IAI7B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI7B,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;CAOjC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacer component that renders empty lines
|
|
3
|
+
*/
|
|
4
|
+
export class Spacer {
|
|
5
|
+
lines;
|
|
6
|
+
constructor(lines = 1) {
|
|
7
|
+
this.lines = lines;
|
|
8
|
+
}
|
|
9
|
+
setLines(lines) {
|
|
10
|
+
this.lines = lines;
|
|
11
|
+
}
|
|
12
|
+
invalidate() {
|
|
13
|
+
// No cached state to invalidate currently
|
|
14
|
+
}
|
|
15
|
+
render(_width) {
|
|
16
|
+
const result = [];
|
|
17
|
+
for (let i = 0; i < this.lines; i++) {
|
|
18
|
+
result.push("");
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal omp-style status bar renderer (powerline chip + semantic colors).
|
|
3
|
+
* Pure ANSI functions, no component state. Keeping this in packages/tui lets
|
|
4
|
+
* both the bottom status line and (optionally) the header reuse it.
|
|
5
|
+
*/
|
|
6
|
+
export interface StatusSegment {
|
|
7
|
+
text: string;
|
|
8
|
+
/** Render as a filled powerline chip (bg + ◀ ▶) instead of plain text. */
|
|
9
|
+
chip?: boolean;
|
|
10
|
+
/** Foreground ANSI for plain segments (default reset). */
|
|
11
|
+
fg?: string;
|
|
12
|
+
/** Background ANSI for chip segments (e.g. `\x1b[48;5;25m`). */
|
|
13
|
+
bg?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One powerline chip: ◀bg label bg▶ using the same color for arrows/fill,
|
|
16
|
+
* but white bold text inside so the label is readable on the tint. */
|
|
17
|
+
export declare function chip(text: string, bgAnsi: string): string;
|
|
18
|
+
/** Join a segment array with the thin smoke separator; chips get their own fill. */
|
|
19
|
+
export declare function renderStatusBar(segs: readonly StatusSegment[]): string;
|
|
20
|
+
/** Compact tool-card header: icon title · description · meta */
|
|
21
|
+
export declare function renderToolHeader(opts: {
|
|
22
|
+
icon?: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
meta?: string[];
|
|
26
|
+
spinnerFrame?: number;
|
|
27
|
+
titleColor?: string;
|
|
28
|
+
dimColor?: string;
|
|
29
|
+
}): string;
|
|
30
|
+
//# sourceMappingURL=statusline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../../src/components/statusline.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;uEACuE;AACvE,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAatE;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAaT"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal omp-style status bar renderer (powerline chip + semantic colors).
|
|
3
|
+
* Pure ANSI functions, no component state. Keeping this in packages/tui lets
|
|
4
|
+
* both the bottom status line and (optionally) the header reuse it.
|
|
5
|
+
*/
|
|
6
|
+
/** One powerline chip: ◀bg label bg▶ using the same color for arrows/fill,
|
|
7
|
+
* but white bold text inside so the label is readable on the tint. */
|
|
8
|
+
export function chip(text, bgAnsi) {
|
|
9
|
+
const fg = bgAnsi.replace("\x1b[48;", "\x1b[38;");
|
|
10
|
+
return `${fg}◀${bgAnsi}\x1b[1m \x1b[37m${text} \x1b[22m\x1b[0m${fg}▶`;
|
|
11
|
+
}
|
|
12
|
+
/** Join a segment array with the thin smoke separator; chips get their own fill. */
|
|
13
|
+
export function renderStatusBar(segs) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (let i = 0; i < segs.length; i++) {
|
|
16
|
+
const s = segs[i];
|
|
17
|
+
if (!s.text)
|
|
18
|
+
continue;
|
|
19
|
+
if (i > 0)
|
|
20
|
+
out.push(`\x1b[38;5;8m ┆ \x1b[0m`);
|
|
21
|
+
if (s.chip && s.bg) {
|
|
22
|
+
out.push(chip(s.text, s.bg));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
out.push(`${s.fg ?? "\x1b[0m"}${s.text}\x1b[0m`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out.join("");
|
|
29
|
+
}
|
|
30
|
+
/** Compact tool-card header: icon title · description · meta */
|
|
31
|
+
export function renderToolHeader(opts) {
|
|
32
|
+
const icon = opts.icon ? `${opts.icon} ` : "";
|
|
33
|
+
const titleColor = opts.titleColor ?? "\u001b[1m";
|
|
34
|
+
const title = `${titleColor}${icon}${opts.title}\u001b[22m`;
|
|
35
|
+
const meta = opts.meta?.filter(Boolean) ?? [];
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (opts.description)
|
|
38
|
+
parts.push(opts.description);
|
|
39
|
+
if (meta.length > 0)
|
|
40
|
+
parts.push(meta.join(" · "));
|
|
41
|
+
if (parts.length > 0) {
|
|
42
|
+
const dim = opts.dimColor ?? "\u001b[2m";
|
|
43
|
+
return `${title} ${dim}${parts.join(" · ")}\u001b[0m`;
|
|
44
|
+
}
|
|
45
|
+
return title;
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Component } from "../tui.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Text component - displays multi-line text with word wrapping
|
|
4
|
+
*/
|
|
5
|
+
export declare class Text implements Component {
|
|
6
|
+
private text;
|
|
7
|
+
private paddingX;
|
|
8
|
+
private paddingY;
|
|
9
|
+
private customBgFn?;
|
|
10
|
+
private cachedText?;
|
|
11
|
+
private cachedWidth?;
|
|
12
|
+
private cachedLines?;
|
|
13
|
+
constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
|
|
14
|
+
setText(text: string): void;
|
|
15
|
+
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
render(width: number): string[];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/components/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C;;GAEG;AACH,qBAAa,IAAK,YAAW,SAAS;IACpC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAC,CAA2B;IAG9C,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAW;gBAG7B,IAAI,GAAE,MAAW,EACjB,QAAQ,GAAE,MAAU,EACpB,QAAQ,GAAE,MAAU,EACpB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;IAQvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO3B,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI;IAO1D,UAAU,IAAI,IAAI;IAMlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CA+DhC"}
|