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>
7.4 KiB
vue-tui — Intentional Divergences from Ink
vue-tui started as a Vue 3 port of Ink, and it still tracks Ink closely — the aim is behavioral parity except where a difference is deliberate. But it is no longer just a port: it has grown its own design decisions, additive features, and Vue-native choices. This document records the places vue-tui intentionally differs from Ink — by design, not as a gap to fix. A difference that is not listed here is treated as a bug (or simply unverified), not a design choice.
Reference baseline: Ink v7.0.4 (commit
40b3a7578811fd616341ca4e31cc7748aeeff12f). When bumping the target Ink version, re-validate every entry below against the new source.
How to read this
Each entry states what Ink does, what vue-tui does, and why the divergence is deliberate. Divergences fall into a few kinds:
- API surface — public API renamed/reshaped to fit Vue idioms.
- Additive — vue-tui supports something Ink doesn't (a strict superset).
- Framework semantics — a consequence of Vue ≠ React that cannot be papered over.
- N/A — a React-only concept with no Vue equivalent.
Public API surface
Entry point — createApp() instead of render()
- Ink:
render(<App/>). - vue-tui:
createApp(App).mount(options). - Why: mirrors Vue's own
createAppmental model — a Vue developer expects an app object they mount, not a one-shot render call.
App composable — useAppContext() instead of useApp()
- Ink:
useApp()returns{ exit, waitUntilRenderFlush }— stdin/stdout/stderr are separate hooks (useStdin/useStdout/useStderr), not part of it. - vue-tui:
useAppContext()returns the same{ exit, waitUntilRenderFlush }, with streams on those same peer composables. - Why: only the name differs —
useAppreads as "the Vue application instance" (createApp,app.mount), so vue-tui qualifies it asuseAppContext. The shape is identical to Ink; a naming divergence, not a surface one.
Named type / prop re-exports
- Ink: re-exports its component prop types plus a few data/handle types:
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize,CursorPosition,DOMElement,RenderOptions,Instance,AppProps,StdinProps,StdoutProps,StderrProps. - vue-tui: re-exports the framework-neutral ones under the same names —
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize({ columns, rows }) andCursorPosition({ x, y }). These are not divergences: a<Box>has props in Vue exactly as in React, so the names carry over. They are derived from the runtimepropsobjects via Vue'sExtractPublicPropTypes, so they never drift from the components' real props. Only the remaining few genuinely differ, each for a concrete reason — never merely to "avoid React-shaped names":DOMElement→TuiNode. The one genuinely DOM-shaped type: Ink'sDOMElementmodels a DOM-emulation node (nodeName/attributes/childNodes). vue-tui's host tree is a different representation (TuiContainer | TuiTextLeaf | TuiComment), exported asTuiNodefrom@vue-tui/runtime/internal.RenderOptions/Instance→MountOptions/TuiApp. Downstream of thecreateApp()entry above — vue-tui mounts a Vue app, so the options bag and the returned handle are Vue-shaped, notrender()-shaped.AppProps/StdinProps/StdoutProps/StderrProps→ N/A. These are the props of Ink's internal React context-provider components (<AppContext>,<StdinContext>, …). vue-tui has no such components — that state is reached viacreateAppplus theuseStdin/useStdout/useStderrcomposables — so there is nothing to name.
- Why: the earlier blanket "expose a Vue-native type surface, don't leak
React-shaped names" over-reached — it withheld names like
BoxPropsthat have no React vs Vue content at all. The rule is narrower: mirror Ink's names wherever the underlying type is framework-neutral; reshape only where Vue genuinely has a different thing (a host node, a mounted app) or no thing at all.
Additive features (vue-tui is a strict superset)
Accessibility props — AriaRole / AriaState
- Ink: no equivalent.
- vue-tui:
<Box>/<Text>acceptAriaRole/AriaStateprops that feed the screen-reader linearization. - Why: additive accessibility surface; does not change visual (non-SR) rendering.
Multiple <Static> regions
- Ink: keeps a single
staticNode; only one<Static>is honored. - vue-tui:
findStatics(root)renders every<Static>in the tree. - Why: strictly more capable — a tree with two
<Static>regions both render. Maintainer decision (2026-05-30): KEEP.
Ctrl+C exits under the kitty protocol too
- Ink: wires
exitOnCtrlConly to the legacy\x03path, so a kitty-protocol Ctrl+C (\x1b[99;5u) does not exit. - vue-tui:
useInputexits oninput === 'c' && key.ctrl, gated onexitOnCtrlC(defaulttrue), so Ctrl+C exits under both legacy and kitty protocols. - Why:
exitOnCtrlCreliably exiting under all protocols is the intended behavior; opt out withexitOnCtrlC: false. Maintainer decision (2026-05-30): KEEP.
Framework-semantic divergences (Vue ≠ React)
Removing flexDirection / flexWrap resets to the default
- Ink:
applyFlexStyleshas noundefinedbranch forflexDirection/flexWrap, so an explicitflexDirection={undefined}leaves the yoga node's stale value in place. (For an omitted prop, Ink's<Box>re-applies itsrow/nowrapdefault before the style spread.) - vue-tui: resets
flexDirection/flexWrapto the Box default (row/nowrap) when the prop is removed across renders. - Why: Vue cannot distinguish an omitted prop from an explicit
undefined— both collapse to the prop's default — so vue-tui must pick one behavior. It matches Ink's common case (omitted →row/nowrap) and the Vue-idiomatic expectation (drop the override → get the default). The residual difference (explicit={undefined}→ vue-tui resets, Ink keeps stale) is an unavoidable Vue-vs-React semantic. Maintainer decision (2026-05-30): KEEP.
Other Vue-vs-React semantics (placeholder)
- (maintainer: candidates to document —
v-if/nullrendering as comment host nodes; reactivity-driven re-render timing vs React's render cycle; keyed reconciliation order. Add the ones that are genuinely by-design.)
Not applicable in Vue
React concurrent mode
- Ink: built on React; Suspense /
useTransitionare React features. - vue-tui: no equivalent — N/A, not a gap.
Framework idioms (noted, not behavioral divergences)
Surface conventions, listed so they aren't mistaken for gaps:
- Vue composables (
useFocus,useInput, …) instead of React hooks. <script setup>SFCs /defineComponentinstead of function components.- kebab-case filenames;
.tsover.tsxwhere there's no JSX. shallowRefby default for reactive state.
Maintainer additions
Space for divergences to add or refine. For each, capture: what Ink does, what vue-tui does, and why it's deliberate (the trade-off, not just the what).