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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.4.1] - 2026-10-13
6
+
7
+ ### Added
8
+
9
+ - Optional `Editor.onPaste(text): boolean` intercepts complete bracketed pastes once, including chunked input; returning true consumes the paste while false retains ordinary text insertion/undo. `EditorComponent` exposes the optional callback for custom editor adapters.
10
+ - `WindowedContainer` virtual prefix (`setPrefixSeed`/`setPrefixSeedViewport`): settled children above the live + seed budget skip rendering at component boundaries. Prefix identity and settledness checks still visit skipped children. Off by default; zero seeds only the live tail, while `setPrefixSeed()` restores full-history rendering. Seed changes and height-only resizes recompute the boundary. `renderAll(width)` renders the complete transcript without changing windowed state. `WindowedRenderStats.virtualChildren` and `getVirtualChildCount()` report the skipped prefix size.
11
+
12
+ ### Fixed
13
+
14
+ - Explicit `TUI.stop({ finalLines })` transcripts bypass the ordinary scrollback repair budget when a clearing redraw is needed, so a previously virtual prefix is emitted on exit. Normal resizes and repaint repairs remain bounded.
15
+
16
+ ## [1.4.0] - 2026-10-11
17
+
18
+ ### Added
19
+
20
+ - `TUI.viewportTop`: the first buffer row inside the viewport as of the last render (0 while the whole frame fits), so components can tell whether their rows have scrolled into scrollback.
21
+
22
+ ### Changed
23
+
24
+ - `visibleWidth` skips grapheme segmentation for text that is printable ASCII, Hangul syllables or CJK unified ideographs once escape sequences are stripped, and the width cache grows from 512 to 8192 entries (strings over 4096 characters are not cached). Width logic moved from `utils.ts` to `visible-width.ts`; `utils.ts` still exports `visibleWidth`.
25
+ - `Box` compares its children's rendered lines with a shallow copy of the previous output before building padded lines, so an unchanged box no longer re-concatenates and re-compares every line each frame. A child that mutates its returned array in place still invalidates the cache.
26
+ - Long transcripts cost far less per-frame work. The new `WindowedContainer` freezes settled children above a live-line budget into bounded line segments and stops re-rendering them; a frozen child that changes is re-rendered alone and only its segment is rebuilt. On a 100k-line headless transcript a keypress frame drops from ~33 ms to ~3 ms mean, and a change to an old, off-screen message stays under 5 ms p95 (was 35–38 ms mean). Per-frame cost still grows slowly with history (each frame scans frozen children and copies the line list twice): a 100k-line keypress frame is ≈3.3× the 25k one.
27
+ - `WindowedContainer.setViewportRows(() => rows)` makes the live-line budget follow the terminal: `max(120, 2 × rows)`, re-read every render; a taller terminal thaws the frozen prefix. Without it the budget stays 120 (`setLiveLineBudget` fixes it).
28
+ - `LineResetMemo` (split out of `tui.ts`) reuses the previous frame's reset output for unchanged rows, including rows shifted by lines inserted or removed above. The differential renderer finds changed rows from both ends and `Container.render` bulk-copies child output.
29
+ - `Text`, `Markdown`, `Box`, `Spacer` and `Container` report `getRenderGeneration()`, a stamp from `nextRenderGeneration()` that rises on every visible change (containers include their children). `Component` gains optional `getRenderGeneration()` / `isRenderSettled()`. `isRenderSettled`, `releaseRenderCache`, `renderGeneration` and `nextRenderGeneration` are exported.
30
+
31
+ ### Fixed
32
+
33
+ - `Editor` and `Input` keep at most 200 undo snapshots (the internal `UndoStack` drops the oldest past its limit), so undo history no longer grows without bound while typing a long draft.
34
+ - Freezing a `Markdown` no longer clears its stream cache by assigning `undefined`, which made the next render after a resize throw; `Markdown.releaseRenderCache()` resets it instead.
35
+ - Freezing no longer clears fields named `cache` / `cachedLines` / `cachedText` / `cachedWidth` by name, which crashed third-party components whose `cache` was not a render cache (e.g. a `Map`) on the next resize. Releasing is now opt-in through `Component.releaseRenderCache()`: `Text`, `Markdown`, `Box` and `Image` drop their caches, `Container` and `Box` forward to their children, other components are left untouched.
36
+
3
37
  ## [1.3.2] - 2026-10-10
4
38
 
5
39
  ### Changed
@@ -8,13 +8,18 @@ export declare class Box implements Component {
8
8
  private paddingY;
9
9
  private bgFn?;
10
10
  private cache?;
11
+ private structureGeneration;
11
12
  constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
12
13
  addChild(component: Component): void;
13
14
  removeChild(component: Component): void;
14
15
  clear(): void;
15
16
  setBgFn(bgFn?: (text: string) => string): void;
16
17
  private invalidateCache;
18
+ /** Raises on child-list / background changes and on any descendant change. */
19
+ getRenderGeneration(): number;
17
20
  private matchCache;
21
+ /** Off-screen windowing: drop the padded-line cache (no generation change) and forward to children. */
22
+ releaseRenderCache(): void;
18
23
  invalidate(): void;
19
24
  render(width: number): string[];
20
25
  private applyBg;
@@ -1 +1 @@
1
- {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IAGxC,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmD9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.ts\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
1
+ {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAW3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IAGxC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,mBAAmB,CAAK;IAEhC,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAM7C;IAED,OAAO,CAAC,eAAe;IAKvB,8EAA8E;IAC9E,mBAAmB,IAAI,MAAM,CAE5B;IAED,OAAO,CAAC,UAAU;IAelB,uGAAuG;IACvG,kBAAkB,IAAI,IAAI,CAGzB;IAED,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4D9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import { maxChildGeneration, nextRenderGeneration, releaseRenderCache } from \"../render-generation.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.ts\";\n\ntype RenderCache = {\n\t/** Shallow copies of each child's rendered lines (guards against in-place mutation). */\n\tchildOutputs: string[][];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\tprivate structureGeneration = 0;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tif (bgFn === this.bgFn) return;\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output.\n\t\t// The generation still moves so off-screen windowing re-renders this box.\n\t\tthis.structureGeneration = nextRenderGeneration();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t\tthis.structureGeneration = nextRenderGeneration();\n\t}\n\n\t/** Raises on child-list / background changes and on any descendant change. */\n\tgetRenderGeneration(): number {\n\t\treturn maxChildGeneration(this.structureGeneration, this.children);\n\t}\n\n\tprivate matchCache(width: number, childOutputs: string[][], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\tif (!cache || cache.width !== width || cache.bgSample !== bgSample) return false;\n\t\tif (cache.childOutputs.length !== childOutputs.length) return false;\n\t\tfor (let c = 0; c < childOutputs.length; c++) {\n\t\t\tconst previous = cache.childOutputs[c];\n\t\t\tconst current = childOutputs[c];\n\t\t\tif (previous.length !== current.length) return false;\n\t\t\tfor (let i = 0; i < current.length; i++) {\n\t\t\t\tif (previous[i] !== current[i]) return false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/** Off-screen windowing: drop the padded-line cache (no generation change) and forward to children. */\n\treleaseRenderCache(): void {\n\t\tthis.cache = undefined;\n\t\tfor (const child of this.children) releaseRenderCache(child);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children. Compare their raw output with the cached copy before\n\t\t// building padded lines: children return cached arrays, so an unchanged Box costs\n\t\t// reference comparisons instead of re-concatenating and re-comparing every line.\n\t\tconst childOutputs: string[][] = [];\n\t\tlet lineCount = 0;\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tchildOutputs.push(lines);\n\t\t\tlineCount += lines.length;\n\t\t}\n\n\t\tif (lineCount === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childOutputs, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\tconst childLines: string[] = [];\n\t\tfor (const lines of childOutputs) {\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childOutputs: childOutputs.map((lines) => lines.slice()), width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { maxChildGeneration, nextRenderGeneration, releaseRenderCache } from "../render-generation.js";
1
2
  import { applyBackgroundToLine, visibleWidth } from "../utils.js";
2
3
  /**
3
4
  * Box component - a container that applies padding and background to all children
@@ -9,6 +10,7 @@ export class Box {
9
10
  bgFn;
10
11
  // Cache for rendered output
11
12
  cache;
13
+ structureGeneration = 0;
12
14
  constructor(paddingX = 1, paddingY = 1, bgFn) {
13
15
  this.paddingX = paddingX;
14
16
  this.paddingY = paddingY;
@@ -30,19 +32,44 @@ export class Box {
30
32
  this.invalidateCache();
31
33
  }
32
34
  setBgFn(bgFn) {
35
+ if (bgFn === this.bgFn)
36
+ return;
33
37
  this.bgFn = bgFn;
34
- // Don't invalidate here - we'll detect bgFn changes by sampling output
38
+ // Don't invalidate here - we'll detect bgFn changes by sampling output.
39
+ // The generation still moves so off-screen windowing re-renders this box.
40
+ this.structureGeneration = nextRenderGeneration();
35
41
  }
36
42
  invalidateCache() {
37
43
  this.cache = undefined;
44
+ this.structureGeneration = nextRenderGeneration();
38
45
  }
39
- matchCache(width, childLines, bgSample) {
46
+ /** Raises on child-list / background changes and on any descendant change. */
47
+ getRenderGeneration() {
48
+ return maxChildGeneration(this.structureGeneration, this.children);
49
+ }
50
+ matchCache(width, childOutputs, bgSample) {
40
51
  const cache = this.cache;
41
- return (!!cache &&
42
- cache.width === width &&
43
- cache.bgSample === bgSample &&
44
- cache.childLines.length === childLines.length &&
45
- cache.childLines.every((line, i) => line === childLines[i]));
52
+ if (!cache || cache.width !== width || cache.bgSample !== bgSample)
53
+ return false;
54
+ if (cache.childOutputs.length !== childOutputs.length)
55
+ return false;
56
+ for (let c = 0; c < childOutputs.length; c++) {
57
+ const previous = cache.childOutputs[c];
58
+ const current = childOutputs[c];
59
+ if (previous.length !== current.length)
60
+ return false;
61
+ for (let i = 0; i < current.length; i++) {
62
+ if (previous[i] !== current[i])
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
68
+ /** Off-screen windowing: drop the padded-line cache (no generation change) and forward to children. */
69
+ releaseRenderCache() {
70
+ this.cache = undefined;
71
+ for (const child of this.children)
72
+ releaseRenderCache(child);
46
73
  }
47
74
  invalidate() {
48
75
  this.invalidateCache();
@@ -56,23 +83,31 @@ export class Box {
56
83
  }
57
84
  const contentWidth = Math.max(1, width - this.paddingX * 2);
58
85
  const leftPad = " ".repeat(this.paddingX);
59
- // Render all children
60
- const childLines = [];
86
+ // Render all children. Compare their raw output with the cached copy before
87
+ // building padded lines: children return cached arrays, so an unchanged Box costs
88
+ // reference comparisons instead of re-concatenating and re-comparing every line.
89
+ const childOutputs = [];
90
+ let lineCount = 0;
61
91
  for (const child of this.children) {
62
92
  const lines = child.render(contentWidth);
63
- for (const line of lines) {
64
- childLines.push(leftPad + line);
65
- }
93
+ childOutputs.push(lines);
94
+ lineCount += lines.length;
66
95
  }
67
- if (childLines.length === 0) {
96
+ if (lineCount === 0) {
68
97
  return [];
69
98
  }
70
99
  // Check if bgFn output changed by sampling
71
100
  const bgSample = this.bgFn ? this.bgFn("test") : undefined;
72
101
  // Check cache validity
73
- if (this.matchCache(width, childLines, bgSample)) {
102
+ if (this.matchCache(width, childOutputs, bgSample)) {
74
103
  return this.cache.lines;
75
104
  }
105
+ const childLines = [];
106
+ for (const lines of childOutputs) {
107
+ for (const line of lines) {
108
+ childLines.push(leftPad + line);
109
+ }
110
+ }
76
111
  // Apply background and padding
77
112
  const result = [];
78
113
  // Top padding
@@ -88,7 +123,7 @@ export class Box {
88
123
  result.push(this.applyBg("", width));
89
124
  }
90
125
  // Update cache
91
- this.cache = { childLines, width, bgSample, lines: result };
126
+ this.cache = { childOutputs: childOutputs.map((lines) => lines.slice()), width, bgSample, lines: result };
92
127
  return result;
93
128
  }
94
129
  applyBg(line, width) {
@@ -1 +1 @@
1
- {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.ts\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
1
+ {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import { maxChildGeneration, nextRenderGeneration, releaseRenderCache } from \"../render-generation.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.ts\";\n\ntype RenderCache = {\n\t/** Shallow copies of each child's rendered lines (guards against in-place mutation). */\n\tchildOutputs: string[][];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\tprivate structureGeneration = 0;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tif (bgFn === this.bgFn) return;\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output.\n\t\t// The generation still moves so off-screen windowing re-renders this box.\n\t\tthis.structureGeneration = nextRenderGeneration();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t\tthis.structureGeneration = nextRenderGeneration();\n\t}\n\n\t/** Raises on child-list / background changes and on any descendant change. */\n\tgetRenderGeneration(): number {\n\t\treturn maxChildGeneration(this.structureGeneration, this.children);\n\t}\n\n\tprivate matchCache(width: number, childOutputs: string[][], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\tif (!cache || cache.width !== width || cache.bgSample !== bgSample) return false;\n\t\tif (cache.childOutputs.length !== childOutputs.length) return false;\n\t\tfor (let c = 0; c < childOutputs.length; c++) {\n\t\t\tconst previous = cache.childOutputs[c];\n\t\t\tconst current = childOutputs[c];\n\t\t\tif (previous.length !== current.length) return false;\n\t\t\tfor (let i = 0; i < current.length; i++) {\n\t\t\t\tif (previous[i] !== current[i]) return false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/** Off-screen windowing: drop the padded-line cache (no generation change) and forward to children. */\n\treleaseRenderCache(): void {\n\t\tthis.cache = undefined;\n\t\tfor (const child of this.children) releaseRenderCache(child);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children. Compare their raw output with the cached copy before\n\t\t// building padded lines: children return cached arrays, so an unchanged Box costs\n\t\t// reference comparisons instead of re-concatenating and re-comparing every line.\n\t\tconst childOutputs: string[][] = [];\n\t\tlet lineCount = 0;\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tchildOutputs.push(lines);\n\t\t\tlineCount += lines.length;\n\t\t}\n\n\t\tif (lineCount === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childOutputs, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\tconst childLines: string[] = [];\n\t\tfor (const lines of childOutputs) {\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childOutputs: childOutputs.map((lines) => lines.slice()), width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
@@ -56,7 +56,7 @@ export declare class Editor implements Component, Focusable {
56
56
  private pastes;
57
57
  private pasteCounter;
58
58
  private pasteBuffer;
59
- private isInPaste;
59
+ protected isInPaste: boolean;
60
60
  private history;
61
61
  private historyIndex;
62
62
  private killRing;
@@ -67,6 +67,8 @@ export declare class Editor implements Component, Focusable {
67
67
  private undoStack;
68
68
  onSubmit?: (text: string) => void;
69
69
  onChange?: (text: string) => void;
70
+ /** Return true to consume a complete bracketed paste (e.g. dropped image paths). */
71
+ onPaste?: (text: string) => boolean;
70
72
  disableSubmit: boolean;
71
73
  constructor(tui: TUI, theme: EditorTheme, options?: EditorOptions);
72
74
  /** Set of currently valid paste IDs, for marker-aware segmentation. */