pi-libtui 0.3.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/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +55 -0
- package/src/activity-presentation.ts +75 -0
- package/src/appearance.ts +632 -0
- package/src/background-surface.ts +125 -0
- package/src/color/palette.ts +186 -0
- package/src/color/preview.ts +157 -0
- package/src/color/resolver.ts +75 -0
- package/src/color/theme.ts +531 -0
- package/src/component-stack.ts +247 -0
- package/src/content/markdown-code.ts +60 -0
- package/src/content/terminal-text.ts +135 -0
- package/src/content/text.ts +105 -0
- package/src/controls/action-panel.ts +420 -0
- package/src/controls/dialog-button-bar.ts +287 -0
- package/src/controls/multi-select.ts +467 -0
- package/src/controls/picker-panel.ts +447 -0
- package/src/controls/screen-icon-actions.ts +134 -0
- package/src/controls/searchable-select.ts +394 -0
- package/src/controls/select-box.ts +224 -0
- package/src/controls/selectable-list.ts +433 -0
- package/src/controls/selection-action-bar.ts +382 -0
- package/src/controls/semantic-input.ts +23 -0
- package/src/controls/tab.ts +189 -0
- package/src/cursor.ts +288 -0
- package/src/decoration/editor-pills.ts +156 -0
- package/src/decoration/glyphs.ts +326 -0
- package/src/decoration/pointer-interaction.ts +123 -0
- package/src/decoration/powerline-pill.ts +104 -0
- package/src/decoration/status.ts +185 -0
- package/src/decoration/transient-pill.ts +176 -0
- package/src/diff/index.ts +21 -0
- package/src/diff/model.ts +76 -0
- package/src/diff/parse.ts +310 -0
- package/src/diff/pierre-syntax.ts +86 -0
- package/src/diff/render.ts +504 -0
- package/src/diff/structured.ts +264 -0
- package/src/diff/view.ts +75 -0
- package/src/editor/chrome.ts +341 -0
- package/src/editor/composition.ts +53 -0
- package/src/editor/layout.ts +81 -0
- package/src/editor/presentation.ts +35 -0
- package/src/editor/protocol.ts +188 -0
- package/src/editor.ts +41 -0
- package/src/extension.ts +30 -0
- package/src/folding.ts +192 -0
- package/src/host/cursor-bridge.ts +210 -0
- package/src/host/editor-bridge.ts +148 -0
- package/src/host/extension-host.ts +231 -0
- package/src/host/mouse-bridge.ts +212 -0
- package/src/host/mouse-dispatch.ts +400 -0
- package/src/host/mouse-input.ts +85 -0
- package/src/host/mouse-targets.ts +238 -0
- package/src/host/pi-layout-adapter.ts +298 -0
- package/src/host/pi-native-selection.ts +227 -0
- package/src/host/split-pane-bridge.ts +720 -0
- package/src/host/transcript-bridge.ts +236 -0
- package/src/index.ts +334 -0
- package/src/line-layout.ts +6 -0
- package/src/motion.ts +1409 -0
- package/src/mouse/events.ts +55 -0
- package/src/mouse/fullscreen-layout.ts +260 -0
- package/src/mouse/registry.ts +348 -0
- package/src/mouse.ts +37 -0
- package/src/native-binary.ts +189 -0
- package/src/overlay/anchored.ts +91 -0
- package/src/overlay/detail-card.ts +158 -0
- package/src/overlay/dialog.ts +266 -0
- package/src/overlay/floating.ts +106 -0
- package/src/overlay/framed-editor.ts +236 -0
- package/src/overlay/fullscreen.ts +170 -0
- package/src/overlay/hover-tooltip.ts +109 -0
- package/src/overlay/modal-mount.ts +118 -0
- package/src/panels.ts +207 -0
- package/src/render-cache.ts +80 -0
- package/src/render-epoch.ts +48 -0
- package/src/request-animation.ts +160 -0
- package/src/scrollbar.ts +27 -0
- package/src/selection.ts +148 -0
- package/src/split-pane.ts +232 -0
- package/src/status-presentation.ts +411 -0
- package/src/stream.ts +543 -0
- package/src/syntax.ts +316 -0
- package/src/terminal/bridge-client.ts +243 -0
- package/src/terminal/embedding.ts +6 -0
- package/src/terminal/output.ts +151 -0
- package/src/terminal/projection.ts +362 -0
- package/src/terminal/pty-host.ts +263 -0
- package/src/terminal/pty-pane.ts +308 -0
- package/src/terminal-colors.ts +334 -0
- package/src/tool/action.ts +170 -0
- package/src/tool/activity.ts +500 -0
- package/src/tool/call-preview.ts +44 -0
- package/src/tool/disclosure-action.ts +89 -0
- package/src/tool/expanded-region-viewport.ts +103 -0
- package/src/tool/index.ts +26 -0
- package/src/tool/output.ts +271 -0
- package/src/tool/transcript.ts +150 -0
- package/src/tool/view-region.ts +581 -0
- package/test/appearance.test.ts +183 -0
- package/test/bounded-stream.test.ts +150 -0
- package/test/bridge-client.test.ts +42 -0
- package/test/color-preview.test.ts +23 -0
- package/test/component-stack.test.ts +206 -0
- package/test/components.test.ts +1165 -0
- package/test/content-text.test.ts +21 -0
- package/test/cursor-bridge.test.ts +151 -0
- package/test/cursor.test.ts +147 -0
- package/test/editor-chrome.test.ts +119 -0
- package/test/editor-layout.test.ts +30 -0
- package/test/editor-registry.test.ts +203 -0
- package/test/editor-theme.test.ts +33 -0
- package/test/editor-token-pill.test.ts +72 -0
- package/test/floating-overlay.test.ts +57 -0
- package/test/folding.test.ts +147 -0
- package/test/harmonious-theme.test.ts +23 -0
- package/test/interaction.test.ts +104 -0
- package/test/key-icons.test.ts +62 -0
- package/test/markdown-code.test.ts +24 -0
- package/test/modal-overlay-mount.test.ts +94 -0
- package/test/motion.test.ts +649 -0
- package/test/mouse/bridge.test.ts +1302 -0
- package/test/mouse/extension.test.ts +205 -0
- package/test/mouse/registry.test.ts +140 -0
- package/test/mouse/screen-decoration.test.ts +167 -0
- package/test/native-binary.test.ts +52 -0
- package/test/palette.test.ts +32 -0
- package/test/panels.test.ts +74 -0
- package/test/pointer-components.test.ts +151 -0
- package/test/progress.test.ts +54 -0
- package/test/pty-host.test.ts +126 -0
- package/test/pty-pane.test.ts +62 -0
- package/test/public-surface.test.ts +75 -0
- package/test/render-cache.test.ts +25 -0
- package/test/render-epoch.test.ts +119 -0
- package/test/request-animation.test.ts +193 -0
- package/test/screen-icon-actions.test.ts +69 -0
- package/test/select-box.test.ts +86 -0
- package/test/select-pointer.test.ts +369 -0
- package/test/selectable-list.test.ts +258 -0
- package/test/selection/registry.test.ts +44 -0
- package/test/selection-action-bar.test.ts +204 -0
- package/test/split-pane-bridge.test.ts +944 -0
- package/test/split-pane.test.ts +134 -0
- package/test/status-presentation.test.ts +39 -0
- package/test/syntax.test.ts +59 -0
- package/test/terminal-colors.test.ts +115 -0
- package/test/terminal-embedding.test.ts +12 -0
- package/test/terminal-projection.test.ts +312 -0
- package/test/theme-colors.test.ts +175 -0
- package/test/tool-output.test.ts +160 -0
- package/test/tool-transcript.test.ts +1190 -0
- package/test/unified-diff.test.ts +691 -0
- package/themes/harmonious.json +61 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { configureTuiAppearance, DEFAULT_TUI_APPEARANCE } from "../src/appearance.ts";
|
|
4
|
+
import type { MotionClock, MotionTimerHandle } from "../src/motion.ts";
|
|
5
|
+
import { MotionScheduler } from "../src/motion.ts";
|
|
6
|
+
import { registerRequestAnimation, RequestAnimationController } from "../src/request-animation.ts";
|
|
7
|
+
|
|
8
|
+
const theme = {
|
|
9
|
+
name: "request-animation-test",
|
|
10
|
+
getColorMode: () => "truecolor",
|
|
11
|
+
getFgAnsi: (token: string) => (token === "accent" ? "\x1b[38;2;80;160;240m" : "\x1b[38;2;180;180;180m"),
|
|
12
|
+
getBgAnsi: () => "\x1b[48;2;20;20;20m",
|
|
13
|
+
} as never as Theme;
|
|
14
|
+
|
|
15
|
+
interface FakeTimer extends MotionTimerHandle {
|
|
16
|
+
callback: () => void;
|
|
17
|
+
cadenceMs: number;
|
|
18
|
+
stopped: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class FakeClock implements MotionClock {
|
|
22
|
+
currentMs = 0;
|
|
23
|
+
readonly timers: FakeTimer[] = [];
|
|
24
|
+
|
|
25
|
+
now(): number {
|
|
26
|
+
return this.currentMs;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
start(callback: () => void, cadenceMs: number): FakeTimer {
|
|
30
|
+
const timer = { callback, cadenceMs, stopped: false };
|
|
31
|
+
this.timers.push(timer);
|
|
32
|
+
return timer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
stop(handle: MotionTimerHandle): void {
|
|
36
|
+
(handle as FakeTimer).stopped = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class FakeUi {
|
|
41
|
+
readonly theme = theme;
|
|
42
|
+
readonly indicators: Array<{ frames?: string[] } | undefined> = [];
|
|
43
|
+
readonly messages: Array<string | undefined> = [];
|
|
44
|
+
|
|
45
|
+
setWorkingIndicator(indicator?: { frames?: string[] }): void {
|
|
46
|
+
this.indicators.push(indicator);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setWorkingMessage(message?: string): void {
|
|
50
|
+
this.messages.push(message);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get text(): string {
|
|
54
|
+
return Bun.stripANSI(this.messages.at(-1) ?? "");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeEach(() => configureTuiAppearance(DEFAULT_TUI_APPEARANCE));
|
|
59
|
+
afterEach(() => configureTuiAppearance(DEFAULT_TUI_APPEARANCE));
|
|
60
|
+
|
|
61
|
+
describe("request animation", () => {
|
|
62
|
+
test("retains live appearance updates across session switches", async () => {
|
|
63
|
+
type LifecycleHandler = (event: { reason?: string }, context: { ui: FakeUi }) => void | Promise<void>;
|
|
64
|
+
// type-boundary: The registration harness implements only ExtensionAPI.on for lifecycle behavior.
|
|
65
|
+
type ExtensionApiBoundary = unknown;
|
|
66
|
+
const handlers = new Map<string, LifecycleHandler>();
|
|
67
|
+
const boundary: ExtensionApiBoundary = {
|
|
68
|
+
on(name: string, handler: LifecycleHandler) {
|
|
69
|
+
handlers.set(name, handler);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
registerRequestAnimation(boundary as ExtensionAPI);
|
|
73
|
+
const first = new FakeUi();
|
|
74
|
+
await handlers.get("agent_start")?.({}, { ui: first });
|
|
75
|
+
await handlers.get("session_shutdown")?.({ reason: "switch" }, { ui: first });
|
|
76
|
+
|
|
77
|
+
const second = new FakeUi();
|
|
78
|
+
await handlers.get("agent_start")?.({}, { ui: second });
|
|
79
|
+
configureTuiAppearance({ activityIndicator: "static", textEffect: "off" });
|
|
80
|
+
|
|
81
|
+
expect(second.text).toBe("● Working...");
|
|
82
|
+
await handlers.get("session_shutdown")?.({ reason: "reload" }, { ui: second });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("tracks thinking, parallel tools, and working with the documented priority", () => {
|
|
86
|
+
const clock = new FakeClock();
|
|
87
|
+
const ui = new FakeUi();
|
|
88
|
+
const controller = new RequestAnimationController({
|
|
89
|
+
now: () => clock.now(),
|
|
90
|
+
scheduler: new MotionScheduler(clock),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
controller.start({ ui });
|
|
94
|
+
expect(ui.indicators.at(-1)).toEqual({ frames: [] });
|
|
95
|
+
expect(ui.text).toContain("Working...");
|
|
96
|
+
|
|
97
|
+
controller.startTool("first");
|
|
98
|
+
controller.startTool("second");
|
|
99
|
+
expect(ui.text).toContain("Running...");
|
|
100
|
+
|
|
101
|
+
controller.setThinking(true);
|
|
102
|
+
expect(ui.text).toContain("Thinking...");
|
|
103
|
+
controller.finishTool("first");
|
|
104
|
+
expect(ui.text).toContain("Thinking...");
|
|
105
|
+
|
|
106
|
+
controller.setThinking(false);
|
|
107
|
+
expect(ui.text).toContain("Running...");
|
|
108
|
+
controller.finishTool("second");
|
|
109
|
+
expect(ui.text).toContain("Working...");
|
|
110
|
+
|
|
111
|
+
controller.finish({ ui });
|
|
112
|
+
expect(ui.messages.at(-1)).toBeUndefined();
|
|
113
|
+
expect(ui.indicators.at(-1)).toBeUndefined();
|
|
114
|
+
controller.dispose();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("applies phase indicator and text-effect overrides without changing the defaults", () => {
|
|
118
|
+
configureTuiAppearance({
|
|
119
|
+
activityIndicator: "spinner",
|
|
120
|
+
textEffect: "off",
|
|
121
|
+
thinkingIndicator: "off",
|
|
122
|
+
thinkingTextEffect: "off",
|
|
123
|
+
toolIndicator: "static",
|
|
124
|
+
});
|
|
125
|
+
const clock = new FakeClock();
|
|
126
|
+
const ui = new FakeUi();
|
|
127
|
+
const controller = new RequestAnimationController({
|
|
128
|
+
now: () => clock.now(),
|
|
129
|
+
scheduler: new MotionScheduler(clock),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
controller.start({ ui });
|
|
133
|
+
expect(ui.text).toBe("⠋ Working...");
|
|
134
|
+
controller.setThinking(true);
|
|
135
|
+
expect(ui.text).toBe("Thinking...");
|
|
136
|
+
controller.setThinking(false);
|
|
137
|
+
controller.startTool("tool");
|
|
138
|
+
expect(ui.text).toBe("● Running...");
|
|
139
|
+
|
|
140
|
+
controller.dispose({ ui });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("uses exclusive status scenes intentionally and resolves phase overrides", () => {
|
|
144
|
+
configureTuiAppearance({
|
|
145
|
+
statusPresentation: "block-wave",
|
|
146
|
+
thinkingPresentation: "pacman",
|
|
147
|
+
toolPresentation: "standard",
|
|
148
|
+
});
|
|
149
|
+
const clock = new FakeClock();
|
|
150
|
+
const ui = new FakeUi();
|
|
151
|
+
const controller = new RequestAnimationController({
|
|
152
|
+
now: () => clock.now(),
|
|
153
|
+
scheduler: new MotionScheduler(clock),
|
|
154
|
+
width: () => 24,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
controller.start({ ui });
|
|
158
|
+
expect(ui.text).toMatch(/[█▓▒]/u);
|
|
159
|
+
expect(ui.text).not.toContain("Working...");
|
|
160
|
+
|
|
161
|
+
controller.setThinking(true);
|
|
162
|
+
expect(ui.text).toMatch(/[ᗧC]/u);
|
|
163
|
+
expect(ui.text).not.toContain("Thinking...");
|
|
164
|
+
|
|
165
|
+
controller.setThinking(false);
|
|
166
|
+
controller.startTool("tool");
|
|
167
|
+
expect(ui.text).toContain("Running...");
|
|
168
|
+
|
|
169
|
+
controller.dispose({ ui });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("composes text effects and message sources with the configured indicator", () => {
|
|
173
|
+
configureTuiAppearance({ activityIndicator: "static", textEffect: "aurora" });
|
|
174
|
+
const clock = new FakeClock();
|
|
175
|
+
const ui = new FakeUi();
|
|
176
|
+
const controller = new RequestAnimationController({
|
|
177
|
+
now: () => clock.now(),
|
|
178
|
+
scheduler: new MotionScheduler(clock),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
controller.start({ ui });
|
|
182
|
+
expect(ui.text).toBe("● Working...");
|
|
183
|
+
clock.currentMs = 350;
|
|
184
|
+
clock.timers.at(-1)?.callback();
|
|
185
|
+
expect(new Set((ui.messages.at(-1) ?? "").match(/\x1b\[38;[^m]+m/gu) ?? []).size).toBeGreaterThan(1);
|
|
186
|
+
|
|
187
|
+
configureTuiAppearance({ textEffect: "off", activityMessage: "typewriter" });
|
|
188
|
+
expect(ui.text).toStartWith("● ");
|
|
189
|
+
expect(ui.text).not.toContain("Working...");
|
|
190
|
+
|
|
191
|
+
controller.dispose({ ui });
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { mountScreenIconActions } from "../src/controls/screen-icon-actions.ts";
|
|
4
|
+
import { getMouseRegistryState } from "../src/mouse/registry.ts";
|
|
5
|
+
import { ensureMouseRegistry } from "../src/mouse.ts";
|
|
6
|
+
|
|
7
|
+
const theme = {
|
|
8
|
+
name: "screen-actions-test",
|
|
9
|
+
bold: (text: string) => text,
|
|
10
|
+
getFgAnsi: () => "\x1b[38;2;120;160;220m",
|
|
11
|
+
getBgAnsi: () => "\x1b[48;2;20;24;30m",
|
|
12
|
+
} as never as Theme;
|
|
13
|
+
|
|
14
|
+
test("mounts icon-only top-right actions with pointer tooltips", () => {
|
|
15
|
+
const registry = ensureMouseRegistry(Object.create(null) as typeof globalThis);
|
|
16
|
+
const activated: string[] = [];
|
|
17
|
+
let visible = true;
|
|
18
|
+
const mount = mountScreenIconActions({
|
|
19
|
+
id: "test.actions",
|
|
20
|
+
theme,
|
|
21
|
+
registry,
|
|
22
|
+
actions: [
|
|
23
|
+
{ value: "expand", glyph: "", tooltip: "Expand side panel", shortcuts: ["alt+shift+z"], visible: () => visible },
|
|
24
|
+
{ value: "toggle", glyph: "", tooltip: "Hide side panel" },
|
|
25
|
+
],
|
|
26
|
+
onActivate: (value) => activated.push(value),
|
|
27
|
+
});
|
|
28
|
+
const blank = Array.from({ length: 6 }, () => " ".repeat(40));
|
|
29
|
+
const context = { width: 40, height: 6, hasOverlay: false } as const;
|
|
30
|
+
const rendered = Bun.stripANSI(registry.dispatchScreenDecorators(blank, context).join("\n"));
|
|
31
|
+
expect(rendered.split("\n")[0]).toContain("");
|
|
32
|
+
expect(rendered.split("\n")[0]).toContain("");
|
|
33
|
+
expect(rendered).not.toContain("Expand side panel");
|
|
34
|
+
expect(mount.reservedWidth).toBe(7);
|
|
35
|
+
|
|
36
|
+
const expand = getMouseRegistryState(registry).regions.find((region) => region.id === "test.actions.expand");
|
|
37
|
+
expect(expand?.getRect()?.y).toBe(0);
|
|
38
|
+
expand?.onMouse(mouse("move"));
|
|
39
|
+
const hovered = Bun.stripANSI(registry.dispatchScreenDecorators(blank, context).join("\n"));
|
|
40
|
+
expect(hovered).toContain("Expand side panel");
|
|
41
|
+
expect(hovered).toContain("⌥");
|
|
42
|
+
expect(hovered).toContain("⇧");
|
|
43
|
+
expect(hovered).not.toContain("╭");
|
|
44
|
+
expand?.onMouse(mouse("release", undefined));
|
|
45
|
+
expect(activated).toEqual(["expand"]);
|
|
46
|
+
expand?.onMouse(mouse("press"));
|
|
47
|
+
expand?.onMouse(mouse("release", undefined));
|
|
48
|
+
expect(activated).toEqual(["expand", "expand"]);
|
|
49
|
+
|
|
50
|
+
visible = false;
|
|
51
|
+
registry.dispatchScreenDecorators(blank, context);
|
|
52
|
+
expect(expand?.getRect()).toBeUndefined();
|
|
53
|
+
mount.dispose();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function mouse(type: "move" | "press" | "release", button: 0 | undefined = type === "move" ? undefined : 0) {
|
|
57
|
+
return {
|
|
58
|
+
type,
|
|
59
|
+
row: 0,
|
|
60
|
+
col: 0,
|
|
61
|
+
screenRow: 0,
|
|
62
|
+
screenCol: 0,
|
|
63
|
+
button,
|
|
64
|
+
wheel: undefined,
|
|
65
|
+
shift: false,
|
|
66
|
+
alt: false,
|
|
67
|
+
ctrl: false,
|
|
68
|
+
} as const;
|
|
69
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { initTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
CURSOR_MARKER,
|
|
5
|
+
KeybindingsManager,
|
|
6
|
+
setKeybindings,
|
|
7
|
+
stripTerminalSequences,
|
|
8
|
+
TUI_KEYBINDINGS,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { tuiTheme } from "../src/color/theme.ts";
|
|
11
|
+
import { SelectBox } from "../src/controls/select-box.ts";
|
|
12
|
+
|
|
13
|
+
describe("SelectBox", () => {
|
|
14
|
+
const theme = {
|
|
15
|
+
bold: (text: string) => text,
|
|
16
|
+
fg: (_color: string, text: string) => text,
|
|
17
|
+
bg: (_color: string, text: string) => text,
|
|
18
|
+
underline: (text: string) => text,
|
|
19
|
+
} as Theme;
|
|
20
|
+
|
|
21
|
+
test("selects immediately without modal action buttons", () => {
|
|
22
|
+
initTheme("dark", false);
|
|
23
|
+
setKeybindings(new KeybindingsManager(TUI_KEYBINDINGS));
|
|
24
|
+
const selected: string[] = [];
|
|
25
|
+
const previewed: string[] = [];
|
|
26
|
+
const box = new SelectBox({
|
|
27
|
+
theme,
|
|
28
|
+
bordered: false,
|
|
29
|
+
options: [
|
|
30
|
+
{ value: "one", label: "One" },
|
|
31
|
+
{ value: "two", label: "Two" },
|
|
32
|
+
],
|
|
33
|
+
selected: "one",
|
|
34
|
+
onSelect: (value) => selected.push(value),
|
|
35
|
+
onPreview: (value) => previewed.push(value),
|
|
36
|
+
onCancel() {},
|
|
37
|
+
});
|
|
38
|
+
box.focused = true;
|
|
39
|
+
|
|
40
|
+
const rendered = box.render(20).map(stripTerminalSequences).join("\n");
|
|
41
|
+
const initial = box.render(20).join("\n");
|
|
42
|
+
expect(initial).toContain(CURSOR_MARKER);
|
|
43
|
+
const colors = tuiTheme(theme);
|
|
44
|
+
const darkerSelected = colors.adjustForegroundBrightness(colors.color("surface.selected"), -0.08);
|
|
45
|
+
expect(initial).toContain(colors.bgAnsi(darkerSelected));
|
|
46
|
+
expect(initial).not.toContain(colors.bgAnsi("surface.selected"));
|
|
47
|
+
box.onMouse({ type: "move", row: 2, col: 3 } as never);
|
|
48
|
+
const darkerHover = colors.adjustForegroundBrightness(colors.color("surface.hover"), -0.08);
|
|
49
|
+
expect(box.render(20).join("\n")).toContain(colors.bgAnsi(darkerHover));
|
|
50
|
+
expect(rendered.split("\n")[0]).not.toContain("> ");
|
|
51
|
+
expect(rendered).not.toContain("╭");
|
|
52
|
+
expect(rendered).toContain("Two");
|
|
53
|
+
expect(rendered).not.toContain("Save");
|
|
54
|
+
expect(rendered).not.toContain("Cancel");
|
|
55
|
+
box.handleInput("T");
|
|
56
|
+
box.handleInput("w");
|
|
57
|
+
expect(previewed).toEqual(["two"]);
|
|
58
|
+
const filtered = box.render(20).join("\n");
|
|
59
|
+
expect(stripTerminalSequences(filtered)).not.toContain("One");
|
|
60
|
+
expect(filtered).toContain(colors.fgAnsi("highlight"));
|
|
61
|
+
box.handleInput("\r");
|
|
62
|
+
expect(selected).toEqual(["two"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("supports a full-pane preview surface", () => {
|
|
66
|
+
initTheme("dark", false);
|
|
67
|
+
setKeybindings(new KeybindingsManager(TUI_KEYBINDINGS));
|
|
68
|
+
const box = new SelectBox({
|
|
69
|
+
theme,
|
|
70
|
+
title: "Composition",
|
|
71
|
+
bordered: false,
|
|
72
|
+
showHint: true,
|
|
73
|
+
options: [{ value: "compact", label: "Compact" }],
|
|
74
|
+
onSelect() {},
|
|
75
|
+
onCancel() {},
|
|
76
|
+
renderPreview: () => ["large preview"],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const rendered = box.render(40).map(stripTerminalSequences).join("\n");
|
|
80
|
+
expect(rendered).toStartWith("Composition\n");
|
|
81
|
+
expect(rendered).toContain("large preview");
|
|
82
|
+
expect(rendered).toContain("select");
|
|
83
|
+
expect(rendered).toContain("back");
|
|
84
|
+
expect(rendered).not.toContain("╭");
|
|
85
|
+
});
|
|
86
|
+
});
|