9d1e4d7805
Ink's `useApp()` returns `{ exit, waitUntilRenderFlush }`. vue-tui previously
exposed only `exit()` via `useExit()` and kept `waitUntilRenderFlush` on the
`TuiApp` handle alone. Align the public surface with Ink: add `useAppContext()`
returning the same pair, and remove `useExit()`.
- thread `waitUntilRenderFlush` into the injected `AppContext` via a hoisted
impl shared by the `TuiApp` handle and the composable, so both resolve
identically
- add `useAppContext()`; delete `useExit()`; migrate all call sites, PTY
fixtures, examples, READMEs and the public-API surface test
- port Ink's two "useApp waitUntilRenderFlush" tests; Ink's third relies on
React concurrent mode (N/A in Vue)
- rewrite the ink-divergences entry: this is now a *naming* divergence
(`useAppContext` vs `useApp`, mirroring `createApp` vs `render`), not a
surface one — and fix the prior wrong claim that Ink's `useApp` returns
stdin/stdout/stderr
The name is qualified (`useAppContext`, not `useApp`) so it doesn't read as the
Vue application instance (`createApp`/`app.mount`) — the same Vue-native naming
choice vue-tui already makes with `createApp()` vs Ink's `render()`.
BREAKING CHANGE: `useExit()` is removed. Replace `const exit = useExit()` with
`const { exit } = useAppContext()`.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
3.8 KiB
TypeScript
146 lines
3.8 KiB
TypeScript
import process from "node:process";
|
|
import { Box, Static, Text, createApp, useAppContext } from "@vue-tui/runtime";
|
|
import { Fragment, defineComponent, h, onMounted, onScopeDispose, shallowRef, watch } from "vue";
|
|
|
|
type RerenderFixtureOptions = {
|
|
readonly completionMarker?: string;
|
|
readonly frameLimit?: number;
|
|
readonly includeStaticLine?: boolean;
|
|
readonly rowsFallback?: number;
|
|
readonly heightForFrame: (rows: number, frameCount: number) => number;
|
|
};
|
|
|
|
const Issue450RerenderFixtureComponent = defineComponent(
|
|
(props: {
|
|
completionMarker?: string;
|
|
frameLimit: number;
|
|
includeStaticLine: boolean;
|
|
heightForFrame: (rows: number, frameCount: number) => number;
|
|
rows: number;
|
|
}) => {
|
|
const { exit } = useAppContext();
|
|
const frameCount = shallowRef(0);
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
watch(
|
|
frameCount,
|
|
(count) => {
|
|
clearTimeout(timer);
|
|
|
|
if (count >= props.frameLimit) {
|
|
timer = setTimeout(() => {
|
|
if (props.completionMarker) {
|
|
process.stdout.write(props.completionMarker);
|
|
}
|
|
|
|
exit();
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
timer = setTimeout(() => {
|
|
frameCount.value++;
|
|
}, 100);
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
onScopeDispose(() => {
|
|
clearTimeout(timer);
|
|
});
|
|
|
|
return () => {
|
|
const targetHeight = props.heightForFrame(props.rows, frameCount.value);
|
|
|
|
return h(Fragment, [
|
|
props.includeStaticLine
|
|
? h(
|
|
Static,
|
|
{ items: ["#450 static line"] },
|
|
{
|
|
default: ({ item }: { item: string }) => h(Text, { key: item }, () => item),
|
|
},
|
|
)
|
|
: null,
|
|
h(Box, { height: targetHeight, flexDirection: "column" }, () => [
|
|
h(Text, null, () => "#450 top"),
|
|
h(Box, { flexGrow: 1 }, () => h(Text, null, () => `frame ${frameCount.value}`)),
|
|
h(Text, null, () => "#450 bottom"),
|
|
]),
|
|
]);
|
|
};
|
|
},
|
|
{ props: ["completionMarker", "frameLimit", "includeStaticLine", "heightForFrame", "rows"] },
|
|
);
|
|
|
|
export const runIssue450RerenderFixture = ({
|
|
completionMarker,
|
|
frameLimit = 8,
|
|
includeStaticLine = false,
|
|
rowsFallback = 6,
|
|
heightForFrame,
|
|
}: RerenderFixtureOptions): void => {
|
|
const rows = Number(process.argv[2]) || rowsFallback;
|
|
process.stdout.rows = rows;
|
|
|
|
const app = createApp(Issue450RerenderFixtureComponent, {
|
|
completionMarker,
|
|
frameLimit,
|
|
includeStaticLine,
|
|
heightForFrame,
|
|
rows,
|
|
});
|
|
app.mount();
|
|
};
|
|
|
|
type InitialFixtureOptions = {
|
|
readonly rowsFallback?: number;
|
|
readonly renderedMarker: string;
|
|
readonly lineCount: number;
|
|
readonly linePrefix: string;
|
|
};
|
|
|
|
const Issue450InitialFixtureComponent = defineComponent(
|
|
(props: { renderedMarker: string; lineCount: number; linePrefix: string }) => {
|
|
const { exit } = useAppContext();
|
|
|
|
onMounted(() => {
|
|
const timer = setTimeout(() => {
|
|
process.stdout.write(props.renderedMarker);
|
|
exit();
|
|
}, 0);
|
|
|
|
onScopeDispose(() => {
|
|
clearTimeout(timer);
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
const lines: ReturnType<typeof h>[] = [];
|
|
for (let lineNumber = 1; lineNumber <= props.lineCount; lineNumber++) {
|
|
lines.push(h(Text, { key: lineNumber }, () => `${props.linePrefix} line ${lineNumber}`));
|
|
}
|
|
|
|
return h(Box, { flexDirection: "column" }, () => lines);
|
|
};
|
|
},
|
|
{ props: ["renderedMarker", "lineCount", "linePrefix"] },
|
|
);
|
|
|
|
export const runIssue450InitialFixture = ({
|
|
rowsFallback = 3,
|
|
renderedMarker,
|
|
lineCount,
|
|
linePrefix,
|
|
}: InitialFixtureOptions): void => {
|
|
const rows = Number(process.argv[2]) || rowsFallback;
|
|
process.stdout.rows = rows;
|
|
|
|
const app = createApp(Issue450InitialFixtureComponent, {
|
|
renderedMarker,
|
|
lineCount,
|
|
linePrefix,
|
|
});
|
|
app.mount();
|
|
};
|