@vizejs/fresco 0.347.7 → 0.351.0
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/app-DJImCkSW.d.mts +135 -0
- package/dist/app-DJImCkSW.d.mts.map +1 -0
- package/dist/{useInput-iUuaOefz.mjs → app-D_Ke7C8w.mjs} +2 -216
- package/dist/{useInput-iUuaOefz.mjs.map → app-D_Ke7C8w.mjs.map} +1 -1
- package/dist/components/index.d.mts +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/{components-rzllq5gt.mjs → components-3-vKPu1z.mjs} +2 -2
- package/dist/{components-rzllq5gt.mjs.map → components-3-vKPu1z.mjs.map} +1 -1
- package/dist/composables/index.d.mts +3 -2
- package/dist/composables/index.mjs +3 -2
- package/dist/{composables-Dh8YqmVY.mjs → composables-Da3c5pxz.mjs} +3 -2
- package/dist/{composables-Dh8YqmVY.mjs.map → composables-Da3c5pxz.mjs.map} +1 -1
- package/dist/{index-Dwx6lKP0.d.mts → index-DqGcW9sz.d.mts} +2 -85
- package/dist/index-DqGcW9sz.d.mts.map +1 -0
- package/dist/{index-CQHg8OjC.d.mts → index-xYdAuAvd.d.mts} +190 -384
- package/dist/index-xYdAuAvd.d.mts.map +1 -0
- package/dist/index.d.mts +7 -136
- package/dist/index.mjs +4 -3
- package/dist/index.mjs.map +1 -1
- package/dist/renderer-DWG0G3g8.d.mts +198 -0
- package/dist/renderer-DWG0G3g8.d.mts.map +1 -0
- package/dist/testing/index.d.mts +124 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +282 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/useApp-CNjer3hJ.d.mts +86 -0
- package/dist/useApp-CNjer3hJ.d.mts.map +1 -0
- package/dist/useInput-BpH-JIU3.mjs +219 -0
- package/dist/useInput-BpH-JIU3.mjs.map +1 -0
- package/package.json +8 -4
- package/dist/index-CQHg8OjC.d.mts.map +0 -1
- package/dist/index-Dwx6lKP0.d.mts.map +0 -1
- package/dist/index.d.mts.map +0 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { A as SCREEN_READER_KEY, C as FOCUS_KEY, D as APP_KEY, O as createAppContext, _ as STREAMS_KEY, a as lastMouseEvent, h as createCursorContext, i as lastKeyEvent, m as CURSOR_KEY, n as lastCompositionEvent, o as lastPasteEvent, r as lastFocusEvent, s as lastResizeEvent, u as createRenderer$1, v as createStreamsContext, w as createFocusManager } from "../app-D_Ke7C8w.mjs";
|
|
2
|
+
import { h, nextTick, ref } from "@vue/runtime-core";
|
|
3
|
+
//#region src/testing/mount.ts
|
|
4
|
+
/**
|
|
5
|
+
* Test-only mount harness for the Fresco JS layer.
|
|
6
|
+
*
|
|
7
|
+
* Fresco's renderer is pure JS up to the byte boundary that crosses into the
|
|
8
|
+
* native module: `renderer.ts` imports only types from `@vizejs/fresco-native`,
|
|
9
|
+
* and `app.ts` loads the native binding lazily and only in interactive mode.
|
|
10
|
+
* This harness mounts components through the real custom renderer with the
|
|
11
|
+
* same provide set `createApp`/`renderToString` install, so tests assert the
|
|
12
|
+
* mounted `FrescoNode` output tree without a native build and without mocks.
|
|
13
|
+
*
|
|
14
|
+
* Not exported from the package entry points; `vp pack` does not ship it.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Mount a component (or a plain render function) through the Fresco renderer.
|
|
18
|
+
*
|
|
19
|
+
* The returned {@link MountedFresco.root} is live: re-read it after reactive
|
|
20
|
+
* updates plus `await nextTick()` to observe the patched output tree.
|
|
21
|
+
*/
|
|
22
|
+
function mountFresco(root, options = {}) {
|
|
23
|
+
const { createApp } = createRenderer$1();
|
|
24
|
+
const app = createApp(typeof root === "function" ? { setup: () => root } : root);
|
|
25
|
+
const focusManager = createFocusManager();
|
|
26
|
+
const screenReaderEnabled = ref(options.screenReader ?? false);
|
|
27
|
+
const noopWrite = () => {};
|
|
28
|
+
const width = options.width ?? 80;
|
|
29
|
+
const height = options.height ?? 24;
|
|
30
|
+
const appContext = createAppContext({
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
stdout: {
|
|
34
|
+
isTTY: false,
|
|
35
|
+
columns: width,
|
|
36
|
+
rows: height,
|
|
37
|
+
write: () => true
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
app.provide(APP_KEY, appContext);
|
|
41
|
+
app.provide(FOCUS_KEY, focusManager);
|
|
42
|
+
app.provide(SCREEN_READER_KEY, screenReaderEnabled);
|
|
43
|
+
app.provide(CURSOR_KEY, createCursorContext(noopWrite));
|
|
44
|
+
app.provide(STREAMS_KEY, createStreamsContext({
|
|
45
|
+
interactive: false,
|
|
46
|
+
writeToStdout: noopWrite,
|
|
47
|
+
writeToStderr: noopWrite
|
|
48
|
+
}));
|
|
49
|
+
const rootElement = {
|
|
50
|
+
id: -1,
|
|
51
|
+
type: "root",
|
|
52
|
+
props: {},
|
|
53
|
+
children: [],
|
|
54
|
+
parent: null
|
|
55
|
+
};
|
|
56
|
+
app.mount(rootElement);
|
|
57
|
+
return {
|
|
58
|
+
root: rootElement,
|
|
59
|
+
app,
|
|
60
|
+
appContext,
|
|
61
|
+
focusManager,
|
|
62
|
+
screenReaderEnabled,
|
|
63
|
+
unmount: () => app.unmount()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Dispatch a key event through the same `lastKeyEvent` ref the interactive
|
|
68
|
+
* event loop writes, then wait for watchers to flush.
|
|
69
|
+
*
|
|
70
|
+
* Pass `char` for printable input or `key` for named keys ("left", "enter",
|
|
71
|
+
* "backspace", ...). Modifiers default to false.
|
|
72
|
+
*/
|
|
73
|
+
function dispatchKey(event) {
|
|
74
|
+
lastKeyEvent.value = {
|
|
75
|
+
type: "key",
|
|
76
|
+
ctrl: false,
|
|
77
|
+
alt: false,
|
|
78
|
+
shift: false,
|
|
79
|
+
meta: false,
|
|
80
|
+
super: false,
|
|
81
|
+
hyper: false,
|
|
82
|
+
capsLock: false,
|
|
83
|
+
numLock: false,
|
|
84
|
+
...event
|
|
85
|
+
};
|
|
86
|
+
return nextTick();
|
|
87
|
+
}
|
|
88
|
+
/** Dispatch a sequence of printable characters via {@link dispatchKey}. */
|
|
89
|
+
async function typeChars(text) {
|
|
90
|
+
for (const char of text) await dispatchKey({ char });
|
|
91
|
+
}
|
|
92
|
+
function definedProps(props) {
|
|
93
|
+
const entries = Object.entries(props).filter(([, value]) => value !== void 0);
|
|
94
|
+
if (entries.length === 0) return void 0;
|
|
95
|
+
return Object.fromEntries(entries);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Convert a mounted node (or the harness root) into a plain snapshot object
|
|
99
|
+
* suitable for `assert.deepEqual` against an inline expected structure.
|
|
100
|
+
*/
|
|
101
|
+
function toTreeSnapshot(node) {
|
|
102
|
+
const snapshot = { type: node.type };
|
|
103
|
+
if (node.text !== void 0) snapshot.text = node.text;
|
|
104
|
+
const props = definedProps(node.props);
|
|
105
|
+
if (props) snapshot.props = props;
|
|
106
|
+
if (node.children.length > 0) snapshot.children = node.children.map((child) => toTreeSnapshot(child));
|
|
107
|
+
return snapshot;
|
|
108
|
+
}
|
|
109
|
+
/** Depth-first list of nodes matching a predicate, starting at `node`. */
|
|
110
|
+
function findNodes(node, predicate) {
|
|
111
|
+
const matches = [];
|
|
112
|
+
const visit = (current) => {
|
|
113
|
+
if (predicate(current)) matches.push(current);
|
|
114
|
+
for (const child of current.children) visit(child);
|
|
115
|
+
};
|
|
116
|
+
visit(node);
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
/** First mounted child under the harness root, asserting it exists. */
|
|
120
|
+
function firstChild(mounted) {
|
|
121
|
+
const child = mounted.root.children[0];
|
|
122
|
+
if (!child) throw new Error("expected the mounted tree to have a root child");
|
|
123
|
+
return child;
|
|
124
|
+
}
|
|
125
|
+
/** Convenience wrapper that renders `component` with `h` and mounts it. */
|
|
126
|
+
function mountComponent(component, props = {}, slot, options = {}) {
|
|
127
|
+
return mountFresco(() => h(component, props, slot), options);
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/testing/index.ts
|
|
131
|
+
/**
|
|
132
|
+
* Test utilities for Fresco component and app behavior.
|
|
133
|
+
*
|
|
134
|
+
* The harness mounts through the real Vue custom renderer and records frames
|
|
135
|
+
* without loading the native terminal binding, so it is safe in plain Node,
|
|
136
|
+
* Vitest, and `node:test` suites.
|
|
137
|
+
*/
|
|
138
|
+
function nodeText(node) {
|
|
139
|
+
if (node.text !== void 0) return node.text;
|
|
140
|
+
const propText = node.props.text ?? node.props.content;
|
|
141
|
+
if (typeof propText === "string" || typeof propText === "number") return String(propText);
|
|
142
|
+
if (node.type !== "input") return "";
|
|
143
|
+
const value = node.props.value;
|
|
144
|
+
const placeholder = node.props.placeholder;
|
|
145
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
146
|
+
if (typeof placeholder === "string" || typeof placeholder === "number") return String(placeholder);
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
function isStaticNode(node) {
|
|
150
|
+
return node.props.internal_static === true || node.props.internalStatic === true;
|
|
151
|
+
}
|
|
152
|
+
function joinOutput(parts, separator = "\n") {
|
|
153
|
+
return parts.filter(Boolean).join(separator);
|
|
154
|
+
}
|
|
155
|
+
function normalizeOutput(output) {
|
|
156
|
+
return output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
157
|
+
}
|
|
158
|
+
function appendOutput(previous, next) {
|
|
159
|
+
const normalized = normalizeOutput(next);
|
|
160
|
+
if (!normalized) return previous;
|
|
161
|
+
return previous ? `${previous}\n${normalized}` : normalized;
|
|
162
|
+
}
|
|
163
|
+
function treeToFrameOutput(node, options = {}) {
|
|
164
|
+
if (options.skipStatic && isStaticNode(node)) return "";
|
|
165
|
+
const ownText = nodeText(node);
|
|
166
|
+
const childOutput = node.children.map((child) => treeToFrameOutput(child, options));
|
|
167
|
+
if (node.type === "text" || node.type === "input") return `${ownText}${childOutput.join("")}`;
|
|
168
|
+
const style = node.props.style ?? {};
|
|
169
|
+
return joinOutput(childOutput, (style.flexDirection ?? style.flex_direction) === "column" ? "\n" : "");
|
|
170
|
+
}
|
|
171
|
+
function captureStaticOutput(node, renderedStaticItems) {
|
|
172
|
+
const output = [];
|
|
173
|
+
const visit = (current) => {
|
|
174
|
+
if (isStaticNode(current)) {
|
|
175
|
+
const renderedItems = renderedStaticItems.get(current.id) ?? 0;
|
|
176
|
+
const nextOutput = joinOutput(current.children.slice(renderedItems).map((child) => treeToFrameOutput(child)), "\n");
|
|
177
|
+
if (nextOutput) output.push(nextOutput);
|
|
178
|
+
renderedStaticItems.set(current.id, current.children.length);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const child of current.children) visit(child);
|
|
182
|
+
};
|
|
183
|
+
visit(node);
|
|
184
|
+
return joinOutput(output, "\n");
|
|
185
|
+
}
|
|
186
|
+
function renderFrame(root, staticOutput) {
|
|
187
|
+
return joinOutput([staticOutput, treeToFrameOutput(root, { skipStatic: true })], "\n");
|
|
188
|
+
}
|
|
189
|
+
function latestSnapshot(snapshots) {
|
|
190
|
+
const snapshot = snapshots[snapshots.length - 1];
|
|
191
|
+
if (!snapshot) throw new Error("expected renderTui to record an initial frame");
|
|
192
|
+
return snapshot;
|
|
193
|
+
}
|
|
194
|
+
async function dispatchPaste(text) {
|
|
195
|
+
lastPasteEvent.value = {
|
|
196
|
+
type: "paste",
|
|
197
|
+
text
|
|
198
|
+
};
|
|
199
|
+
await nextTick();
|
|
200
|
+
}
|
|
201
|
+
async function dispatchMouse(event) {
|
|
202
|
+
lastMouseEvent.value = {
|
|
203
|
+
type: "mouse",
|
|
204
|
+
...event
|
|
205
|
+
};
|
|
206
|
+
await nextTick();
|
|
207
|
+
}
|
|
208
|
+
async function dispatchFocus(focused) {
|
|
209
|
+
lastFocusEvent.value = {
|
|
210
|
+
type: "focus",
|
|
211
|
+
focused
|
|
212
|
+
};
|
|
213
|
+
await nextTick();
|
|
214
|
+
}
|
|
215
|
+
async function dispatchComposition(type, text, cursor) {
|
|
216
|
+
lastCompositionEvent.value = {
|
|
217
|
+
type,
|
|
218
|
+
text,
|
|
219
|
+
cursor
|
|
220
|
+
};
|
|
221
|
+
await nextTick();
|
|
222
|
+
}
|
|
223
|
+
async function dispatchResize(mounted, width, height) {
|
|
224
|
+
lastResizeEvent.value = {
|
|
225
|
+
type: "resize",
|
|
226
|
+
width,
|
|
227
|
+
height
|
|
228
|
+
};
|
|
229
|
+
mounted.appContext.width.value = width;
|
|
230
|
+
mounted.appContext.height.value = height;
|
|
231
|
+
await nextTick();
|
|
232
|
+
}
|
|
233
|
+
/** Mount a Fresco app for tests with frame snapshots and input injection. */
|
|
234
|
+
function renderTui(root, options = {}) {
|
|
235
|
+
const mounted = mountFresco(root, options);
|
|
236
|
+
const frames = [];
|
|
237
|
+
const frameSnapshots = [];
|
|
238
|
+
const renderedStaticItems = /* @__PURE__ */ new Map();
|
|
239
|
+
let staticOutput = "";
|
|
240
|
+
const captureFrame = () => {
|
|
241
|
+
staticOutput = appendOutput(staticOutput, captureStaticOutput(mounted.root, renderedStaticItems));
|
|
242
|
+
const snapshot = {
|
|
243
|
+
output: renderFrame(mounted.root, staticOutput),
|
|
244
|
+
tree: toTreeSnapshot(mounted.root)
|
|
245
|
+
};
|
|
246
|
+
frames.push(snapshot.output);
|
|
247
|
+
frameSnapshots.push(snapshot);
|
|
248
|
+
return snapshot;
|
|
249
|
+
};
|
|
250
|
+
const recordAfter = async (dispatch) => {
|
|
251
|
+
await dispatch;
|
|
252
|
+
return captureFrame();
|
|
253
|
+
};
|
|
254
|
+
const input = {
|
|
255
|
+
key: (event) => recordAfter(dispatchKey(event)),
|
|
256
|
+
async text(text) {
|
|
257
|
+
for (const char of text) await dispatchKey({ char });
|
|
258
|
+
return captureFrame();
|
|
259
|
+
},
|
|
260
|
+
paste: (text) => recordAfter(dispatchPaste(text)),
|
|
261
|
+
resize: (width, height) => recordAfter(dispatchResize(mounted, width, height)),
|
|
262
|
+
mouse: (event) => recordAfter(dispatchMouse(event)),
|
|
263
|
+
focus: (focused) => recordAfter(dispatchFocus(focused)),
|
|
264
|
+
compositionStart: (text = "", cursor = 0) => recordAfter(dispatchComposition("compositionstart", text, cursor)),
|
|
265
|
+
compositionUpdate: (text, cursor) => recordAfter(dispatchComposition("compositionupdate", text, cursor)),
|
|
266
|
+
compositionEnd: (text, cursor = text.length) => recordAfter(dispatchComposition("compositionend", text, cursor))
|
|
267
|
+
};
|
|
268
|
+
captureFrame();
|
|
269
|
+
return {
|
|
270
|
+
...mounted,
|
|
271
|
+
frames,
|
|
272
|
+
frameSnapshots,
|
|
273
|
+
input,
|
|
274
|
+
captureFrame,
|
|
275
|
+
lastFrame: () => latestSnapshot(frameSnapshots).output,
|
|
276
|
+
frameSnapshot: () => latestSnapshot(frameSnapshots)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { dispatchKey, findNodes, firstChild, mountComponent, mountFresco, renderTui, toTreeSnapshot, typeChars };
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["createRenderer"],"sources":["../../src/testing/mount.ts","../../src/testing/index.ts"],"sourcesContent":["/**\n * Test-only mount harness for the Fresco JS layer.\n *\n * Fresco's renderer is pure JS up to the byte boundary that crosses into the\n * native module: `renderer.ts` imports only types from `@vizejs/fresco-native`,\n * and `app.ts` loads the native binding lazily and only in interactive mode.\n * This harness mounts components through the real custom renderer with the\n * same provide set `createApp`/`renderToString` install, so tests assert the\n * mounted `FrescoNode` output tree without a native build and without mocks.\n *\n * Not exported from the package entry points; `vp pack` does not ship it.\n */\n\nimport {\n h,\n ref,\n nextTick,\n type App as VueApp,\n type Component,\n type Ref,\n type VNodeChild,\n} from \"@vue/runtime-core\";\nimport { SCREEN_READER_KEY } from \"../accessibility.js\";\nimport { lastKeyEvent, type KeyEvent } from \"../app.js\";\nimport { APP_KEY, createAppContext, type UseAppReturn } from \"../composables/useApp.js\";\nimport { createCursorContext, CURSOR_KEY } from \"../composables/useCursor.js\";\nimport { createFocusManager, FOCUS_KEY, type FocusManager } from \"../composables/useFocus.js\";\nimport { createStreamsContext, STREAMS_KEY } from \"../composables/useStreams.js\";\nimport { createRenderer, type FrescoElement, type FrescoNode } from \"../renderer.js\";\n\n/** Options for {@link mountFresco}. */\nexport interface MountFrescoOptions {\n /** Terminal width provided through the app context. Defaults to 80. */\n width?: number;\n /** Terminal height provided through the app context. Defaults to 24. */\n height?: number;\n /** Initial screen reader mode. Defaults to false. */\n screenReader?: boolean;\n}\n\n/** A mounted Fresco component tree under test. */\nexport interface MountedFresco {\n /** Root element the Vue app mounted into; children form the output tree. */\n root: FrescoElement;\n /** The Vue app instance (for `provide`-level introspection if needed). */\n app: VueApp<FrescoElement>;\n /** App context provided to composables such as `useApp` and `useWindowSize`. */\n appContext: UseAppReturn;\n /** Focus manager provided to the tree, as the real app would. */\n focusManager: FocusManager;\n /** Screen reader flag provided to the tree; writable to flip modes. */\n screenReaderEnabled: Ref<boolean>;\n /** Unmount the tree and dispose all component watchers. */\n unmount(): void;\n}\n\n/**\n * Mount a component (or a plain render function) through the Fresco renderer.\n *\n * The returned {@link MountedFresco.root} is live: re-read it after reactive\n * updates plus `await nextTick()` to observe the patched output tree.\n */\nexport function mountFresco(\n root: Component | (() => VNodeChild),\n options: MountFrescoOptions = {},\n): MountedFresco {\n const { createApp } = createRenderer();\n const component: Component = typeof root === \"function\" ? { setup: () => root } : root;\n const app = createApp(component);\n\n const focusManager = createFocusManager();\n const screenReaderEnabled = ref(options.screenReader ?? false);\n const noopWrite = () => {};\n const width = options.width ?? 80;\n const height = options.height ?? 24;\n // A detached stdout stand-in keeps createAppContext from attaching resize\n // listeners to the real process.stdout on every mounted test tree.\n const stdout = {\n isTTY: false,\n columns: width,\n rows: height,\n write: () => true,\n } as unknown as NodeJS.WriteStream;\n\n const appContext = createAppContext({ width, height, stdout });\n app.provide(APP_KEY, appContext);\n app.provide(FOCUS_KEY, focusManager);\n app.provide(SCREEN_READER_KEY, screenReaderEnabled);\n app.provide(CURSOR_KEY, createCursorContext(noopWrite));\n app.provide(\n STREAMS_KEY,\n createStreamsContext({\n interactive: false,\n writeToStdout: noopWrite,\n writeToStderr: noopWrite,\n }),\n );\n\n const rootElement: FrescoElement = {\n id: -1,\n type: \"root\",\n props: {},\n children: [],\n parent: null,\n };\n app.mount(rootElement);\n\n return {\n root: rootElement,\n app,\n appContext,\n focusManager,\n screenReaderEnabled,\n unmount: () => app.unmount(),\n };\n}\n\n/**\n * Dispatch a key event through the same `lastKeyEvent` ref the interactive\n * event loop writes, then wait for watchers to flush.\n *\n * Pass `char` for printable input or `key` for named keys (\"left\", \"enter\",\n * \"backspace\", ...). Modifiers default to false.\n */\nexport function dispatchKey(event: Partial<Omit<KeyEvent, \"type\">>): Promise<void> {\n lastKeyEvent.value = {\n type: \"key\",\n ctrl: false,\n alt: false,\n shift: false,\n meta: false,\n super: false,\n hyper: false,\n capsLock: false,\n numLock: false,\n ...event,\n };\n return nextTick();\n}\n\n/** Dispatch a sequence of printable characters via {@link dispatchKey}. */\nexport async function typeChars(text: string): Promise<void> {\n for (const char of text) {\n await dispatchKey({ char });\n }\n}\n\n/**\n * Serializable snapshot of a mounted output tree.\n *\n * Node ids come from a module-global counter and depend on mount order, and\n * `parent` back-references make trees cyclic, so both are omitted. Props are\n * kept only when defined, so snapshots stay inline-sized and deterministic.\n */\nexport interface FrescoNodeSnapshot {\n type: FrescoNode[\"type\"];\n text?: string;\n props?: Record<string, unknown>;\n children?: FrescoNodeSnapshot[];\n}\n\nfunction definedProps(props: Record<string, unknown>): Record<string, unknown> | undefined {\n const entries = Object.entries(props).filter(([, value]) => value !== undefined);\n if (entries.length === 0) return undefined;\n return Object.fromEntries(entries);\n}\n\n/**\n * Convert a mounted node (or the harness root) into a plain snapshot object\n * suitable for `assert.deepEqual` against an inline expected structure.\n */\nexport function toTreeSnapshot(node: FrescoNode): FrescoNodeSnapshot {\n const snapshot: FrescoNodeSnapshot = { type: node.type };\n if (node.text !== undefined) snapshot.text = node.text;\n const props = definedProps(node.props);\n if (props) snapshot.props = props;\n if (node.children.length > 0) {\n snapshot.children = node.children.map((child) => toTreeSnapshot(child));\n }\n return snapshot;\n}\n\n/** Depth-first list of nodes matching a predicate, starting at `node`. */\nexport function findNodes(\n node: FrescoNode,\n predicate: (candidate: FrescoNode) => boolean,\n): FrescoNode[] {\n const matches: FrescoNode[] = [];\n const visit = (current: FrescoNode) => {\n if (predicate(current)) matches.push(current);\n for (const child of current.children) visit(child);\n };\n visit(node);\n return matches;\n}\n\n/** First mounted child under the harness root, asserting it exists. */\nexport function firstChild(mounted: MountedFresco): FrescoNode {\n const child = mounted.root.children[0];\n if (!child) throw new Error(\"expected the mounted tree to have a root child\");\n return child;\n}\n\n/** Convenience wrapper that renders `component` with `h` and mounts it. */\nexport function mountComponent(\n component: Component,\n props: Record<string, unknown> = {},\n slot?: () => VNodeChild,\n options: MountFrescoOptions = {},\n): MountedFresco {\n return mountFresco(() => h(component, props, slot), options);\n}\n","/**\n * Test utilities for Fresco component and app behavior.\n *\n * The harness mounts through the real Vue custom renderer and records frames\n * without loading the native terminal binding, so it is safe in plain Node,\n * Vitest, and `node:test` suites.\n */\n\nimport { nextTick, type Component, type VNodeChild } from \"@vue/runtime-core\";\nimport {\n lastCompositionEvent,\n lastFocusEvent,\n lastMouseEvent,\n lastPasteEvent,\n lastResizeEvent,\n type CompositionEvent,\n type FocusEvent,\n type KeyEvent,\n type MouseEvent,\n type PasteEvent,\n type ResizeEvent,\n} from \"../app.js\";\nimport type { FrescoNode } from \"../renderer.js\";\nimport {\n dispatchKey,\n mountFresco,\n toTreeSnapshot,\n type FrescoNodeSnapshot,\n type MountedFresco,\n type MountFrescoOptions,\n} from \"./mount.js\";\n\nexport {\n dispatchKey,\n findNodes,\n firstChild,\n mountComponent,\n mountFresco,\n toTreeSnapshot,\n typeChars,\n type FrescoNodeSnapshot,\n type MountedFresco,\n type MountFrescoOptions,\n} from \"./mount.js\";\n\nexport type RenderTuiOptions = MountFrescoOptions;\nexport type RenderTuiRoot = Component | (() => VNodeChild);\nexport type KeyInput = Partial<Omit<KeyEvent, \"type\">>;\nexport type MouseInput = Omit<MouseEvent, \"type\">;\n\n/** A single recorded harness frame. */\nexport interface FrescoFrameSnapshot {\n /** Plain terminal output projected from the current mounted tree. */\n output: string;\n /** Serializable renderer tree for structural assertions. */\n tree: FrescoNodeSnapshot;\n}\n\n/** Input driver that injects events through Fresco's public event refs. */\nexport interface FrescoInputDriver {\n /** Dispatch a key event and record the resulting frame. */\n key(event: KeyInput): Promise<FrescoFrameSnapshot>;\n /** Dispatch each printable character and record one frame after the text. */\n text(text: string): Promise<FrescoFrameSnapshot>;\n /** Dispatch a bracketed paste payload and record the resulting frame. */\n paste(text: string): Promise<FrescoFrameSnapshot>;\n /** Update the provided app size, dispatch a resize event, and record a frame. */\n resize(width: number, height: number): Promise<FrescoFrameSnapshot>;\n /** Dispatch a mouse event and record the resulting frame. */\n mouse(event: MouseInput): Promise<FrescoFrameSnapshot>;\n /** Dispatch terminal focus state and record the resulting frame. */\n focus(focused: boolean): Promise<FrescoFrameSnapshot>;\n /** Dispatch a composition start event and record the resulting frame. */\n compositionStart(text?: string, cursor?: number): Promise<FrescoFrameSnapshot>;\n /** Dispatch a composition update event and record the resulting frame. */\n compositionUpdate(text: string, cursor: number): Promise<FrescoFrameSnapshot>;\n /** Dispatch a composition end event and record the resulting frame. */\n compositionEnd(text: string, cursor?: number): Promise<FrescoFrameSnapshot>;\n}\n\n/** Mounted TUI harness with frame history and input helpers. */\nexport interface RenderTuiResult extends MountedFresco {\n /** Recorded plain frames, including the initial mount frame. */\n readonly frames: readonly string[];\n /** Recorded frame snapshots, including the initial mount frame. */\n readonly frameSnapshots: readonly FrescoFrameSnapshot[];\n /** Event injection helpers. */\n readonly input: FrescoInputDriver;\n /** Record and return a fresh snapshot of the current mounted tree. */\n captureFrame(): FrescoFrameSnapshot;\n /** Return the latest recorded plain frame. */\n lastFrame(): string;\n /** Return the latest recorded frame snapshot. */\n frameSnapshot(): FrescoFrameSnapshot;\n}\n\nfunction nodeText(node: FrescoNode): string {\n if (node.text !== undefined) return node.text;\n const propText = node.props.text ?? node.props.content;\n if (typeof propText === \"string\" || typeof propText === \"number\") return String(propText);\n if (node.type !== \"input\") return \"\";\n\n const value = node.props.value;\n const placeholder = node.props.placeholder;\n if (typeof value === \"string\" || typeof value === \"number\") return String(value);\n if (typeof placeholder === \"string\" || typeof placeholder === \"number\")\n return String(placeholder);\n return \"\";\n}\n\nfunction isStaticNode(node: FrescoNode): boolean {\n return node.props.internal_static === true || node.props.internalStatic === true;\n}\n\nfunction joinOutput(parts: readonly string[], separator = \"\\n\"): string {\n return parts.filter(Boolean).join(separator);\n}\n\nfunction normalizeOutput(output: string): string {\n return output.endsWith(\"\\n\") ? output.slice(0, -1) : output;\n}\n\nfunction appendOutput(previous: string, next: string): string {\n const normalized = normalizeOutput(next);\n if (!normalized) return previous;\n return previous ? `${previous}\\n${normalized}` : normalized;\n}\n\nfunction treeToFrameOutput(node: FrescoNode, options: { skipStatic?: boolean } = {}): string {\n if (options.skipStatic && isStaticNode(node)) return \"\";\n\n const ownText = nodeText(node);\n const childOutput = node.children.map((child) => treeToFrameOutput(child, options));\n\n if (node.type === \"text\" || node.type === \"input\") {\n return `${ownText}${childOutput.join(\"\")}`;\n }\n\n const style = (node.props.style ?? {}) as Record<string, unknown>;\n const flexDirection = style.flexDirection ?? style.flex_direction;\n const separator = flexDirection === \"column\" ? \"\\n\" : \"\";\n return joinOutput(childOutput, separator);\n}\n\nfunction captureStaticOutput(node: FrescoNode, renderedStaticItems: Map<number, number>): string {\n const output: string[] = [];\n\n const visit = (current: FrescoNode) => {\n if (isStaticNode(current)) {\n const renderedItems = renderedStaticItems.get(current.id) ?? 0;\n const nextItems = current.children.slice(renderedItems);\n const nextOutput = joinOutput(\n nextItems.map((child) => treeToFrameOutput(child)),\n \"\\n\",\n );\n if (nextOutput) output.push(nextOutput);\n renderedStaticItems.set(current.id, current.children.length);\n return;\n }\n\n for (const child of current.children) visit(child);\n };\n\n visit(node);\n return joinOutput(output, \"\\n\");\n}\n\nfunction renderFrame(root: FrescoNode, staticOutput: string): string {\n return joinOutput([staticOutput, treeToFrameOutput(root, { skipStatic: true })], \"\\n\");\n}\n\nfunction latestSnapshot(snapshots: readonly FrescoFrameSnapshot[]): FrescoFrameSnapshot {\n const snapshot = snapshots[snapshots.length - 1];\n if (!snapshot) throw new Error(\"expected renderTui to record an initial frame\");\n return snapshot;\n}\n\nasync function dispatchPaste(text: string): Promise<void> {\n const event: PasteEvent = { type: \"paste\", text };\n lastPasteEvent.value = event;\n await nextTick();\n}\n\nasync function dispatchMouse(event: MouseInput): Promise<void> {\n lastMouseEvent.value = { type: \"mouse\", ...event };\n await nextTick();\n}\n\nasync function dispatchFocus(focused: boolean): Promise<void> {\n const event: FocusEvent = { type: \"focus\", focused };\n lastFocusEvent.value = event;\n await nextTick();\n}\n\nasync function dispatchComposition(\n type: CompositionEvent[\"type\"],\n text: string,\n cursor: number,\n): Promise<void> {\n lastCompositionEvent.value = { type, text, cursor };\n await nextTick();\n}\n\nasync function dispatchResize(\n mounted: MountedFresco,\n width: number,\n height: number,\n): Promise<void> {\n const event: ResizeEvent = { type: \"resize\", width, height };\n lastResizeEvent.value = event;\n mounted.appContext.width.value = width;\n mounted.appContext.height.value = height;\n await nextTick();\n}\n\n/** Mount a Fresco app for tests with frame snapshots and input injection. */\nexport function renderTui(root: RenderTuiRoot, options: RenderTuiOptions = {}): RenderTuiResult {\n const mounted = mountFresco(root, options);\n const frames: string[] = [];\n const frameSnapshots: FrescoFrameSnapshot[] = [];\n const renderedStaticItems = new Map<number, number>();\n let staticOutput = \"\";\n\n const captureFrame = () => {\n staticOutput = appendOutput(\n staticOutput,\n captureStaticOutput(mounted.root, renderedStaticItems),\n );\n const snapshot: FrescoFrameSnapshot = {\n output: renderFrame(mounted.root, staticOutput),\n tree: toTreeSnapshot(mounted.root),\n };\n frames.push(snapshot.output);\n frameSnapshots.push(snapshot);\n return snapshot;\n };\n\n const recordAfter = async (dispatch: Promise<void>) => {\n await dispatch;\n return captureFrame();\n };\n\n const input: FrescoInputDriver = {\n key: (event) => recordAfter(dispatchKey(event)),\n async text(text) {\n for (const char of text) {\n await dispatchKey({ char });\n }\n return captureFrame();\n },\n paste: (text) => recordAfter(dispatchPaste(text)),\n resize: (width, height) => recordAfter(dispatchResize(mounted, width, height)),\n mouse: (event) => recordAfter(dispatchMouse(event)),\n focus: (focused) => recordAfter(dispatchFocus(focused)),\n compositionStart: (text = \"\", cursor = 0) =>\n recordAfter(dispatchComposition(\"compositionstart\", text, cursor)),\n compositionUpdate: (text, cursor) =>\n recordAfter(dispatchComposition(\"compositionupdate\", text, cursor)),\n compositionEnd: (text, cursor = text.length) =>\n recordAfter(dispatchComposition(\"compositionend\", text, cursor)),\n };\n\n captureFrame();\n\n return {\n ...mounted,\n frames,\n frameSnapshots,\n input,\n captureFrame,\n lastFrame: () => latestSnapshot(frameSnapshots).output,\n frameSnapshot: () => latestSnapshot(frameSnapshots),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Ref } from "@vue/runtime-core";
|
|
2
|
+
|
|
3
|
+
//#region src/composables/useFocus.d.ts
|
|
4
|
+
interface UseFocusOptions {
|
|
5
|
+
/** Enable or disable this focus target while keeping its logical identity */
|
|
6
|
+
isActive?: boolean | Ref<boolean>;
|
|
7
|
+
/** Auto-focus this component when nothing else is focused */
|
|
8
|
+
autoFocus?: boolean;
|
|
9
|
+
/** Focus ID for this element */
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
interface FocusManager {
|
|
13
|
+
/** Currently focused element ID */
|
|
14
|
+
focusedId: Ref<string | null>;
|
|
15
|
+
/** Ink-compatible currently focused ID alias */
|
|
16
|
+
activeId: Readonly<Ref<string | undefined>>;
|
|
17
|
+
/** All active focusable element IDs */
|
|
18
|
+
focusableIds: Readonly<Ref<string[]>>;
|
|
19
|
+
/** Whether focus management is enabled */
|
|
20
|
+
isEnabled: Ref<boolean>;
|
|
21
|
+
/** Enable focus management */
|
|
22
|
+
enableFocus: () => void;
|
|
23
|
+
/** Disable focus management */
|
|
24
|
+
disableFocus: () => void;
|
|
25
|
+
/** Focus a specific element */
|
|
26
|
+
focus: (id: string) => void;
|
|
27
|
+
/** Focus next element */
|
|
28
|
+
focusNext: () => void;
|
|
29
|
+
/** Focus previous element */
|
|
30
|
+
focusPrevious: () => void;
|
|
31
|
+
/** Register a focusable element */
|
|
32
|
+
register: (id: string, options?: {
|
|
33
|
+
isActive?: boolean;
|
|
34
|
+
autoFocus?: boolean;
|
|
35
|
+
}) => void;
|
|
36
|
+
/** Unregister a focusable element */
|
|
37
|
+
unregister: (id: string) => void;
|
|
38
|
+
/** Activate or deactivate a focusable element while keeping its order */
|
|
39
|
+
setActive: (id: string, isActive: boolean) => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Use focus management
|
|
43
|
+
*/
|
|
44
|
+
declare function useFocus(options?: UseFocusOptions): {
|
|
45
|
+
id: string;
|
|
46
|
+
isFocused: import("@vue/reactivity").ComputedRef<boolean>;
|
|
47
|
+
focus: (targetId?: string) => void;
|
|
48
|
+
blur: () => void;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Use global focus manager controls.
|
|
52
|
+
*/
|
|
53
|
+
declare function useFocusManager(): {
|
|
54
|
+
enableFocus: () => void;
|
|
55
|
+
disableFocus: () => void;
|
|
56
|
+
focusNext: () => void;
|
|
57
|
+
focusPrevious: () => void;
|
|
58
|
+
focus: (id: string) => void;
|
|
59
|
+
activeId: Readonly<Ref<string | undefined, string | undefined>>;
|
|
60
|
+
focusableIds: Readonly<Ref<string[], string[]>>;
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/composables/useApp.d.ts
|
|
64
|
+
interface UseAppReturn {
|
|
65
|
+
/** Terminal width */
|
|
66
|
+
width: Ref<number>;
|
|
67
|
+
/** Terminal height */
|
|
68
|
+
height: Ref<number>;
|
|
69
|
+
/** Whether app is running */
|
|
70
|
+
isRunning: Ref<boolean>;
|
|
71
|
+
/** Exit the app */
|
|
72
|
+
exit: (errorOrResult?: unknown) => void;
|
|
73
|
+
/** Force re-render */
|
|
74
|
+
render: () => void;
|
|
75
|
+
/** Clear the screen */
|
|
76
|
+
clear: () => void;
|
|
77
|
+
/** Wait until pending render output has flushed */
|
|
78
|
+
waitUntilRenderFlush: () => Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Use app context
|
|
82
|
+
*/
|
|
83
|
+
declare function useApp(): UseAppReturn;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { useFocus as a, UseFocusOptions as i, useApp as n, useFocusManager as o, FocusManager as r, UseAppReturn as t };
|
|
86
|
+
//# sourceMappingURL=useApp-CNjer3hJ.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApp-CNjer3hJ.d.mts","names":[],"sources":["../src/composables/useFocus.ts","../src/composables/useApp.ts"],"mappings":";;;UAkBiB,eAAA;EAES;EAAxB,QAAA,aAAqB,GAAG;EAAH;EAErB,SAAA;EAEA;EAAA,EAAA;AAAA;AAAA,UAGe,YAAA;EAAY;EAE3B,SAAA,EAAW,GAAA;EAAA;EAEX,QAAA,EAAU,QAAA,CAAS,GAAA;EAAT;EAEV,YAAA,EAAc,QAAA,CAAS,GAAA;EAAT;EAEd,SAAA,EAAW,GAAA;EAAG;EAEd,WAAA;EARA;EAUA,YAAA;EARA;EAUA,KAAA,GAAQ,EAAA;EAVW;EAYnB,SAAA;EAVc;EAYd,aAAA;EAVA;EAYA,QAAA,GAAW,EAAA,UAAY,OAAA;IAAY,QAAA;IAAoB,SAAA;EAAA;EAN/C;EAQR,UAAA,GAAa,EAAA;EAJb;EAMA,SAAA,GAAY,EAAA,UAAY,QAAA;AAAA;;AAAiB;AA4H3C;iBAAgB,QAAA,CAAS,OAAA,GAAS,eAAoB;;;;;;;;;iBAgEtC,eAAA;;;;;;;;;;;UCvOC,YAAA;EDYS;ECVxB,KAAA,EAAO,GAAA;EDUc;ECRrB,MAAA,EAAQ,GAAA;EDYR;ECVA,SAAA,EAAW,GAAA;EDUT;ECRF,IAAA,GAAO,aAAA;EDWoB;ECT3B,MAAA;EDWW;ECTX,KAAA;EDWU;ECTV,oBAAA,QAA4B,OAAA;AAAA;;;;iBAwEd,MAAA,IAAU,YAAY"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { A as SCREEN_READER_KEY, S as useStreamsContext, i as lastKeyEvent, j as isScreenReaderEnabledByDefault, n as lastCompositionEvent, o as lastPasteEvent } from "./app-D_Ke7C8w.mjs";
|
|
2
|
+
import { inject, isRef, onUnmounted, ref, watch } from "@vue/runtime-core";
|
|
3
|
+
//#region src/composables/useIsScreenReaderEnabled.ts
|
|
4
|
+
/**
|
|
5
|
+
* useIsScreenReaderEnabled - screen reader mode flag.
|
|
6
|
+
*/
|
|
7
|
+
function useIsScreenReaderEnabled() {
|
|
8
|
+
return inject(SCREEN_READER_KEY, null)?.value ?? isScreenReaderEnabledByDefault();
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/composables/usePaste.ts
|
|
12
|
+
/**
|
|
13
|
+
* usePaste - bracketed paste handling.
|
|
14
|
+
*/
|
|
15
|
+
let activePasteHandlerCount = 0;
|
|
16
|
+
function hasActivePasteHandlers() {
|
|
17
|
+
return activePasteHandlerCount > 0;
|
|
18
|
+
}
|
|
19
|
+
function toRef$1(value) {
|
|
20
|
+
return isRef(value) ? value : ref(value);
|
|
21
|
+
}
|
|
22
|
+
function usePaste(handler, options = {}) {
|
|
23
|
+
const isActive = toRef$1(options.isActive ?? true);
|
|
24
|
+
const streams = useStreamsContext();
|
|
25
|
+
let rawModeEnabled = false;
|
|
26
|
+
let bracketedPasteEnabled = false;
|
|
27
|
+
let pasteHandlerRegistered = false;
|
|
28
|
+
const syncRawMode = (isEnabled) => {
|
|
29
|
+
if (rawModeEnabled === isEnabled) return;
|
|
30
|
+
streams.setRawMode(isEnabled);
|
|
31
|
+
rawModeEnabled = isEnabled;
|
|
32
|
+
};
|
|
33
|
+
const syncBracketedPasteMode = (isEnabled) => {
|
|
34
|
+
if (bracketedPasteEnabled === isEnabled) return;
|
|
35
|
+
streams.setBracketedPasteMode(isEnabled);
|
|
36
|
+
bracketedPasteEnabled = isEnabled;
|
|
37
|
+
};
|
|
38
|
+
const syncPasteRegistration = (isEnabled) => {
|
|
39
|
+
if (pasteHandlerRegistered === isEnabled) return;
|
|
40
|
+
activePasteHandlerCount += isEnabled ? 1 : -1;
|
|
41
|
+
pasteHandlerRegistered = isEnabled;
|
|
42
|
+
};
|
|
43
|
+
const syncActiveState = (isEnabled) => {
|
|
44
|
+
syncRawMode(isEnabled);
|
|
45
|
+
syncBracketedPasteMode(isEnabled);
|
|
46
|
+
syncPasteRegistration(isEnabled);
|
|
47
|
+
};
|
|
48
|
+
watch(lastPasteEvent, (event) => {
|
|
49
|
+
if (!event || !isActive.value) return;
|
|
50
|
+
handler(event.text);
|
|
51
|
+
});
|
|
52
|
+
watch(isActive, syncActiveState, { immediate: true });
|
|
53
|
+
onUnmounted(() => syncActiveState(false));
|
|
54
|
+
return {
|
|
55
|
+
isActive,
|
|
56
|
+
enable: () => {
|
|
57
|
+
isActive.value = true;
|
|
58
|
+
},
|
|
59
|
+
disable: () => {
|
|
60
|
+
isActive.value = false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/composables/useInput.ts
|
|
66
|
+
/**
|
|
67
|
+
* useInput - Input handling composable
|
|
68
|
+
*/
|
|
69
|
+
function toRef(value) {
|
|
70
|
+
return isRef(value) ? value : ref(value);
|
|
71
|
+
}
|
|
72
|
+
function keyName(event) {
|
|
73
|
+
return event.char ?? event.key ?? "";
|
|
74
|
+
}
|
|
75
|
+
function toInkKey(event) {
|
|
76
|
+
const key = event.key;
|
|
77
|
+
return {
|
|
78
|
+
upArrow: key === "up",
|
|
79
|
+
downArrow: key === "down",
|
|
80
|
+
leftArrow: key === "left",
|
|
81
|
+
rightArrow: key === "right",
|
|
82
|
+
pageDown: key === "pagedown" || key === "pageDown",
|
|
83
|
+
pageUp: key === "pageup" || key === "pageUp",
|
|
84
|
+
home: key === "home",
|
|
85
|
+
end: key === "end",
|
|
86
|
+
return: key === "enter" || key === "return",
|
|
87
|
+
escape: key === "escape" || key === "esc",
|
|
88
|
+
ctrl: event.ctrl,
|
|
89
|
+
shift: event.shift,
|
|
90
|
+
tab: key === "tab" || key === "backtab",
|
|
91
|
+
backspace: key === "backspace",
|
|
92
|
+
delete: key === "delete",
|
|
93
|
+
meta: event.meta,
|
|
94
|
+
super: event.super,
|
|
95
|
+
hyper: event.hyper,
|
|
96
|
+
capsLock: event.capsLock,
|
|
97
|
+
numLock: event.numLock,
|
|
98
|
+
eventType: event.eventType
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function emptyKey() {
|
|
102
|
+
return {
|
|
103
|
+
upArrow: false,
|
|
104
|
+
downArrow: false,
|
|
105
|
+
leftArrow: false,
|
|
106
|
+
rightArrow: false,
|
|
107
|
+
pageDown: false,
|
|
108
|
+
pageUp: false,
|
|
109
|
+
home: false,
|
|
110
|
+
end: false,
|
|
111
|
+
return: false,
|
|
112
|
+
escape: false,
|
|
113
|
+
ctrl: false,
|
|
114
|
+
shift: false,
|
|
115
|
+
tab: false,
|
|
116
|
+
backspace: false,
|
|
117
|
+
delete: false,
|
|
118
|
+
meta: false,
|
|
119
|
+
super: false,
|
|
120
|
+
hyper: false,
|
|
121
|
+
capsLock: false,
|
|
122
|
+
numLock: false
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function inputValue(event, key) {
|
|
126
|
+
if (event.char) return event.char;
|
|
127
|
+
if (key.return) return "\r";
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
function handleStructuredOptions(event, options, lastKey) {
|
|
131
|
+
const modifiers = {
|
|
132
|
+
ctrl: event.ctrl,
|
|
133
|
+
alt: event.alt,
|
|
134
|
+
shift: event.shift,
|
|
135
|
+
meta: event.meta
|
|
136
|
+
};
|
|
137
|
+
const pressedKey = keyName(event);
|
|
138
|
+
const inkKey = toInkKey(event);
|
|
139
|
+
if (event.char) {
|
|
140
|
+
lastKey.value = event.char;
|
|
141
|
+
options.onChar?.(event.char);
|
|
142
|
+
options.onKey?.(event.char, modifiers);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (pressedKey) {
|
|
146
|
+
lastKey.value = pressedKey;
|
|
147
|
+
options.onKey?.(pressedKey, modifiers);
|
|
148
|
+
}
|
|
149
|
+
if (inkKey.return) options.onSubmit?.();
|
|
150
|
+
if (inkKey.escape) options.onEscape?.();
|
|
151
|
+
if (inkKey.upArrow) options.onArrow?.("up");
|
|
152
|
+
if (inkKey.downArrow) options.onArrow?.("down");
|
|
153
|
+
if (inkKey.leftArrow) options.onArrow?.("left");
|
|
154
|
+
if (inkKey.rightArrow) options.onArrow?.("right");
|
|
155
|
+
}
|
|
156
|
+
function useInput(handlerOrOptions = {}, handlerOptions = {}) {
|
|
157
|
+
const options = typeof handlerOrOptions === "function" ? {
|
|
158
|
+
handler: handlerOrOptions,
|
|
159
|
+
isActive: handlerOptions.isActive
|
|
160
|
+
} : handlerOrOptions;
|
|
161
|
+
const isActive = toRef(options.isActive ?? options.active ?? true);
|
|
162
|
+
const lastKey = ref(null);
|
|
163
|
+
const streams = useStreamsContext();
|
|
164
|
+
let rawModeEnabled = false;
|
|
165
|
+
const syncRawMode = (isEnabled) => {
|
|
166
|
+
if (rawModeEnabled === isEnabled) return;
|
|
167
|
+
streams.setRawMode(isEnabled);
|
|
168
|
+
rawModeEnabled = isEnabled;
|
|
169
|
+
};
|
|
170
|
+
watch(lastKeyEvent, (event) => {
|
|
171
|
+
if (!event || !isActive.value) return;
|
|
172
|
+
const inkKey = toInkKey(event);
|
|
173
|
+
const input = inputValue(event, inkKey);
|
|
174
|
+
const pressedKey = keyName(event);
|
|
175
|
+
if (input === "c" && inkKey.ctrl && streams.internal_exitOnCtrlC) return;
|
|
176
|
+
lastKey.value = pressedKey || null;
|
|
177
|
+
options.handler?.(input, inkKey);
|
|
178
|
+
handleStructuredOptions(event, options, lastKey);
|
|
179
|
+
});
|
|
180
|
+
watch(lastPasteEvent, (event) => {
|
|
181
|
+
if (!event || !isActive.value || hasActivePasteHandlers()) return;
|
|
182
|
+
lastKey.value = event.text;
|
|
183
|
+
options.handler?.(event.text, emptyKey());
|
|
184
|
+
options.onChar?.(event.text);
|
|
185
|
+
});
|
|
186
|
+
watch(lastCompositionEvent, (event) => {
|
|
187
|
+
if (!event || !isActive.value) return;
|
|
188
|
+
if (event.type === "compositionstart") options.onCompositionStart?.();
|
|
189
|
+
else if (event.type === "compositionupdate") options.onCompositionUpdate?.(event.text, event.cursor);
|
|
190
|
+
else options.onCompositionEnd?.(event.text);
|
|
191
|
+
});
|
|
192
|
+
watch(isActive, syncRawMode, { immediate: true });
|
|
193
|
+
onUnmounted(() => syncRawMode(false));
|
|
194
|
+
const enable = () => {
|
|
195
|
+
isActive.value = true;
|
|
196
|
+
};
|
|
197
|
+
const disable = () => {
|
|
198
|
+
isActive.value = false;
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
isActive,
|
|
202
|
+
lastKey,
|
|
203
|
+
enable,
|
|
204
|
+
disable
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Shorthand for handling specific key combinations
|
|
209
|
+
*/
|
|
210
|
+
function useKeyPress(key, handler, options = {}) {
|
|
211
|
+
const { ctrl = false, alt = false, shift = false, meta = false } = options;
|
|
212
|
+
useInput({ onKey: (pressedKey, modifiers) => {
|
|
213
|
+
if (pressedKey.toLowerCase() === key.toLowerCase() && modifiers.ctrl === ctrl && modifiers.alt === alt && modifiers.shift === shift && modifiers.meta === meta) handler();
|
|
214
|
+
} });
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { useIsScreenReaderEnabled as i, useKeyPress as n, usePaste as r, useInput as t };
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=useInput-BpH-JIU3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInput-BpH-JIU3.mjs","names":["toRef"],"sources":["../src/composables/useIsScreenReaderEnabled.ts","../src/composables/usePaste.ts","../src/composables/useInput.ts"],"sourcesContent":["/**\n * useIsScreenReaderEnabled - screen reader mode flag.\n */\n\nimport { inject } from \"@vue/runtime-core\";\nimport { SCREEN_READER_KEY, isScreenReaderEnabledByDefault } from \"../accessibility.js\";\n\nexport function useIsScreenReaderEnabled(): boolean {\n const enabled = inject(SCREEN_READER_KEY, null);\n return enabled?.value ?? isScreenReaderEnabledByDefault();\n}\n","/**\n * usePaste - bracketed paste handling.\n */\n\nimport { isRef, onUnmounted, ref, watch, type Ref } from \"@vue/runtime-core\";\nimport { lastPasteEvent } from \"../app.js\";\nimport { useStreamsContext } from \"./useStreams.js\";\n\nlet activePasteHandlerCount = 0;\n\nexport interface UsePasteOptions {\n /** Whether the paste handler is active */\n isActive?: boolean | Ref<boolean>;\n}\n\nexport function hasActivePasteHandlers(): boolean {\n return activePasteHandlerCount > 0;\n}\n\nfunction toRef(value: boolean | Ref<boolean>): Ref<boolean> {\n return isRef(value) ? value : ref(value);\n}\n\nexport function usePaste(handler: (text: string) => void, options: UsePasteOptions = {}) {\n const isActive = toRef(options.isActive ?? true);\n const streams = useStreamsContext();\n let rawModeEnabled = false;\n let bracketedPasteEnabled = false;\n let pasteHandlerRegistered = false;\n\n const syncRawMode = (isEnabled: boolean) => {\n if (rawModeEnabled === isEnabled) return;\n streams.setRawMode(isEnabled);\n rawModeEnabled = isEnabled;\n };\n\n const syncBracketedPasteMode = (isEnabled: boolean) => {\n if (bracketedPasteEnabled === isEnabled) return;\n streams.setBracketedPasteMode(isEnabled);\n bracketedPasteEnabled = isEnabled;\n };\n\n const syncPasteRegistration = (isEnabled: boolean) => {\n if (pasteHandlerRegistered === isEnabled) return;\n activePasteHandlerCount += isEnabled ? 1 : -1;\n pasteHandlerRegistered = isEnabled;\n };\n\n const syncActiveState = (isEnabled: boolean) => {\n syncRawMode(isEnabled);\n syncBracketedPasteMode(isEnabled);\n syncPasteRegistration(isEnabled);\n };\n\n watch(lastPasteEvent, (event) => {\n if (!event || !isActive.value) return;\n handler(event.text);\n });\n\n watch(isActive, syncActiveState, { immediate: true });\n onUnmounted(() => syncActiveState(false));\n\n return {\n isActive,\n enable: () => {\n isActive.value = true;\n },\n disable: () => {\n isActive.value = false;\n },\n };\n}\n","/**\n * useInput - Input handling composable\n */\n\nimport { isRef, onUnmounted, ref, watch, type Ref } from \"@vue/runtime-core\";\nimport { lastCompositionEvent, lastKeyEvent, lastPasteEvent, type KeyEvent } from \"../app.js\";\nimport { hasActivePasteHandlers } from \"./usePaste.js\";\nimport { useStreamsContext } from \"./useStreams.js\";\n\nexport interface Key {\n upArrow: boolean;\n downArrow: boolean;\n leftArrow: boolean;\n rightArrow: boolean;\n pageDown: boolean;\n pageUp: boolean;\n home: boolean;\n end: boolean;\n return: boolean;\n escape: boolean;\n ctrl: boolean;\n shift: boolean;\n tab: boolean;\n backspace: boolean;\n delete: boolean;\n meta: boolean;\n super: boolean;\n hyper: boolean;\n capsLock: boolean;\n numLock: boolean;\n eventType?: \"press\" | \"repeat\" | \"release\";\n}\n\nexport interface KeyHandler {\n (key: string, modifiers: { ctrl: boolean; alt: boolean; shift: boolean; meta: boolean }): void;\n}\n\nexport interface UseInputOptions {\n /** Whether to capture input (boolean or Ref<boolean>) */\n active?: boolean | Ref<boolean>;\n /** Whether to capture input (alias for active, boolean or Ref<boolean>) */\n isActive?: boolean | Ref<boolean>;\n /** Ink-compatible input callback */\n handler?: (input: string, key: Key) => void;\n /** Called on key press */\n onKey?: KeyHandler;\n /** Called on character input */\n onChar?: (char: string) => void;\n /** Called on Enter */\n onSubmit?: () => void;\n /** Called on Escape */\n onEscape?: () => void;\n /** Called on arrow keys */\n onArrow?: (direction: \"up\" | \"down\" | \"left\" | \"right\") => void;\n /** Called when an IME composition starts */\n onCompositionStart?: () => void;\n /** Called when IME preedit text changes */\n onCompositionUpdate?: (text: string, cursor: number) => void;\n /** Called when IME commits text */\n onCompositionEnd?: (text: string) => void;\n}\n\nexport interface UseInputReturn {\n isActive: Ref<boolean>;\n lastKey: Ref<string | null>;\n enable: () => void;\n disable: () => void;\n}\n\nexport type InputHandler = (input: string, key: Key) => void;\n\nexport interface UseInputHandlerOptions {\n isActive?: boolean | Ref<boolean>;\n}\n\nfunction toRef(value: boolean | Ref<boolean>): Ref<boolean> {\n return isRef(value) ? value : ref(value);\n}\n\nfunction keyName(event: KeyEvent): string {\n return event.char ?? event.key ?? \"\";\n}\n\nfunction toInkKey(event: KeyEvent): Key {\n const key = event.key;\n\n return {\n upArrow: key === \"up\",\n downArrow: key === \"down\",\n leftArrow: key === \"left\",\n rightArrow: key === \"right\",\n pageDown: key === \"pagedown\" || key === \"pageDown\",\n pageUp: key === \"pageup\" || key === \"pageUp\",\n home: key === \"home\",\n end: key === \"end\",\n return: key === \"enter\" || key === \"return\",\n escape: key === \"escape\" || key === \"esc\",\n ctrl: event.ctrl,\n shift: event.shift,\n tab: key === \"tab\" || key === \"backtab\",\n backspace: key === \"backspace\",\n delete: key === \"delete\",\n meta: event.meta,\n super: event.super,\n hyper: event.hyper,\n capsLock: event.capsLock,\n numLock: event.numLock,\n eventType: event.eventType,\n };\n}\n\nfunction emptyKey(): Key {\n return {\n upArrow: false,\n downArrow: false,\n leftArrow: false,\n rightArrow: false,\n pageDown: false,\n pageUp: false,\n home: false,\n end: false,\n return: false,\n escape: false,\n ctrl: false,\n shift: false,\n tab: false,\n backspace: false,\n delete: false,\n meta: false,\n super: false,\n hyper: false,\n capsLock: false,\n numLock: false,\n };\n}\n\nfunction inputValue(event: KeyEvent, key: Key): string {\n if (event.char) return event.char;\n if (key.return) return \"\\r\";\n return \"\";\n}\n\nfunction handleStructuredOptions(\n event: KeyEvent,\n options: UseInputOptions,\n lastKey: Ref<string | null>,\n) {\n const modifiers = {\n ctrl: event.ctrl,\n alt: event.alt,\n shift: event.shift,\n meta: event.meta,\n };\n const pressedKey = keyName(event);\n const inkKey = toInkKey(event);\n\n if (event.char) {\n lastKey.value = event.char;\n options.onChar?.(event.char);\n options.onKey?.(event.char, modifiers);\n return;\n }\n\n if (pressedKey) {\n lastKey.value = pressedKey;\n options.onKey?.(pressedKey, modifiers);\n }\n\n if (inkKey.return) options.onSubmit?.();\n if (inkKey.escape) options.onEscape?.();\n if (inkKey.upArrow) options.onArrow?.(\"up\");\n if (inkKey.downArrow) options.onArrow?.(\"down\");\n if (inkKey.leftArrow) options.onArrow?.(\"left\");\n if (inkKey.rightArrow) options.onArrow?.(\"right\");\n}\n\nexport function useInput(handler: InputHandler, options?: UseInputHandlerOptions): UseInputReturn;\nexport function useInput(options?: UseInputOptions): UseInputReturn;\nexport function useInput(\n handlerOrOptions: InputHandler | UseInputOptions = {},\n handlerOptions: UseInputHandlerOptions = {},\n): UseInputReturn {\n const options =\n typeof handlerOrOptions === \"function\"\n ? { handler: handlerOrOptions, isActive: handlerOptions.isActive }\n : handlerOrOptions;\n\n const activeSource = options.isActive ?? options.active ?? true;\n const isActive = toRef(activeSource);\n const lastKey = ref<string | null>(null);\n const streams = useStreamsContext();\n let rawModeEnabled = false;\n\n const syncRawMode = (isEnabled: boolean) => {\n if (rawModeEnabled === isEnabled) return;\n streams.setRawMode(isEnabled);\n rawModeEnabled = isEnabled;\n };\n\n watch(lastKeyEvent, (event) => {\n if (!event || !isActive.value) return;\n\n const inkKey = toInkKey(event);\n const input = inputValue(event, inkKey);\n const pressedKey = keyName(event);\n\n if (input === \"c\" && inkKey.ctrl && streams.internal_exitOnCtrlC) return;\n\n lastKey.value = pressedKey || null;\n options.handler?.(input, inkKey);\n handleStructuredOptions(event, options, lastKey);\n });\n\n watch(lastPasteEvent, (event) => {\n if (!event || !isActive.value || hasActivePasteHandlers()) return;\n\n lastKey.value = event.text;\n options.handler?.(event.text, emptyKey());\n options.onChar?.(event.text);\n });\n\n watch(lastCompositionEvent, (event) => {\n if (!event || !isActive.value) return;\n\n if (event.type === \"compositionstart\") {\n options.onCompositionStart?.();\n } else if (event.type === \"compositionupdate\") {\n options.onCompositionUpdate?.(event.text, event.cursor);\n } else {\n options.onCompositionEnd?.(event.text);\n }\n });\n\n watch(isActive, syncRawMode, { immediate: true });\n onUnmounted(() => syncRawMode(false));\n\n const enable = () => {\n isActive.value = true;\n };\n\n const disable = () => {\n isActive.value = false;\n };\n\n return {\n isActive,\n lastKey,\n enable,\n disable,\n };\n}\n\n/**\n * Shorthand for handling specific key combinations\n */\nexport function useKeyPress(\n key: string,\n handler: () => void,\n options: { ctrl?: boolean; alt?: boolean; shift?: boolean; meta?: boolean } = {},\n) {\n const { ctrl = false, alt = false, shift = false, meta = false } = options;\n\n useInput({\n onKey: (pressedKey, modifiers) => {\n const matches =\n pressedKey.toLowerCase() === key.toLowerCase() &&\n modifiers.ctrl === ctrl &&\n modifiers.alt === alt &&\n modifiers.shift === shift &&\n modifiers.meta === meta;\n\n if (matches) {\n handler();\n }\n },\n });\n}\n"],"mappings":"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"}
|