@oh-my-pi/pi-tui 14.5.8 → 14.5.10

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "14.5.8",
4
+ "version": "14.5.10",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,9 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "14.5.8",
41
- "@oh-my-pi/pi-utils": "14.5.8",
40
+ "@oh-my-pi/pi-natives": "14.5.10",
41
+ "@oh-my-pi/pi-utils": "14.5.10",
42
+ "lru-cache": "11.3.5",
42
43
  "marked": "^18.0.2"
43
44
  },
44
45
  "devDependencies": {
@@ -1,9 +1,40 @@
1
+ import { LRUCache } from "lru-cache/raw";
1
2
  import { marked, type Token, type Tokens } from "marked";
2
3
  import type { SymbolTheme } from "../symbols";
3
4
  import { TERMINAL } from "../terminal-capabilities";
4
5
  import type { Component } from "../tui";
5
6
  import { applyBackgroundToLine, padding, replaceTabs, visibleWidth, wrapTextWithAnsi } from "../utils";
6
7
 
8
+ // ---------------------------------------------------------------------------
9
+ // Module-level LRU render cache
10
+ // ---------------------------------------------------------------------------
11
+ // Each session-tree navigation discards and recreates Markdown component
12
+ // instances, so the per-instance #cachedLines field is always cold on first
13
+ // render of a fresh component. This module-level cache survives across
14
+ // component lifetimes and eliminates redundant marked.lexer + highlightCode
15
+ // (Rust FFI) work for content/layout combinations already seen this session.
16
+
17
+ const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
18
+ const renderCache = new LRUCache<string, string[]>({ max: RENDER_CACHE_MAX });
19
+
20
+ /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
21
+ export function clearRenderCache(): void {
22
+ renderCache.clear();
23
+ }
24
+
25
+ // Stable numeric IDs for structural theme/style objects (no ID field on type).
26
+ // WeakMap so GC can collect orphaned themes/styles without a leak.
27
+ const _objectIds = new WeakMap<object, number>();
28
+ let _nextObjectId = 0;
29
+ function objectId(o: object): number {
30
+ let id = _objectIds.get(o);
31
+ if (id === undefined) {
32
+ id = _nextObjectId++;
33
+ _objectIds.set(o, id);
34
+ }
35
+ return id;
36
+ }
37
+
7
38
  /**
8
39
  * Default text styling for markdown content.
9
40
  * Applied to all text unless overridden by markdown formatting.
@@ -116,7 +147,8 @@ export class Markdown implements Component {
116
147
  }
117
148
 
118
149
  render(width: number): string[] {
119
- // Check cache
150
+ // L1: per-instance cache — fastest path for repeated renders of the same
151
+ // instance at the same width (e.g. resize debounce, repeated redraws).
120
152
  if (this.#cachedLines && this.#cachedText === this.#text && this.#cachedWidth === width) {
121
153
  return this.#cachedLines;
122
154
  }
@@ -127,7 +159,7 @@ export class Markdown implements Component {
127
159
  // Don't render anything if there's no actual text
128
160
  if (!this.#text || this.#text.trim() === "") {
129
161
  const result: string[] = [];
130
- // Update cache
162
+ // Update per-instance cache
131
163
  this.#cachedText = this.#text;
132
164
  this.#cachedWidth = width;
133
165
  this.#cachedLines = result;
@@ -137,6 +169,19 @@ export class Markdown implements Component {
137
169
  // Replace tabs with 3 spaces for consistent rendering
138
170
  const normalizedText = replaceTabs(this.#text);
139
171
 
172
+ // L2: module-level LRU — survives component disposal/recreation across
173
+ // session-tree navigations. Key encodes every dimension that affects the
174
+ // render output so different configurations never collide.
175
+ const cacheKey = `${normalizedText}\x00${width}\x00${this.#paddingX}\x00${this.#paddingY}\x00${this.#codeBlockIndent}\x00${objectId(this.#theme)}\x00${this.#defaultTextStyle ? objectId(this.#defaultTextStyle) : -1}`;
176
+ const cached = renderCache.get(cacheKey);
177
+ if (cached !== undefined) {
178
+ // Populate L1 so subsequent calls from this instance are O(1) map lookup.
179
+ this.#cachedText = this.#text;
180
+ this.#cachedWidth = width;
181
+ this.#cachedLines = cached;
182
+ return cached;
183
+ }
184
+
140
185
  // Parse markdown to HTML-like tokens
141
186
  const tokens = marked.lexer(normalizedText);
142
187
 
@@ -195,14 +240,19 @@ export class Markdown implements Component {
195
240
  }
196
241
 
197
242
  // Combine top padding, content, and bottom padding
198
- const result = [...emptyLines, ...contentLines, ...emptyLines];
243
+ const rawResult = [...emptyLines, ...contentLines, ...emptyLines];
244
+ const result = rawResult.length > 0 ? rawResult : [""];
199
245
 
200
- // Update cache
246
+ // Update L1 per-instance cache
201
247
  this.#cachedText = this.#text;
202
248
  this.#cachedWidth = width;
203
249
  this.#cachedLines = result;
204
250
 
205
- return result.length > 0 ? result : [""];
251
+ // Update L2 module-level LRU so future instances with the same key skip
252
+ // the marked.lexer + highlightCode (Rust FFI) work entirely.
253
+ renderCache.set(cacheKey, result);
254
+
255
+ return result;
206
256
  }
207
257
 
208
258
  /**
package/src/utils.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { Ellipsis, ExtractSegmentsResult, SliceResult } from "@oh-my-pi/pi-natives";
2
1
  import {
2
+ Ellipsis,
3
+ type ExtractSegmentsResult,
3
4
  extractSegments as nativeExtractSegments,
4
5
  sliceWithWidth as nativeSliceWithWidth,
5
6
  truncateToWidth as nativeTruncateToWidth,
6
7
  wrapTextWithAnsi as nativeWrapTextWithAnsi,
8
+ type SliceResult,
7
9
  } from "@oh-my-pi/pi-natives";
8
10
  import { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
9
11
 
@@ -21,7 +23,12 @@ export function truncateToWidth(
21
23
  ellipsisKind?: Ellipsis | null,
22
24
  pad?: boolean | null,
23
25
  ): string {
24
- return nativeTruncateToWidth(text, maxWidth, ellipsisKind ?? null, pad ?? null, getDefaultTabWidth());
26
+ // Guard nullish napi inputs: napi-rs 3 on the Windows prebuilt rejects
27
+ // `null` for `Option<u8>` (Ellipsis) / `Option<bool>` (pad) (issue #848),
28
+ // and `maxWidth` is a required `u32` that throws on `null`/`undefined`
29
+ // everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s.
30
+ const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0;
31
+ return nativeTruncateToWidth(text, safeWidth, ellipsisKind ?? Ellipsis.Unicode, pad ?? false, getDefaultTabWidth());
25
32
  }
26
33
 
27
34
  export function wrapTextWithAnsi(text: string, width: number): string[] {