fix(runtime): unmount written <Static> items to match Ink (G01) (#34)
* fix(runtime): unmount written <Static> items to match Ink (G01) Ink's <Static> renders `items.slice(index)` and advances `index` to `items.length` in a post-commit `useLayoutEffect`, so once an item has been painted it is removed from the tree and its component unmounts. vue-tui kept every Static item mounted forever: the component always mapped the full `props.items`, and write-once was enforced only at flush time via a positional `writtenCount` slice — the item components never tore down. Now the <Static> component owns a `cursor` (Ink's `index`) and renders only `items.slice(cursor)`. The renderer advances the cursor AFTER a commit has painted the fresh items, via an `onWritten` callback registered on the host static node — the vue-tui analogue of Ink's post-commit layout effect. This ordering guarantees items are written before they are sliced out and unmounted, so no item is ever lost or re-painted. Write-once bookkeeping moved from a positional `writtenCount` to a `writtenNodes` Set keyed by host-node identity. A single logical item expands to several host nodes (the <Text>/<Box> plus empty text-leaf fragment anchors Vue inserts), so a positional count mis-sliced once the cursor advanced; identity tracking is anchor-agnostic. The shared `paintStaticNode` helper paints children not yet in the set, records them, prunes unmounted entries, then fires `onWritten`; render.ts, render-to-string.ts and flushStatic all use it. Make the cursor mirror Ink fully so it can DECREASE, not just increase. `onWritten` now SETS the cursor to items.length (was max-with-current), and a length watch lowers it on shrink — needed because a shrink that leaves the already-sliced children empty produces no host mutation, hence no commit/ onWritten to re-sync. Without this, [A,B] (cursor→2) → [A] → [A,C] sliced(2)=[] and silently dropped C. paintStaticNode now always prunes and calls onWritten (even on empty commits), painting only when there are fresh children. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(parity): ledger — G01 pr-open, reconcile G12 merged Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,24 +29,24 @@ Non-obvious calls made while fixing gaps, recorded for review in the final repor
|
||||
|
||||
`status` ∈ `todo · in-progress · pr-open · merged · blocked · refuted`. Priority: correctness/behavior first, omissions next.
|
||||
|
||||
| id | area | summary | priority | status | branch | PR |
|
||||
| --- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -------------------------- | --- |
|
||||
| G01 | static-newline-spacer | Static keeps every already-written item permanently mounted instead of unmounting it | P1 | todo | — | — |
|
||||
| G02 | app-exit-instances-animation-sr | useAnimation does not coalesce ticks within the render-throttle window — delta does not 'account for throttled renders' | P1 | todo | — | — |
|
||||
| G03 | render-lifecycle-reconciler | Live screen-reader render path is missing; commit() always paints the visual grid | P1 | todo | — | — |
|
||||
| G04 | box-layout-border | Border edges incorrectly inherit the Box backgroundColor | P2 | merged | `fix/parity-border-bg` | #30 |
|
||||
| G05 | box-layout-border | Borders skipped when content area is 1 cell tall or wide (w<2 / h<2 guard) | P2 | todo | — | — |
|
||||
| G06 | text-wrap-transform | Nested <Transform>/<Text> transform fn receives hardcoded index 0 instead of childNode index | P2 | refuted | — | — |
|
||||
| G07 | input-keypress-kitty-paste | Kitty-protocol Ctrl+C triggers app exit in vue-tui but only suppresses the handler in Ink | P2 | todo | — | — |
|
||||
| G08 | focus | useFocus does not react to changes in the id prop | P2 | merged | `fix/parity-usefocus-id` | #31 |
|
||||
| G09 | stdout-stderr-stdin-size-cursor | External stdout/stderr writes are not wrapped in synchronized-update (BSU/ESU) markers | P2 | todo | — | — |
|
||||
| G10 | stdout-stderr-stdin-size-cursor | setRawMode silently no-ops in unsupported environments instead of throwing a descriptive error | P2 | todo | — | — |
|
||||
| G11 | render-lifecycle-reconciler | Resize handler does not clear+reset on terminal-width decrease | P2 | todo | — | — |
|
||||
| G12 | render-lifecycle-reconciler | Renderer frame width/rows lack terminal-size fallback (only ?? defaults) | P2 | pr-open | `fix/parity-renderer-size` | #33 |
|
||||
| G13 | box-layout-border | Custom border style objects (BoxStyle) not supported | P3 | todo | — | — |
|
||||
| G14 | app-exit-instances-animation-sr | No per-stdout instance reuse/guard — two concurrent renderers can compete for the same stdout | P3 | todo | — | — |
|
||||
| G15 | box-layout-border | Vertical border sides not shifted up when borderTop=false (Ink offsetY) — left/right rails mispositioned | P2 | todo | — | — |
|
||||
| G16 | box-layout-border | Per-edge borderDimColor=false cannot override general borderDimColor (`\|\| dimAll` vs Ink's `??`) | P3 | todo | — | — |
|
||||
| id | area | summary | priority | status | branch | PR |
|
||||
| --- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ------- | --------------------------- | --- |
|
||||
| G01 | static-newline-spacer | Static keeps every already-written item permanently mounted instead of unmounting it | P1 | pr-open | `fix/parity-static-unmount` | #34 |
|
||||
| G02 | app-exit-instances-animation-sr | useAnimation does not coalesce ticks within the render-throttle window — delta does not 'account for throttled renders' | P1 | todo | — | — |
|
||||
| G03 | render-lifecycle-reconciler | Live screen-reader render path is missing; commit() always paints the visual grid | P1 | todo | — | — |
|
||||
| G04 | box-layout-border | Border edges incorrectly inherit the Box backgroundColor | P2 | merged | `fix/parity-border-bg` | #30 |
|
||||
| G05 | box-layout-border | Borders skipped when content area is 1 cell tall or wide (w<2 / h<2 guard) | P2 | todo | — | — |
|
||||
| G06 | text-wrap-transform | Nested <Transform>/<Text> transform fn receives hardcoded index 0 instead of childNode index | P2 | refuted | — | — |
|
||||
| G07 | input-keypress-kitty-paste | Kitty-protocol Ctrl+C triggers app exit in vue-tui but only suppresses the handler in Ink | P2 | todo | — | — |
|
||||
| G08 | focus | useFocus does not react to changes in the id prop | P2 | merged | `fix/parity-usefocus-id` | #31 |
|
||||
| G09 | stdout-stderr-stdin-size-cursor | External stdout/stderr writes are not wrapped in synchronized-update (BSU/ESU) markers | P2 | todo | — | — |
|
||||
| G10 | stdout-stderr-stdin-size-cursor | setRawMode silently no-ops in unsupported environments instead of throwing a descriptive error | P2 | todo | — | — |
|
||||
| G11 | render-lifecycle-reconciler | Resize handler does not clear+reset on terminal-width decrease | P2 | todo | — | — |
|
||||
| G12 | render-lifecycle-reconciler | Renderer frame width/rows lack terminal-size fallback (only ?? defaults) | P2 | merged | `fix/parity-renderer-size` | #33 |
|
||||
| G13 | box-layout-border | Custom border style objects (BoxStyle) not supported | P3 | todo | — | — |
|
||||
| G14 | app-exit-instances-animation-sr | No per-stdout instance reuse/guard — two concurrent renderers can compete for the same stdout | P3 | todo | — | — |
|
||||
| G15 | box-layout-border | Vertical border sides not shifted up when borderTop=false (Ink offsetY) — left/right rails mispositioned | P2 | todo | — | — |
|
||||
| G16 | box-layout-border | Per-edge borderDimColor=false cannot override general borderDimColor (`\|\| dimAll` vs Ink's `??`) | P3 | todo | — | — |
|
||||
|
||||
## Gap details
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PassThrough } from "node:stream";
|
||||
import { defineComponent, nextTick, shallowRef } from "vue";
|
||||
import { defineComponent, nextTick, onUnmounted, shallowRef } from "vue";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { render } from "@vue-tui/testing";
|
||||
import { Box, Text, Static, createApp } from "@vue-tui/runtime";
|
||||
@@ -361,6 +361,104 @@ test("render only new items in static output on final render", async () => {
|
||||
expect(allOutput).toContain("B");
|
||||
});
|
||||
|
||||
// Ink reference: src/components/Static.tsx — `itemsToRender = items.slice(index)`
|
||||
// with `useLayoutEffect(() => setIndex(items.length))`. Once an item is written
|
||||
// (committed/painted), the effect advances `index` past it, so on the next
|
||||
// render `items.slice(index)` no longer includes it and its element is removed
|
||||
// from the tree → its component UNMOUNTS. vue-tui must match: a written Static
|
||||
// item's component must unmount (onUnmounted fires) while not-yet-written items
|
||||
// stay mounted.
|
||||
test("written Static items unmount their components (Ink parity)", async () => {
|
||||
const unmounted: string[] = [];
|
||||
|
||||
const Item = defineComponent({
|
||||
name: "StaticItem",
|
||||
props: { label: { type: String, required: true } },
|
||||
setup(props) {
|
||||
onUnmounted(() => {
|
||||
unmounted.push(props.label);
|
||||
});
|
||||
return () => <Text key={props.label}>{props.label}</Text>;
|
||||
},
|
||||
});
|
||||
|
||||
const items = shallowRef<string[]>(["A"]);
|
||||
|
||||
const App = defineComponent(() => () => (
|
||||
<Box>
|
||||
<Static items={items.value}>
|
||||
{{
|
||||
default: ({ item }: { item: string }) => <Item key={item} label={item} />,
|
||||
}}
|
||||
</Static>
|
||||
<Text>[live]</Text>
|
||||
</Box>
|
||||
));
|
||||
|
||||
const { unmount } = await render(App);
|
||||
|
||||
// After the first render, item "A" has been written. Once the write settles,
|
||||
// its component must unmount (mirroring Ink advancing the cursor past it).
|
||||
await nextTick();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
expect(unmounted).toContain("A");
|
||||
|
||||
// Add "B". "A" was already unmounted; only "B" is freshly mounted+written.
|
||||
items.value = ["A", "B"];
|
||||
await nextTick();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
expect(unmounted).toContain("B");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
// Ink reference: Static resets `index` to `items.length` on every length change
|
||||
// (`useLayoutEffect(() => setIndex(items.length), [items.length])`), so the cursor
|
||||
// can DECREASE. vue-tui must mirror this. A monotonic cursor (only-increase) drops
|
||||
// items after a shrink-then-grow: [A,B] writes (cursor→2); shrink to [A] (Ink resets
|
||||
// cursor→1); grow to [A,C] renders slice(1)=[C] and writes C. With a monotonic
|
||||
// cursor, slice(2)=[] and C is silently never painted.
|
||||
test("Static resets cursor on shrink so later items still paint (Ink parity)", async () => {
|
||||
const items = shallowRef<string[]>(["A", "B"]);
|
||||
|
||||
const App = defineComponent(() => () => (
|
||||
<Box>
|
||||
<Static items={items.value}>
|
||||
{{
|
||||
default: ({ item }: { item: string }) => <Text key={item}>{item}</Text>,
|
||||
}}
|
||||
</Static>
|
||||
<Text>[live]</Text>
|
||||
</Box>
|
||||
));
|
||||
|
||||
const { frames } = await render(App);
|
||||
|
||||
// Let A and B write (cursor advances to 2).
|
||||
await nextTick();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
expect(frames.join("")).toContain("A");
|
||||
expect(frames.join("")).toContain("B");
|
||||
|
||||
// Shrink to [A]. Ink resets the cursor to items.length (1); no re-paint of A.
|
||||
items.value = ["A"];
|
||||
await nextTick();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
|
||||
// Grow to [A, C]. With Ink-parity cursor reset, slice(1)=[C] paints C.
|
||||
// With the monotonic bug, slice(2)=[] and C is dropped forever.
|
||||
items.value = ["A", "C"];
|
||||
await nextTick();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await nextTick();
|
||||
|
||||
expect(frames.join("")).toContain("C");
|
||||
});
|
||||
|
||||
test("Static items do not add blank lines to the dynamic frame", async () => {
|
||||
const items = shallowRef<string[]>([]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineComponent, h, type PropType } from "vue";
|
||||
import { defineComponent, h, shallowRef, watch, type PropType } from "vue";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
const StaticImpl = defineComponent({
|
||||
@@ -13,12 +13,51 @@ const StaticImpl = defineComponent({
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
// Mirrors Ink's `const [index, setIndex] = useState(0)`. Only items at or
|
||||
// after `cursor` are rendered; once written, the renderer advances the
|
||||
// cursor (via the onWritten callback below) so written items unmount.
|
||||
// shallowRef is sufficient — we only ever reassign the number.
|
||||
const cursor = shallowRef(0);
|
||||
|
||||
// Invoked by the renderer AFTER a commit has painted the freshly-written
|
||||
// items. Together with the watch below this is the vue-tui analogue of Ink's
|
||||
// post-commit `useLayoutEffect(() => setIndex(items.length), [items.length])`.
|
||||
//
|
||||
// This callback handles the GROW / steady-state direction: advancing the
|
||||
// cursor only AFTER paint guarantees freshly-appended items are written
|
||||
// before they are sliced out and unmounted. It must run post-paint, never
|
||||
// during render, which is why it can't be a plain watcher. We SET the cursor
|
||||
// to items.length (not max-with-current); assigning an equal number is a
|
||||
// reactivity no-op (Vue triggers on Object.is inequality), so the common
|
||||
// resync-to-same-length case can't loop.
|
||||
const onWritten = () => {
|
||||
cursor.value = (props.items as unknown[]).length;
|
||||
};
|
||||
|
||||
// Handles the SHRINK direction, mirroring Ink's effect firing on every
|
||||
// [items.length] change — including decreases. When items shrink, the
|
||||
// already-rendered Static children may already be empty (sliced out), so no
|
||||
// host mutation occurs and no commit/onWritten fires; the cursor would stay
|
||||
// stranded above the new length and silently drop any later-appended items
|
||||
// (e.g. [A,B] cursor→2, shrink to [A], grow to [A,C] → slice(2)=[] drops C).
|
||||
// Lowering the cursor on shrink is safe without waiting for a paint: shrinking
|
||||
// never needs to write anything, it only re-syncs the slice window down.
|
||||
watch(
|
||||
() => (props.items as unknown[]).length,
|
||||
(len) => {
|
||||
if (len < cursor.value) cursor.value = len;
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
const merged = { ...defaultStyle, ...props.style };
|
||||
const items = props.items as unknown[];
|
||||
const start = cursor.value;
|
||||
const itemsToRender = items.slice(start);
|
||||
return h(
|
||||
"static",
|
||||
merged,
|
||||
(props.items as unknown[]).map((item, index) => slots.default?.({ item, index })),
|
||||
{ ...merged, internal_onWritten: onWritten },
|
||||
itemsToRender.map((item, i) => slots.default?.({ item, index: start + i })),
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -253,6 +253,13 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
||||
onCommit();
|
||||
return;
|
||||
}
|
||||
if (el.type === "static" && key === "internal_onWritten") {
|
||||
// Callback the renderer invokes post-commit to advance the <Static>
|
||||
// component's cursor so written items unmount. Not styling/layout.
|
||||
el.onWritten = typeof next === "function" ? (next as () => void) : undefined;
|
||||
onCommit();
|
||||
return;
|
||||
}
|
||||
if (el.type === "box" || el.type === "text" || el.type === "static" || el.type === "root") {
|
||||
if (isYogaProp(key)) {
|
||||
applyYogaProp(el, key, next);
|
||||
|
||||
@@ -83,7 +83,25 @@ export interface TuiStatic extends NodeBase {
|
||||
children: TuiNode[];
|
||||
yoga: YogaNodeRef;
|
||||
props: BoxProps;
|
||||
writtenCount: number;
|
||||
/**
|
||||
* Host child nodes already written to the static channel. Static items are
|
||||
* write-once: each commit only paints the children NOT in this set. We track
|
||||
* by node identity rather than a count because a single logical item expands
|
||||
* to several host nodes (the <Text>/<Box> plus empty text-leaf fragment
|
||||
* anchors Vue inserts), so a positional `writtenCount` would mis-slice. Once a
|
||||
* child is painted it is recorded here, then the <Static> component advances
|
||||
* its cursor and unmounts it (mirroring Ink's `setIndex(items.length)`).
|
||||
*/
|
||||
writtenNodes: Set<TuiNode>;
|
||||
/**
|
||||
* Callback registered by the <Static> component, invoked by the renderer AFTER
|
||||
* a commit has painted freshly-written items. It advances the component's
|
||||
* reactive cursor (Ink's `index`) so the just-written items are sliced out and
|
||||
* unmount on the next render — the vue-tui analogue of Ink's post-commit
|
||||
* `useLayoutEffect(() => setIndex(items.length))`. Advancing AFTER paint (never
|
||||
* during render) guarantees items are written before they are dropped.
|
||||
*/
|
||||
onWritten?: () => void;
|
||||
}
|
||||
|
||||
export interface TuiTransform extends NodeBase {
|
||||
@@ -171,7 +189,7 @@ export function createStatic(): TuiStatic {
|
||||
children: [],
|
||||
yoga: UNATTACHED_YOGA,
|
||||
props: {},
|
||||
writtenCount: 0,
|
||||
writtenNodes: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,55 @@ export function findStatics(root: TuiNode, out: TuiStatic[] = []): TuiStatic[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paint the not-yet-written children of a single <Static> node and record them
|
||||
* as written. Returns the painted frame (without trailing "\n"), or "" when
|
||||
* there is nothing fresh to write.
|
||||
*
|
||||
* Static items are write-once. `stat.children` only ever holds the currently-
|
||||
* mounted (un-written) items because the <Static> component slices written ones
|
||||
* out — but between a write and the component's cursor advance, the just-written
|
||||
* children are still mounted, so we must skip any child already in `writtenNodes`
|
||||
* (identity-tracked, since one item = several host nodes incl. fragment anchors).
|
||||
* After painting the fresh children we call `onWritten` so the component advances
|
||||
* its cursor and unmounts them (the post-commit step mirroring Ink's
|
||||
* `useLayoutEffect(setIndex)`).
|
||||
*/
|
||||
export function paintStaticNode(stat: TuiStatic, columns: number): string {
|
||||
const fresh = stat.children.filter((child) => !stat.writtenNodes.has(child));
|
||||
// Paint (and record as written) only when there is something fresh — but the
|
||||
// prune and onWritten steps below run on EVERY commit, including the empty
|
||||
// commit that follows a cursor advance (children sliced to []). That empty
|
||||
// commit is exactly when we must (a) prune stale unmounted nodes and (b)
|
||||
// re-sync the cursor to items.length: on a shrink ([A,B]→[A]), nothing fresh
|
||||
// paints, yet Ink's `useLayoutEffect(setIndex(items.length))` still fires and
|
||||
// lowers the cursor so subsequent grows ([A,C]) render and write the new item.
|
||||
let frame = "";
|
||||
if (fresh.length > 0) {
|
||||
frame = paintIsolated(fresh, columns, stat);
|
||||
for (const child of fresh) stat.writtenNodes.add(child);
|
||||
}
|
||||
// Prune entries that are no longer mounted so the set can't grow unbounded
|
||||
// over a long-running app (written children get unmounted on the next render).
|
||||
if (stat.writtenNodes.size > stat.children.length) {
|
||||
const live = new Set(stat.children);
|
||||
for (const node of stat.writtenNodes) {
|
||||
if (!live.has(node)) stat.writtenNodes.delete(node);
|
||||
}
|
||||
}
|
||||
// Defer the cursor sync to AFTER this commit so the just-painted items are
|
||||
// still mounted while they are written; the callback re-renders and drops
|
||||
// them. Always called (even on empty commits) so the cursor tracks
|
||||
// items.length every commit — mirroring Ink's effect on [items.length].
|
||||
// Setting the cursor to an unchanged value is a reactivity no-op, so this
|
||||
// cannot loop.
|
||||
stat.onWritten?.();
|
||||
return frame;
|
||||
}
|
||||
|
||||
export function flushStatic(root: TuiNode, stream: NodeJS.WriteStream): void {
|
||||
for (const stat of findStatics(root)) {
|
||||
const fresh = stat.children.slice(stat.writtenCount);
|
||||
if (fresh.length === 0) continue;
|
||||
const frame = paintIsolated(fresh, stream.columns ?? 80, stat);
|
||||
const frame = paintStaticNode(stat, stream.columns ?? 80);
|
||||
if (frame.length > 0) stream.write(frame + "\n");
|
||||
stat.writtenCount = stat.children.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import Yoga from "yoga-layout";
|
||||
import { createRoot, type TuiNode } from "./host/nodes.ts";
|
||||
import { attachYoga, detachYoga } from "./host/yoga.ts";
|
||||
import { buildNodeOps } from "./host/node-ops.ts";
|
||||
import { paint, paintIsolated } from "./paint/paint.ts";
|
||||
import { paint } from "./paint/paint.ts";
|
||||
import { renderScreenReaderOutput } from "./paint/screen-reader.ts";
|
||||
import { findStatics } from "./paint/static-channel.ts";
|
||||
import { findStatics, paintStaticNode } from "./paint/static-channel.ts";
|
||||
import {
|
||||
AppContextKey,
|
||||
FocusContextKey,
|
||||
@@ -78,13 +78,10 @@ export function renderToString(component: Component, options?: RenderToStringOpt
|
||||
root.yoga.calculateLayout(columns, undefined, Yoga.DIRECTION_LTR);
|
||||
// Flush static output from intermediate renders
|
||||
for (const stat of findStatics(root)) {
|
||||
const fresh = stat.children.slice(stat.writtenCount);
|
||||
if (fresh.length === 0) continue;
|
||||
const staticFrame = paintIsolated(fresh, columns, stat);
|
||||
const staticFrame = paintStaticNode(stat, columns);
|
||||
if (staticFrame && staticFrame !== "\n") {
|
||||
capturedStaticOutput += staticFrame + "\n";
|
||||
}
|
||||
stat.writtenCount = stat.children.length;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -21,8 +21,8 @@ import { attachYoga, detachYoga } from "./host/yoga.ts";
|
||||
import { buildNodeOps } from "./host/node-ops.ts";
|
||||
import { createCommitScheduler } from "./scheduler.ts";
|
||||
import { createAnimationScheduler } from "./animation-scheduler.ts";
|
||||
import { paint, paintIsolated } from "./paint/paint.ts";
|
||||
import { findStatics } from "./paint/static-channel.ts";
|
||||
import { paint } from "./paint/paint.ts";
|
||||
import { findStatics, paintStaticNode } from "./paint/static-channel.ts";
|
||||
import { createFrameWriter } from "./io/frame-writer.ts";
|
||||
import { bsu, esu, shouldSynchronize } from "./io/write-synchronized.ts";
|
||||
import {
|
||||
@@ -550,13 +550,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
const w = resolveSize(stdout).columns;
|
||||
let staticOutput = "";
|
||||
for (const stat of findStatics(tuiRoot)) {
|
||||
const fresh = stat.children.slice(stat.writtenCount);
|
||||
if (fresh.length === 0) continue;
|
||||
const staticFrame = paintIsolated(fresh, w, stat);
|
||||
const staticFrame = paintStaticNode(stat, w);
|
||||
if (staticFrame.length > 0) {
|
||||
staticOutput += staticFrame + "\n";
|
||||
}
|
||||
stat.writtenCount = stat.children.length;
|
||||
}
|
||||
const hasStaticOutput = staticOutput !== "" && staticOutput !== "\n";
|
||||
if (hasStaticOutput) {
|
||||
|
||||
Reference in New Issue
Block a user