omk-tui 1.3.2 → 1.4.1

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/components/box.d.ts +5 -0
  3. package/dist/components/box.d.ts.map +1 -1
  4. package/dist/components/box.js +50 -15
  5. package/dist/components/box.js.map +1 -1
  6. package/dist/components/editor.d.ts +3 -1
  7. package/dist/components/editor.d.ts.map +1 -1
  8. package/dist/components/editor.js +4 -7
  9. package/dist/components/editor.js.map +1 -1
  10. package/dist/components/image.d.ts +2 -0
  11. package/dist/components/image.d.ts.map +1 -1
  12. package/dist/components/image.js +4 -0
  13. package/dist/components/image.js.map +1 -1
  14. package/dist/components/markdown.d.ts +4 -0
  15. package/dist/components/markdown.d.ts.map +1 -1
  16. package/dist/components/markdown.js +14 -0
  17. package/dist/components/markdown.js.map +1 -1
  18. package/dist/components/spacer.d.ts +2 -0
  19. package/dist/components/spacer.d.ts.map +1 -1
  20. package/dist/components/spacer.js +8 -0
  21. package/dist/components/spacer.js.map +1 -1
  22. package/dist/components/text.d.ts +4 -0
  23. package/dist/components/text.d.ts.map +1 -1
  24. package/dist/components/text.js +14 -0
  25. package/dist/components/text.js.map +1 -1
  26. package/dist/editor-component.d.ts +2 -0
  27. package/dist/editor-component.d.ts.map +1 -1
  28. package/dist/editor-component.js.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/line-reset-memo.d.ts +27 -0
  34. package/dist/line-reset-memo.d.ts.map +1 -0
  35. package/dist/line-reset-memo.js +101 -0
  36. package/dist/line-reset-memo.js.map +1 -0
  37. package/dist/render-generation.d.ts +24 -0
  38. package/dist/render-generation.d.ts.map +1 -0
  39. package/dist/render-generation.js +48 -0
  40. package/dist/render-generation.js.map +1 -0
  41. package/dist/terminal-output.d.ts +20 -1
  42. package/dist/terminal-output.d.ts.map +1 -1
  43. package/dist/terminal-output.js +62 -1
  44. package/dist/terminal-output.js.map +1 -1
  45. package/dist/terminal-resize-log.d.ts +39 -0
  46. package/dist/terminal-resize-log.d.ts.map +1 -0
  47. package/dist/terminal-resize-log.js +57 -0
  48. package/dist/terminal-resize-log.js.map +1 -0
  49. package/dist/terminal-resync.d.ts +74 -0
  50. package/dist/terminal-resync.d.ts.map +1 -0
  51. package/dist/terminal-resync.js +112 -0
  52. package/dist/terminal-resync.js.map +1 -0
  53. package/dist/terminal.d.ts.map +1 -1
  54. package/dist/terminal.js +1 -1
  55. package/dist/terminal.js.map +1 -1
  56. package/dist/tui.d.ts +29 -9
  57. package/dist/tui.d.ts.map +1 -1
  58. package/dist/tui.js +65 -86
  59. package/dist/tui.js.map +1 -1
  60. package/dist/undo-stack.d.ts +8 -2
  61. package/dist/undo-stack.d.ts.map +1 -1
  62. package/dist/undo-stack.js +12 -2
  63. package/dist/undo-stack.js.map +1 -1
  64. package/dist/utils.d.ts +2 -4
  65. package/dist/utils.d.ts.map +1 -1
  66. package/dist/utils.js +2 -128
  67. package/dist/utils.js.map +1 -1
  68. package/dist/visible-width.d.ts +14 -0
  69. package/dist/visible-width.d.ts.map +1 -0
  70. package/dist/visible-width.js +163 -0
  71. package/dist/visible-width.js.map +1 -0
  72. package/dist/windowed-container.d.ts +120 -0
  73. package/dist/windowed-container.d.ts.map +1 -0
  74. package/dist/windowed-container.js +258 -0
  75. package/dist/windowed-container.js.map +1 -0
  76. package/dist/windowed-segments.d.ts +37 -0
  77. package/dist/windowed-segments.d.ts.map +1 -0
  78. package/dist/windowed-segments.js +133 -0
  79. package/dist/windowed-segments.js.map +1 -0
  80. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windowed-container.js","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRenderSettled } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\nimport { dropFrozenChild, type FrozenSegment, freezeAboveBudget, reconcileSegments } from \"./windowed-segments.ts\";\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n\t/** Settled children skipped entirely this frame (virtual prefix). */\n\tvirtualChildren: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n *\n * Virtual prefix (opt-in via `setPrefixSeed`/`setPrefixSeedViewport`):\n * when a prefix seed is configured, children above the live budget PLUS the\n * seed budget never render at all — `render()` returns zero lines for them.\n * Rendered line work is bounded by live + seed; prefix identity/settledness\n * checks still visit skipped children. The\n * prefix is a contiguous run anchored at index 0; children above the seed\n * budget never enter the output, so callers that need the whole transcript\n * (`buildFinalSessionLog`, exports) must use `renderAll(width)`.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = {\n\t\tliveChildren: 0,\n\t\trefreshedChildren: 0,\n\t\trefreshedSegments: 0,\n\t\tvirtualChildren: 0,\n\t};\n\t/**\n\t * Children above live+seed that never render (opt-in). Contiguous from\n\t * index 0; emits zero lines every frame once established.\n\t */\n\tprivate virtualChildren: Component[] = [];\n\t/** Unset preserves full-history rendering; zero seeds only the live tail. */\n\tprivate seedLines?: number;\n\tprivate seedViewportRows?: () => number;\n\tprivate seedScreens?: number;\n\tprivate lastPrefixSeedLines?: number;\n\t/** Bound `isChildSettled` for the extracted segment helpers. */\n\tprivate readonly settled = (child: Component): boolean => this.isChildSettled(child);\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\t/**\n\t * Opt in to the virtual prefix: settled children above\n\t * `live budget + seedLines` never render — the first frame of a long\n\t * transcript renders only live + seed lines. Zero seeds only the live\n\t * tail; unset or non-finite input restores full-history rendering.\n\t */\n\tsetPrefixSeed(lines?: number): void {\n\t\tthis.seedViewportRows = undefined;\n\t\tthis.seedScreens = undefined;\n\t\tthis.seedLines = lines !== undefined && Number.isFinite(lines) ? Math.max(0, Math.floor(lines)) : undefined;\n\t}\n\n\t/** Seed budget follows the terminal height: `screens × rows`, re-read on every render. */\n\tsetPrefixSeedViewport(screens: number, rows: () => number): void {\n\t\tthis.seedLines = undefined;\n\t\tthis.seedScreens = Number.isFinite(screens) ? Math.max(0, Math.floor(screens)) : undefined;\n\t\tthis.seedViewportRows = rows;\n\t}\n\n\t/** Current seed budget in lines; undefined means virtualization is off. */\n\tgetPrefixSeedLines(): number | undefined {\n\t\tif (!this.seedViewportRows) return this.seedLines;\n\t\tif (this.seedScreens === undefined) return undefined;\n\t\tconst rows = Math.floor(this.seedViewportRows());\n\t\treturn Math.max(0, Number.isFinite(rows) ? rows * this.seedScreens : 0);\n\t}\n\n\t/** Children skipped entirely by the last render (virtual prefix). */\n\tgetVirtualChildCount(): number {\n\t\treturn this.virtualChildren.length;\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tif (index < this.virtualChildren.length) this.virtualChildren.splice(index, 1);\n\t\tdropFrozenChild(this.segments, index);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment and the virtual prefix; the next render re-lays out all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.virtualChildren = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\t/**\n\t * Render every child without any windowing or virtualization. For callers\n\t * that need the complete transcript (final session log, exports) — the\n\t * windowed state is untouched and the next `render()` resumes windowing.\n\t */\n\trenderAll(width: number): string[] {\n\t\treturn super.render(width);\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\tconst seed = this.getPrefixSeedLines();\n\t\t// Rebuild on seed changes too: a taller terminal can grow it without growing the live budget.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget || seed !== this.lastPrefixSeedLines)\n\t\t\tthis.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.lastPrefixSeedLines = seed;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0, virtualChildren: 0 };\n\t\t// Build the virtual prefix once, while no frozen segments exist. Lines\n\t\t// rendered during the boundary scan are memoized into the emit pass.\n\t\tconst memo =\n\t\t\tseed !== undefined && this.virtualChildren.length === 0 && this.segments.length === 0\n\t\t\t\t? this.scanVirtualBoundary(width, budget, seed)\n\t\t\t\t: undefined;\n\t\tthis.reconcileVirtual();\n\t\tthis.reconcile(width);\n\t\tthis.stats.virtualChildren = this.virtualChildren.length;\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = this.virtualChildren.length; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = memo?.get(i) ?? this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tfreezeAboveBudget(this.segments, this.children, this.settled, parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Establish the virtual prefix in a single bottom-up pass: render children\n\t * from the tail until `live + seed` lines are covered and mark everything\n\t * above as virtual. Returns the rendered lines keyed by child index for\n\t * the emit pass to reuse (children render at most once per frame).\n\t *\n\t * An unsettled child may end up inside the virtual prefix (the scan does\n\t * not check settledness): `reconcileVirtual` then truncates the prefix\n\t * there and the emit pass renders it live as usual, so no output is lost.\n\t */\n\tprivate scanVirtualBoundary(width: number, budget: number, seed: number): Map<number, string[]> {\n\t\tconst memo = new Map<number, string[]>();\n\t\tconst target = budget + seed;\n\t\tlet acc = 0;\n\t\tlet i = this.children.length;\n\t\twhile (i > 0 && acc < target) {\n\t\t\ti -= 1;\n\t\t\tconst lines = this.children[i].render(width);\n\t\t\tmemo.set(i, lines);\n\t\t\tacc += lines.length;\n\t\t}\n\t\tthis.virtualChildren = this.children.slice(0, i);\n\t\treturn memo;\n\t}\n\n\t/**\n\t * Verify the virtual prefix is still a valid prefix of `children` and still\n\t * settled. On the first mismatch (identity, reorder, un-settled) the prefix\n\t * is truncated there: children past the cut render live this frame and\n\t * their lines materialize as new output (a bounded repaint repair).\n\t */\n\tprivate reconcileVirtual(): void {\n\t\tconst count = this.virtualChildren.length;\n\t\tlet v = 0;\n\t\twhile (v < count && this.children[v] === this.virtualChildren[v] && this.isChildSettled(this.children[v])) v += 1;\n\t\tif (v < count) this.virtualChildren.length = v;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept = reconcileSegments(this.segments, this.children, this.settled, width, this.stats);\n\t\tthis.segments = kept ?? [];\n\t}\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import type { Component } from "./tui.ts";
2
+ /**
3
+ * A frozen segment never grows past this many lines (a single larger child
4
+ * gets a segment of its own). Bounds the work of refreshing one changed child:
5
+ * only its segment's line buffer is rebuilt, never the whole frozen history.
6
+ */
7
+ export declare const SEGMENT_MAX_LINES = 512;
8
+ /** Contiguous settled children whose rendered lines are reused verbatim. */
9
+ export type FrozenSegment = {
10
+ /** Index in `children` of the first frozen child. */
11
+ from: number;
12
+ /** The frozen children themselves (identity-checked every frame). */
13
+ children: Component[];
14
+ lineCounts: number[];
15
+ /** `getRenderGeneration()` of each child when its lines were taken. */
16
+ generations: number[];
17
+ /** Concatenation of the children's lines. */
18
+ lines: string[];
19
+ };
20
+ /** Per-frame counters a refresh accumulates into. */
21
+ export type SegmentRefreshStats = {
22
+ refreshedChildren: number;
23
+ refreshedSegments: number;
24
+ };
25
+ type SettledCheck = (child: Component) => boolean;
26
+ /** Splice one child out of the segment table after `children[index]` was removed. */
27
+ export declare function dropFrozenChild(segments: FrozenSegment[], index: number): void;
28
+ /**
29
+ * Drop unsettled segments and refresh children whose generation moved.
30
+ * Returns the kept segments, or null when a foreign edit of `children`
31
+ * invalidated every segment (the caller drops all state).
32
+ */
33
+ export declare function reconcileSegments(segments: FrozenSegment[], children: readonly Component[], isSettled: SettledCheck, width: number, stats: SegmentRefreshStats): FrozenSegment[] | null;
34
+ /** Freeze settled live children that sit above the live-line budget. */
35
+ export declare function freezeAboveBudget(segments: FrozenSegment[], children: readonly Component[], isSettled: SettledCheck, parts: string[][], partStarts: number[], live: number[], liveLines: string[][], budget: number): void;
36
+ export {};
37
+ //# sourceMappingURL=windowed-segments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windowed-segments.d.ts","sourceRoot":"","sources":["../src/windowed-segments.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,4EAA4E;AAC5E,MAAM,MAAM,aAAa,GAAG;IAC3B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uEAAuE;IACvE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;AAElD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAiB9E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,mBAAmB,GACxB,aAAa,EAAE,GAAG,IAAI,CAyBxB;AA0BD,wEAAwE;AACxE,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,MAAM,EAAE,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,EAAE,EAAE,EACrB,MAAM,EAAE,MAAM,GACZ,IAAI,CAuCN","sourcesContent":["import { releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport type { Component } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nexport const SEGMENT_MAX_LINES = 512;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\nexport type FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\n/** Per-frame counters a refresh accumulates into. */\nexport type SegmentRefreshStats = { refreshedChildren: number; refreshedSegments: number };\n\ntype SettledCheck = (child: Component) => boolean;\n\n/** Splice one child out of the segment table after `children[index]` was removed. */\nexport function dropFrozenChild(segments: FrozenSegment[], index: number): void {\n\tfor (const segment of segments) {\n\t\tif (index < segment.from) {\n\t\t\tsegment.from -= 1;\n\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\tconst j = index - segment.from;\n\t\t\tlet offset = 0;\n\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\tsegment.children.splice(j, 1);\n\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\tsegment.generations.splice(j, 1);\n\t\t}\n\t}\n\tfor (let i = segments.length - 1; i >= 0; i--) {\n\t\tif (segments[i].children.length === 0) segments.splice(i, 1);\n\t}\n}\n\n/**\n * Drop unsettled segments and refresh children whose generation moved.\n * Returns the kept segments, or null when a foreign edit of `children`\n * invalidated every segment (the caller drops all state).\n */\nexport function reconcileSegments(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\twidth: number,\n\tstats: SegmentRefreshStats,\n): FrozenSegment[] | null {\n\tconst kept: FrozenSegment[] = [];\n\tfor (const segment of segments) {\n\t\tlet dirty: number[] | undefined;\n\t\tlet settled = true;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst child = segment.children[j];\n\t\t\tif (children[segment.from + j] !== child) {\n\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!isSettled(child)) {\n\t\t\t\tsettled = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\tdirty ??= [];\n\t\t\t\tdirty.push(j);\n\t\t\t}\n\t\t}\n\t\tif (!settled) continue;\n\t\tif (dirty) refreshSegment(segment, dirty, width, stats);\n\t\tkept.push(segment);\n\t}\n\treturn kept;\n}\n\nfunction refreshSegment(segment: FrozenSegment, dirty: number[], width: number, stats: SegmentRefreshStats): void {\n\tconst lines: string[] = [];\n\tlet offset = 0;\n\tlet next = 0;\n\tfor (let j = 0; j < segment.children.length; j++) {\n\t\tconst count = segment.lineCounts[j];\n\t\tif (dirty[next] === j) {\n\t\t\tnext += 1;\n\t\t\tconst child = segment.children[j];\n\t\t\tconst fresh = child.render(width);\n\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\treleaseRenderCache(child);\n\t\t} else {\n\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t}\n\t\toffset += count;\n\t}\n\tsegment.lines = lines;\n\tstats.refreshedChildren += dirty.length;\n\tstats.refreshedSegments += 1;\n}\n\n/** Freeze settled live children that sit above the live-line budget. */\nexport function freezeAboveBudget(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\tparts: string[][],\n\tpartStarts: number[],\n\tlive: number[],\n\tliveLines: string[][],\n\tbudget: number,\n): void {\n\tlet acc = 0;\n\tlet liveStart = children.length;\n\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\tacc += parts[p].length;\n\t\tliveStart = partStarts[p];\n\t\tif (acc >= budget) break;\n\t}\n\tlet segmentIndex = 0;\n\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\tconst index = live[n];\n\t\tconst child = children[index];\n\t\tif (!isSettled(child)) continue;\n\t\tconst lines = liveLines[n];\n\t\twhile (segmentIndex < segments.length && segments[segmentIndex].from < index) segmentIndex += 1;\n\t\tconst previous = segments[segmentIndex - 1];\n\t\tconst generation = renderGeneration(child);\n\t\tif (\n\t\t\tprevious &&\n\t\t\tprevious.from + previous.children.length === index &&\n\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t) {\n\t\t\tprevious.children.push(child);\n\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\tprevious.generations.push(generation);\n\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t} else {\n\t\t\tconst segment: FrozenSegment = {\n\t\t\t\tfrom: index,\n\t\t\t\tchildren: [child],\n\t\t\t\tlineCounts: [lines.length],\n\t\t\t\tgenerations: [generation],\n\t\t\t\tlines: lines.slice(),\n\t\t\t};\n\t\t\tsegments.splice(segmentIndex, 0, segment);\n\t\t\tsegmentIndex += 1;\n\t\t}\n\t\treleaseRenderCache(child);\n\t}\n}\n"]}
@@ -0,0 +1,133 @@
1
+ import { releaseRenderCache, renderGeneration } from "./render-generation.js";
2
+ /**
3
+ * A frozen segment never grows past this many lines (a single larger child
4
+ * gets a segment of its own). Bounds the work of refreshing one changed child:
5
+ * only its segment's line buffer is rebuilt, never the whole frozen history.
6
+ */
7
+ export const SEGMENT_MAX_LINES = 512;
8
+ /** Splice one child out of the segment table after `children[index]` was removed. */
9
+ export function dropFrozenChild(segments, index) {
10
+ for (const segment of segments) {
11
+ if (index < segment.from) {
12
+ segment.from -= 1;
13
+ }
14
+ else if (index < segment.from + segment.children.length) {
15
+ const j = index - segment.from;
16
+ let offset = 0;
17
+ for (let k = 0; k < j; k++)
18
+ offset += segment.lineCounts[k];
19
+ segment.lines.splice(offset, segment.lineCounts[j]);
20
+ segment.children.splice(j, 1);
21
+ segment.lineCounts.splice(j, 1);
22
+ segment.generations.splice(j, 1);
23
+ }
24
+ }
25
+ for (let i = segments.length - 1; i >= 0; i--) {
26
+ if (segments[i].children.length === 0)
27
+ segments.splice(i, 1);
28
+ }
29
+ }
30
+ /**
31
+ * Drop unsettled segments and refresh children whose generation moved.
32
+ * Returns the kept segments, or null when a foreign edit of `children`
33
+ * invalidated every segment (the caller drops all state).
34
+ */
35
+ export function reconcileSegments(segments, children, isSettled, width, stats) {
36
+ const kept = [];
37
+ for (const segment of segments) {
38
+ let dirty;
39
+ let settled = true;
40
+ for (let j = 0; j < segment.children.length; j++) {
41
+ const child = segment.children[j];
42
+ if (children[segment.from + j] !== child) {
43
+ // `children` was edited behind our back: nothing frozen can be trusted.
44
+ return null;
45
+ }
46
+ if (!isSettled(child)) {
47
+ settled = false;
48
+ break;
49
+ }
50
+ if (renderGeneration(child) !== segment.generations[j]) {
51
+ dirty ??= [];
52
+ dirty.push(j);
53
+ }
54
+ }
55
+ if (!settled)
56
+ continue;
57
+ if (dirty)
58
+ refreshSegment(segment, dirty, width, stats);
59
+ kept.push(segment);
60
+ }
61
+ return kept;
62
+ }
63
+ function refreshSegment(segment, dirty, width, stats) {
64
+ const lines = [];
65
+ let offset = 0;
66
+ let next = 0;
67
+ for (let j = 0; j < segment.children.length; j++) {
68
+ const count = segment.lineCounts[j];
69
+ if (dirty[next] === j) {
70
+ next += 1;
71
+ const child = segment.children[j];
72
+ const fresh = child.render(width);
73
+ for (const line of fresh)
74
+ lines.push(line);
75
+ segment.lineCounts[j] = fresh.length;
76
+ segment.generations[j] = renderGeneration(child);
77
+ releaseRenderCache(child);
78
+ }
79
+ else {
80
+ for (let l = offset; l < offset + count; l++)
81
+ lines.push(segment.lines[l]);
82
+ }
83
+ offset += count;
84
+ }
85
+ segment.lines = lines;
86
+ stats.refreshedChildren += dirty.length;
87
+ stats.refreshedSegments += 1;
88
+ }
89
+ /** Freeze settled live children that sit above the live-line budget. */
90
+ export function freezeAboveBudget(segments, children, isSettled, parts, partStarts, live, liveLines, budget) {
91
+ let acc = 0;
92
+ let liveStart = children.length;
93
+ for (let p = parts.length - 1; p >= 0; p--) {
94
+ acc += parts[p].length;
95
+ liveStart = partStarts[p];
96
+ if (acc >= budget)
97
+ break;
98
+ }
99
+ let segmentIndex = 0;
100
+ for (let n = 0; n < live.length && live[n] < liveStart; n++) {
101
+ const index = live[n];
102
+ const child = children[index];
103
+ if (!isSettled(child))
104
+ continue;
105
+ const lines = liveLines[n];
106
+ while (segmentIndex < segments.length && segments[segmentIndex].from < index)
107
+ segmentIndex += 1;
108
+ const previous = segments[segmentIndex - 1];
109
+ const generation = renderGeneration(child);
110
+ if (previous &&
111
+ previous.from + previous.children.length === index &&
112
+ previous.lines.length + lines.length <= SEGMENT_MAX_LINES) {
113
+ previous.children.push(child);
114
+ previous.lineCounts.push(lines.length);
115
+ previous.generations.push(generation);
116
+ for (const line of lines)
117
+ previous.lines.push(line);
118
+ }
119
+ else {
120
+ const segment = {
121
+ from: index,
122
+ children: [child],
123
+ lineCounts: [lines.length],
124
+ generations: [generation],
125
+ lines: lines.slice(),
126
+ };
127
+ segments.splice(segmentIndex, 0, segment);
128
+ segmentIndex += 1;
129
+ }
130
+ releaseRenderCache(child);
131
+ }
132
+ }
133
+ //# sourceMappingURL=windowed-segments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windowed-segments.js","sourceRoot":"","sources":["../src/windowed-segments.ts"],"names":[],"mappings":"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","sourcesContent":["import { releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport type { Component } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nexport const SEGMENT_MAX_LINES = 512;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\nexport type FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\n/** Per-frame counters a refresh accumulates into. */\nexport type SegmentRefreshStats = { refreshedChildren: number; refreshedSegments: number };\n\ntype SettledCheck = (child: Component) => boolean;\n\n/** Splice one child out of the segment table after `children[index]` was removed. */\nexport function dropFrozenChild(segments: FrozenSegment[], index: number): void {\n\tfor (const segment of segments) {\n\t\tif (index < segment.from) {\n\t\t\tsegment.from -= 1;\n\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\tconst j = index - segment.from;\n\t\t\tlet offset = 0;\n\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\tsegment.children.splice(j, 1);\n\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\tsegment.generations.splice(j, 1);\n\t\t}\n\t}\n\tfor (let i = segments.length - 1; i >= 0; i--) {\n\t\tif (segments[i].children.length === 0) segments.splice(i, 1);\n\t}\n}\n\n/**\n * Drop unsettled segments and refresh children whose generation moved.\n * Returns the kept segments, or null when a foreign edit of `children`\n * invalidated every segment (the caller drops all state).\n */\nexport function reconcileSegments(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\twidth: number,\n\tstats: SegmentRefreshStats,\n): FrozenSegment[] | null {\n\tconst kept: FrozenSegment[] = [];\n\tfor (const segment of segments) {\n\t\tlet dirty: number[] | undefined;\n\t\tlet settled = true;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst child = segment.children[j];\n\t\t\tif (children[segment.from + j] !== child) {\n\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!isSettled(child)) {\n\t\t\t\tsettled = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\tdirty ??= [];\n\t\t\t\tdirty.push(j);\n\t\t\t}\n\t\t}\n\t\tif (!settled) continue;\n\t\tif (dirty) refreshSegment(segment, dirty, width, stats);\n\t\tkept.push(segment);\n\t}\n\treturn kept;\n}\n\nfunction refreshSegment(segment: FrozenSegment, dirty: number[], width: number, stats: SegmentRefreshStats): void {\n\tconst lines: string[] = [];\n\tlet offset = 0;\n\tlet next = 0;\n\tfor (let j = 0; j < segment.children.length; j++) {\n\t\tconst count = segment.lineCounts[j];\n\t\tif (dirty[next] === j) {\n\t\t\tnext += 1;\n\t\t\tconst child = segment.children[j];\n\t\t\tconst fresh = child.render(width);\n\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\treleaseRenderCache(child);\n\t\t} else {\n\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t}\n\t\toffset += count;\n\t}\n\tsegment.lines = lines;\n\tstats.refreshedChildren += dirty.length;\n\tstats.refreshedSegments += 1;\n}\n\n/** Freeze settled live children that sit above the live-line budget. */\nexport function freezeAboveBudget(\n\tsegments: FrozenSegment[],\n\tchildren: readonly Component[],\n\tisSettled: SettledCheck,\n\tparts: string[][],\n\tpartStarts: number[],\n\tlive: number[],\n\tliveLines: string[][],\n\tbudget: number,\n): void {\n\tlet acc = 0;\n\tlet liveStart = children.length;\n\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\tacc += parts[p].length;\n\t\tliveStart = partStarts[p];\n\t\tif (acc >= budget) break;\n\t}\n\tlet segmentIndex = 0;\n\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\tconst index = live[n];\n\t\tconst child = children[index];\n\t\tif (!isSettled(child)) continue;\n\t\tconst lines = liveLines[n];\n\t\twhile (segmentIndex < segments.length && segments[segmentIndex].from < index) segmentIndex += 1;\n\t\tconst previous = segments[segmentIndex - 1];\n\t\tconst generation = renderGeneration(child);\n\t\tif (\n\t\t\tprevious &&\n\t\t\tprevious.from + previous.children.length === index &&\n\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t) {\n\t\t\tprevious.children.push(child);\n\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\tprevious.generations.push(generation);\n\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t} else {\n\t\t\tconst segment: FrozenSegment = {\n\t\t\t\tfrom: index,\n\t\t\t\tchildren: [child],\n\t\t\t\tlineCounts: [lines.length],\n\t\t\t\tgenerations: [generation],\n\t\t\t\tlines: lines.slice(),\n\t\t\t};\n\t\t\tsegments.splice(segmentIndex, 0, segment);\n\t\t\tsegmentIndex += 1;\n\t\t}\n\t\treleaseRenderCache(child);\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omk-tui",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",