@sayknow-cli/tui 0.3.7 → 0.3.9

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/src/utils.ts CHANGED
@@ -3,16 +3,31 @@ import {
3
3
  type ExtractSegmentsResult,
4
4
  extractSegments as nativeExtractSegments,
5
5
  sliceWithWidth as nativeSliceWithWidth,
6
+ truncateLinesToWidth as nativeTruncateLinesToWidth,
6
7
  truncateToWidth as nativeTruncateToWidth,
8
+ visibleWidths as nativeVisibleWidths,
7
9
  wrapTextWithAnsi as nativeWrapTextWithAnsi,
8
10
  type SliceResult,
9
11
  } from "@sayknow-cli/natives";
10
- import { getDefaultTabWidth, getIndentation } from "@sayknow-cli/utils";
12
+ import { getDefaultTabWidth, getIndentation, onDefaultTabWidthChange } from "@sayknow-cli/utils";
11
13
  import { renderMetrics } from "./metrics";
12
14
 
13
15
  export { Ellipsis } from "@sayknow-cli/natives";
14
16
 
15
17
  export { getDefaultTabWidth, getIndentation } from "@sayknow-cli/utils";
18
+ /** Test-only performance counters for advisory baseline tests. */
19
+ export const __textHelperPerfCounters = {
20
+ truncateToWidthCalls: 0,
21
+ wrapTextWithAnsiCalls: 0,
22
+ truncateLinesToWidthCalls: 0,
23
+ visibleWidthsCalls: 0,
24
+ reset(): void {
25
+ this.truncateToWidthCalls = 0;
26
+ this.wrapTextWithAnsiCalls = 0;
27
+ this.truncateLinesToWidthCalls = 0;
28
+ this.visibleWidthsCalls = 0;
29
+ },
30
+ };
16
31
 
17
32
  function recordTextHelper<T>(name: string, fn: () => T): T {
18
33
  if (!renderMetrics.enabled) return fn();
@@ -24,6 +39,18 @@ function recordTextHelper<T>(name: string, fn: () => T): T {
24
39
  }
25
40
  }
26
41
 
42
+ let cachedTabWidth: number | undefined;
43
+
44
+ function getCachedTabWidth(): number {
45
+ cachedTabWidth ??= getDefaultTabWidth();
46
+ return cachedTabWidth;
47
+ }
48
+
49
+ export function invalidateTabWidthCache(): void {
50
+ cachedTabWidth = undefined;
51
+ }
52
+ onDefaultTabWidthChange(invalidateTabWidthCache);
53
+
27
54
  export function isPrintableAscii(text: string): boolean {
28
55
  for (let i = 0; i < text.length; i++) {
29
56
  const code = text.charCodeAt(i);
@@ -33,7 +60,7 @@ export function isPrintableAscii(text: string): boolean {
33
60
  }
34
61
 
35
62
  export function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult {
36
- return nativeSliceWithWidth(line, startCol, length, strict ?? null, getDefaultTabWidth());
63
+ return nativeSliceWithWidth(line, startCol, length, strict ?? null, getCachedTabWidth());
37
64
  }
38
65
 
39
66
  export function truncateToWidth(
@@ -42,26 +69,53 @@ export function truncateToWidth(
42
69
  ellipsisKind?: Ellipsis | null,
43
70
  pad?: boolean | null,
44
71
  ): string {
72
+ __textHelperPerfCounters.truncateToWidthCalls += 1;
45
73
  // Guard nullish napi inputs: napi-rs 3 on the Windows prebuilt rejects
46
74
  // `null` for `Option<u8>` (Ellipsis) / `Option<bool>` (pad) (issue #848),
47
75
  // and `maxWidth` is a required `u32` that throws on `null`/`undefined`
48
- // everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s.
76
+ // everywhere. The `text` arg is a required `String` that likewise throws on
77
+ // `null`/`undefined` on every platform, which crashed renderers that passed
78
+ // an optional/possibly-undefined field. Pass concrete defaults that mirror
79
+ // the Rust `unwrap_or`s.
80
+ const safeText = typeof text === "string" ? text : String(text ?? "");
49
81
  const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0;
50
82
  let resolvedEllipsis: Ellipsis | null | undefined | string = ellipsisKind;
51
83
  if (typeof resolvedEllipsis === "string") {
52
84
  resolvedEllipsis = resolvedEllipsis === "" ? Ellipsis.Omit : Ellipsis.Unicode;
53
85
  }
54
86
  return nativeTruncateToWidth(
55
- text,
87
+ safeText,
88
+ safeWidth,
89
+ resolvedEllipsis ?? Ellipsis.Unicode,
90
+ pad ?? false,
91
+ getCachedTabWidth(),
92
+ );
93
+ }
94
+
95
+ export function truncateLinesToWidth(
96
+ lines: readonly string[],
97
+ maxWidth: number,
98
+ ellipsisKind?: Ellipsis | null,
99
+ pad?: boolean | null,
100
+ ): string[] {
101
+ __textHelperPerfCounters.truncateLinesToWidthCalls += 1;
102
+ const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0;
103
+ let resolvedEllipsis: Ellipsis | null | undefined | string = ellipsisKind;
104
+ if (typeof resolvedEllipsis === "string") {
105
+ resolvedEllipsis = resolvedEllipsis === "" ? Ellipsis.Omit : Ellipsis.Unicode;
106
+ }
107
+ return nativeTruncateLinesToWidth(
108
+ lines.map(line => (typeof line === "string" ? line : String(line ?? ""))),
56
109
  safeWidth,
57
110
  resolvedEllipsis ?? Ellipsis.Unicode,
58
111
  pad ?? false,
59
- getDefaultTabWidth(),
112
+ getCachedTabWidth(),
60
113
  );
61
114
  }
62
115
 
63
116
  export function wrapTextWithAnsi(text: string, width: number): string[] {
64
- return nativeWrapTextWithAnsi(text, width, getDefaultTabWidth());
117
+ __textHelperPerfCounters.wrapTextWithAnsiCalls += 1;
118
+ return nativeWrapTextWithAnsi(text, width, getCachedTabWidth());
65
119
  }
66
120
 
67
121
  export function extractSegments(
@@ -71,7 +125,7 @@ export function extractSegments(
71
125
  afterLen: number,
72
126
  strictAfter: boolean,
73
127
  ): ExtractSegmentsResult {
74
- return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, getDefaultTabWidth());
128
+ return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, getCachedTabWidth());
75
129
  }
76
130
 
77
131
  // Pre-allocated space buffer for padding
@@ -120,7 +174,7 @@ export function visibleWidthRaw(str: string): number {
120
174
  if (str.length === 1) {
121
175
  const code = str.charCodeAt(0);
122
176
  if (code >= 0x20 && code <= 0x7e) return 1;
123
- if (code === 9) return getDefaultTabWidth();
177
+ if (code === 9) return getCachedTabWidth();
124
178
  }
125
179
 
126
180
  let tabCount = 0;
@@ -134,12 +188,11 @@ export function visibleWidthRaw(str: string): number {
134
188
  }
135
189
  }
136
190
  if (isPureAscii) {
137
- return str.length + tabCount * (getDefaultTabWidth() - 1);
191
+ return str.length + tabCount * (getCachedTabWidth() - 1);
138
192
  }
139
-
140
193
  const normalized = normalizeForWidth(str);
141
194
  if (tabCount === 0) return Bun.stringWidth(normalized);
142
- return Bun.stringWidth(normalized.replaceAll("\t", " ".repeat(getDefaultTabWidth())));
195
+ return Bun.stringWidth(normalized.replaceAll("\t", " ".repeat(getCachedTabWidth())));
143
196
  }
144
197
 
145
198
  /**
@@ -150,6 +203,19 @@ export function visibleWidth(str: string): number {
150
203
  return recordTextHelper("text.visibleWidth", () => visibleWidthRaw(str));
151
204
  }
152
205
 
206
+ export function visibleWidthsNative(lines: readonly string[]): number[] {
207
+ __textHelperPerfCounters.visibleWidthsCalls += 1;
208
+ return nativeVisibleWidths(
209
+ lines.map(line => (typeof line === "string" ? line : String(line ?? ""))),
210
+ getCachedTabWidth(),
211
+ );
212
+ }
213
+
214
+ export function visibleWidths(lines: readonly string[]): number[] {
215
+ void visibleWidthsNative(lines);
216
+ return lines.map(line => visibleWidth(typeof line === "string" ? line : String(line ?? "")));
217
+ }
218
+
153
219
  const THAI_LAO_AM_REGEX = /[\u0e33\u0eb3]/;
154
220
  const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g;
155
221
  const HANGUL_JAMO_REGEX = /[\u1100-\u11ff\ua960-\ua97f\ud7b0-\ud7ff]/;