@prometheus-ai/tui 0.5.3 → 0.5.8

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 (55) hide show
  1. package/dist/types/autocomplete.d.ts +3 -1
  2. package/dist/types/components/box.d.ts +1 -1
  3. package/dist/types/components/editor.d.ts +35 -2
  4. package/dist/types/components/image.d.ts +22 -3
  5. package/dist/types/components/input.d.ts +6 -1
  6. package/dist/types/components/loader.d.ts +9 -2
  7. package/dist/types/components/markdown.d.ts +3 -1
  8. package/dist/types/components/scroll-view.d.ts +23 -1
  9. package/dist/types/components/select-list.d.ts +19 -1
  10. package/dist/types/components/settings-list.d.ts +87 -7
  11. package/dist/types/components/spacer.d.ts +1 -1
  12. package/dist/types/components/tab-bar.d.ts +37 -4
  13. package/dist/types/components/text.d.ts +2 -2
  14. package/dist/types/components/truncated-text.d.ts +1 -1
  15. package/dist/types/fuzzy.d.ts +10 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/keybindings.d.ts +5 -3
  18. package/dist/types/keys.d.ts +1 -1
  19. package/dist/types/kill-ring.d.ts +0 -7
  20. package/dist/types/kitty-graphics.d.ts +16 -31
  21. package/dist/types/loop-watchdog.d.ts +39 -0
  22. package/dist/types/mouse.d.ts +41 -0
  23. package/dist/types/stdin-buffer.d.ts +17 -0
  24. package/dist/types/terminal-capabilities.d.ts +74 -18
  25. package/dist/types/terminal.d.ts +34 -36
  26. package/dist/types/tui.d.ts +191 -79
  27. package/dist/types/utils.d.ts +5 -2
  28. package/package.json +4 -4
  29. package/src/autocomplete.ts +79 -65
  30. package/src/components/box.ts +43 -63
  31. package/src/components/editor.ts +471 -136
  32. package/src/components/image.ts +85 -9
  33. package/src/components/input.ts +12 -3
  34. package/src/components/loader.ts +35 -21
  35. package/src/components/markdown.ts +174 -53
  36. package/src/components/scroll-view.ts +63 -2
  37. package/src/components/select-list.ts +233 -38
  38. package/src/components/settings-list.ts +626 -64
  39. package/src/components/spacer.ts +9 -5
  40. package/src/components/tab-bar.ts +153 -28
  41. package/src/components/text.ts +6 -2
  42. package/src/components/truncated-text.ts +10 -2
  43. package/src/fuzzy.ts +214 -59
  44. package/src/index.ts +3 -1
  45. package/src/keybindings.ts +72 -14
  46. package/src/keys.ts +1 -1
  47. package/src/kill-ring.ts +5 -0
  48. package/src/kitty-graphics.ts +2 -101
  49. package/src/loop-watchdog.ts +106 -0
  50. package/src/mouse.ts +55 -0
  51. package/src/stdin-buffer.ts +291 -81
  52. package/src/terminal-capabilities.ts +206 -168
  53. package/src/terminal.ts +367 -110
  54. package/src/tui.ts +2102 -1729
  55. package/src/utils.ts +92 -60
package/src/utils.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  extractSegments as nativeExtractSegments,
5
5
  sliceWithWidth as nativeSliceWithWidth,
6
6
  truncateToWidth as nativeTruncateToWidth,
7
- visibleWidth as nativeVisibleWidth,
8
7
  wrapTextWithAnsi as nativeWrapTextWithAnsi,
9
8
  type SliceResult,
10
9
  } from "@prometheus-ai/natives";
@@ -145,44 +144,6 @@ export function padding(n: number): string {
145
144
  // Grapheme segmenter (shared instance)
146
145
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
147
146
 
148
- const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
149
-
150
- // Matches CSI (`\x1b[…`) and OSC (`\x1b]…` terminated by BEL/ST) escape
151
- // sequences. Mirrors the standard ansi-regex coverage so visible-span
152
- // segmentation lines up with the native ANSI scanner.
153
- const ANSI_ESCAPE_REGEX =
154
- /[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
155
-
156
- function pictographicSpanWidth(span: string): number {
157
- let width = 0;
158
- for (const { segment } of segmenter.segment(span)) {
159
- width += EXTENDED_PICTOGRAPHIC_REGEX.test(segment) ? 2 : nativeVisibleWidth(segment, getDefaultTabWidth());
160
- }
161
- return width;
162
- }
163
-
164
- // Width fallback for strings that mix ANSI styling with ZWJ pictographic
165
- // emoji. `Intl.Segmenter` would split an escape sequence into individual
166
- // graphemes, so the native scanner (which only skips ANSI when handed the
167
- // complete sequence) double-counts the printable SGR bytes. Excise the ANSI
168
- // spans first — they contribute zero cells — and apply the pictographic
169
- // grapheme override only to the visible spans, then sum.
170
- function visibleWidthByGrapheme(str: string): number {
171
- let width = 0;
172
- let lastIndex = 0;
173
- ANSI_ESCAPE_REGEX.lastIndex = 0;
174
- for (let match = ANSI_ESCAPE_REGEX.exec(str); match !== null; match = ANSI_ESCAPE_REGEX.exec(str)) {
175
- if (match.index > lastIndex) {
176
- width += pictographicSpanWidth(str.slice(lastIndex, match.index));
177
- }
178
- lastIndex = ANSI_ESCAPE_REGEX.lastIndex;
179
- }
180
- if (lastIndex < str.length) {
181
- width += lastIndex === 0 ? pictographicSpanWidth(str) : pictographicSpanWidth(str.slice(lastIndex));
182
- }
183
- return width;
184
- }
185
-
186
147
  /**
187
148
  * Get the shared grapheme segmenter instance.
188
149
  */
@@ -190,35 +151,106 @@ export function getSegmenter(): Intl.Segmenter {
190
151
  return segmenter;
191
152
  }
192
153
 
193
- export function visibleWidthRaw(str: string): number {
194
- if (!str) {
195
- return 0;
154
+ // Kitty OSC 66 text-sizing spans: `\x1b]66;<meta>;<payload>` terminated by BEL
155
+ // or ST. `Bun.stringWidth` strips the whole span (payload included) to zero
156
+ // cells, but the payload is visible and scales by the `s=` factor, so each is
157
+ // added back so width matches the native truncate/slice/wrap helpers.
158
+ const OSC66_SPAN_REGEX = /\x1b\]66;([^;]*);([\s\S]*?)(?:\x07|\x1b\\)/g;
159
+ const OSC66_PREFIX = "\x1b]66;";
160
+ const ESC = "\x1b";
161
+ const TAB = "\t";
162
+ const LONG_WIDTH_FAST_PATH_MIN = 128;
163
+
164
+ // Pin Bun.stringWidth semantics to the native width engine and guard against Bun
165
+ // default drift: strip ANSI/OSC (don't count escape bytes) and treat
166
+ // ambiguous-width East Asian chars as narrow (1 cell), matching `unicode-width`'s
167
+ // non-CJK tables that back truncate/slice/wrap. Hoisted so no per-call alloc.
168
+ const STRING_WIDTH_OPTS = { countAnsiEscapeCodes: false, ambiguousIsNarrow: true } as const;
169
+
170
+ /**
171
+ * Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
172
+ *
173
+ * `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
174
+ * stripping); this adds the two corrections it omits — tabs (expanded to
175
+ * `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
176
+ */
177
+ export function visibleWidth(str: string): number {
178
+ if (!str) return 0;
179
+
180
+ // Long non-escape text is faster through Bun's native scanner than through
181
+ // a JS printable-ASCII prepass. Escape-bearing strings stay on the scanner
182
+ // below so CSI/OSC-heavy render output can still bail out at the first ESC.
183
+ if (str.length >= LONG_WIDTH_FAST_PATH_MIN && !str.includes(ESC)) {
184
+ let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
185
+ let tabCount = 0;
186
+ for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
187
+ tabCount++;
188
+ }
189
+ if (tabCount > 0) width += tabCount * getDefaultTabWidth();
190
+ return width;
196
191
  }
197
192
 
198
- // Fast path: printable ASCII has one cell per code unit. Defer every
199
- // control/non-ASCII case (tabs, ANSI/OSC, combining marks, CJK) to the
200
- // native text engine so all width/slice/wrap helpers share one Unicode
201
- // model instead of mixing Bun.stringWidth quirks with Rust truncation.
202
- for (let i = 0; i < str.length; i++) {
193
+ let tabCount = 0;
194
+ let i = 0;
195
+ for (; i < str.length; i++) {
203
196
  const code = str.charCodeAt(i);
204
197
  if (code < 0x20 || code > 0x7e) {
205
- const tabWidth = getDefaultTabWidth();
206
- if (str.includes("\x1b]66;")) return nativeVisibleWidth(str, tabWidth);
207
- return str.includes("\u200d") ? visibleWidthByGrapheme(str) : nativeVisibleWidth(str, tabWidth);
198
+ if (code === 0x09) {
199
+ tabCount++;
200
+ continue;
201
+ }
202
+ break;
208
203
  }
209
204
  }
210
- return str.length;
211
- }
205
+ if (i === str.length) {
206
+ return tabCount === 0 ? str.length : str.length + tabCount * (getDefaultTabWidth() - 1);
207
+ }
212
208
 
213
- /**
214
- * Calculate the visible width of a string in terminal columns.
215
- */
216
- export function visibleWidth(str: string): number {
217
- if (!str) return 0;
218
- return visibleWidthRaw(str);
209
+ if (tabCount === 0) {
210
+ let tabIndex = str.indexOf(TAB, i + 1);
211
+ if (tabIndex !== -1) {
212
+ tabCount = 1;
213
+ for (tabIndex = str.indexOf(TAB, tabIndex + 1); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
214
+ tabCount++;
215
+ }
216
+ }
217
+ } else {
218
+ for (let tabIndex = str.indexOf(TAB, i + 1); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
219
+ tabCount++;
220
+ }
221
+ }
222
+
223
+ // `Bun.stringWidth` is a JSC builtin (no per-call N-API number box, unlike
224
+ // the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
225
+ // CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
226
+ let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
227
+ if (tabCount > 0) width += tabCount * getDefaultTabWidth();
228
+
229
+ // OSC 66: add back each stripped span as `scale * (explicit w ?? payload
230
+ // width)`. Matched rather than replaced to avoid reallocating the string.
231
+ if (str.includes(OSC66_PREFIX, i)) {
232
+ OSC66_SPAN_REGEX.lastIndex = 0;
233
+ for (let m = OSC66_SPAN_REGEX.exec(str); m !== null; m = OSC66_SPAN_REGEX.exec(str)) {
234
+ let scale = 1;
235
+ let explicit: number | undefined;
236
+ for (const part of m[1].split(":")) {
237
+ // metadata keys are single chars, e.g. `s=2`, `w=5`
238
+ if (part.indexOf("=") !== 1) continue;
239
+ const value = Number.parseInt(part.slice(2), 10);
240
+ if (!Number.isFinite(value)) continue;
241
+ if (part[0] === "s") {
242
+ if (value >= 1 && value <= 7) scale = value;
243
+ } else if (part[0] === "w" && value > 0) {
244
+ explicit = value;
245
+ }
246
+ }
247
+ width += scale * (explicit ?? Bun.stringWidth(m[2], STRING_WIDTH_OPTS));
248
+ }
249
+ }
250
+
251
+ return width;
219
252
  }
220
253
 
221
- const THAI_LAO_AM_REGEX = /[\u0e33\u0eb3]/;
222
254
  const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g;
223
255
 
224
256
  /**
@@ -228,7 +260,7 @@ const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g;
228
260
  * width but avoid stale-cell artifacts in terminal renderers.
229
261
  */
230
262
  export function normalizeTerminalOutput(str: string): string {
231
- if (!THAI_LAO_AM_REGEX.test(str)) return str;
263
+ if (str.indexOf("\u0e33") === -1 && str.indexOf("\u0eb3") === -1) return str;
232
264
  return str.replace(THAI_LAO_AM_GLOBAL_REGEX, char => (char === "\u0e33" ? "\u0e4d\u0e32" : "\u0ecd\u0eb2"));
233
265
  }
234
266