@oh-my-pi/pi-tui 11.8.2 → 11.8.3

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/README.md CHANGED
@@ -574,8 +574,8 @@ class MyInteractiveComponent implements Component {
574
574
  private selectedIndex = 0;
575
575
  private items = ["Option 1", "Option 2", "Option 3"];
576
576
 
577
- public onSelect?: (index: number) => void;
578
- public onCancel?: () => void;
577
+ onSelect?: (index: number) => void;
578
+ onCancel?: () => void;
579
579
 
580
580
  handleInput(data: string): void {
581
581
  if (isArrowUp(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "11.8.2",
3
+ "version": "11.8.3",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -47,8 +47,8 @@
47
47
  "bun": ">=1.3.7"
48
48
  },
49
49
  "dependencies": {
50
- "@oh-my-pi/pi-natives": "11.8.2",
51
- "@oh-my-pi/pi-utils": "11.8.2",
50
+ "@oh-my-pi/pi-natives": "11.8.3",
51
+ "@oh-my-pi/pi-utils": "11.8.3",
52
52
  "@types/mime-types": "^3.0.1",
53
53
  "chalk": "^5.6.2",
54
54
  "marked": "^17.0.1",
@@ -169,14 +169,14 @@ export interface AutocompleteProvider {
169
169
 
170
170
  // Combined provider that handles both slash commands and file paths
171
171
  export class CombinedAutocompleteProvider implements AutocompleteProvider {
172
- private commands: (SlashCommand | AutocompleteItem)[];
173
- private basePath: string;
174
- private dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
175
- private readonly DIR_CACHE_TTL = 2000; // 2 seconds
172
+ #commands: (SlashCommand | AutocompleteItem)[];
173
+ #basePath: string;
174
+ #dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
175
+ readonly #DIR_CACHE_TTL = 2000; // 2 seconds
176
176
 
177
177
  constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {
178
- this.commands = commands;
179
- this.basePath = basePath;
178
+ this.#commands = commands;
179
+ this.#basePath = basePath;
180
180
  }
181
181
 
182
182
  async getSuggestions(
@@ -188,15 +188,15 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
188
188
  const textBeforeCursor = currentLine.slice(0, cursorCol);
189
189
 
190
190
  // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
191
- const atPrefix = this.extractAtPrefix(textBeforeCursor);
191
+ const atPrefix = this.#extractAtPrefix(textBeforeCursor);
192
192
  if (atPrefix) {
193
193
  const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
194
194
  const suggestions =
195
195
  rawPrefix.length > 0
196
- ? await this.getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
197
- : await this.getFileSuggestions("@");
196
+ ? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
197
+ : await this.#getFileSuggestions("@");
198
198
  if (suggestions.length === 0 && rawPrefix.length > 0) {
199
- const fallback = await this.getFileSuggestions(atPrefix);
199
+ const fallback = await this.#getFileSuggestions(atPrefix);
200
200
  if (fallback.length === 0) return null;
201
201
  return { items: fallback, prefix: atPrefix };
202
202
  }
@@ -218,7 +218,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
218
218
  const lowerPrefix = prefix.toLowerCase();
219
219
 
220
220
  // Filter commands using fuzzy matching (subsequence match)
221
- const matches = this.commands
221
+ const matches = this.#commands
222
222
  .filter(cmd => {
223
223
  const name = "name" in cmd ? cmd.name : cmd.value;
224
224
  if (!name) return false;
@@ -255,7 +255,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
255
255
  const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
256
256
  const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
257
257
 
258
- const command = this.commands.find(cmd => {
258
+ const command = this.#commands.find(cmd => {
259
259
  const name = "name" in cmd ? cmd.name : cmd.value;
260
260
  return name === commandName;
261
261
  });
@@ -276,10 +276,10 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
276
276
  }
277
277
 
278
278
  // Check for file paths - triggered by Tab or if we detect a path pattern
279
- const pathMatch = this.extractPathPrefix(textBeforeCursor, false);
279
+ const pathMatch = this.#extractPathPrefix(textBeforeCursor, false);
280
280
 
281
281
  if (pathMatch !== null) {
282
- const suggestions = await this.getFileSuggestions(pathMatch);
282
+ const suggestions = await this.#getFileSuggestions(pathMatch);
283
283
  if (suggestions.length === 0) return null;
284
284
 
285
285
  // Check if we have an exact match that is a directory
@@ -372,7 +372,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
372
372
  }
373
373
 
374
374
  // Extract @ prefix for fuzzy file suggestions
375
- private extractAtPrefix(text: string): string | null {
375
+ #extractAtPrefix(text: string): string | null {
376
376
  const quotedPrefix = extractQuotedPrefix(text);
377
377
  if (quotedPrefix?.startsWith('@"')) {
378
378
  return quotedPrefix;
@@ -389,7 +389,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
389
389
  }
390
390
 
391
391
  // Extract a path-like prefix from the text before cursor
392
- private extractPathPrefix(text: string, forceExtract: boolean = false): string | null {
392
+ #extractPathPrefix(text: string, forceExtract: boolean = false): string | null {
393
393
  const quotedPrefix = extractQuotedPrefix(text);
394
394
  if (quotedPrefix) {
395
395
  return quotedPrefix;
@@ -419,7 +419,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
419
419
  }
420
420
 
421
421
  // Expand home directory (~/) to actual home path
422
- private expandHomePath(filePath: string): string {
422
+ #expandHomePath(filePath: string): string {
423
423
  if (filePath.startsWith("~/")) {
424
424
  const expandedPath = path.join(os.homedir(), filePath.slice(2));
425
425
  // Preserve trailing slash if original path had one
@@ -430,40 +430,40 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
430
430
  return filePath;
431
431
  }
432
432
 
433
- private async getCachedDirEntries(searchDir: string): Promise<fs.Dirent[]> {
433
+ async #getCachedDirEntries(searchDir: string): Promise<fs.Dirent[]> {
434
434
  const now = Date.now();
435
- const cached = this.dirCache.get(searchDir);
435
+ const cached = this.#dirCache.get(searchDir);
436
436
 
437
- if (cached && now - cached.timestamp < this.DIR_CACHE_TTL) {
437
+ if (cached && now - cached.timestamp < this.#DIR_CACHE_TTL) {
438
438
  return cached.entries;
439
439
  }
440
440
 
441
441
  const entries = await fs.promises.readdir(searchDir, { withFileTypes: true });
442
- this.dirCache.set(searchDir, { entries, timestamp: now });
442
+ this.#dirCache.set(searchDir, { entries, timestamp: now });
443
443
 
444
- if (this.dirCache.size > 100) {
445
- const sortedKeys = [...this.dirCache.entries()]
444
+ if (this.#dirCache.size > 100) {
445
+ const sortedKeys = [...this.#dirCache.entries()]
446
446
  .sort((a, b) => a[1].timestamp - b[1].timestamp)
447
447
  .slice(0, 50)
448
448
  .map(([key]) => key);
449
449
  for (const key of sortedKeys) {
450
- this.dirCache.delete(key);
450
+ this.#dirCache.delete(key);
451
451
  }
452
452
  }
453
453
 
454
454
  return entries;
455
455
  }
456
456
 
457
- public invalidateDirCache(dir?: string): void {
457
+ invalidateDirCache(dir?: string): void {
458
458
  if (dir) {
459
- this.dirCache.delete(dir);
459
+ this.#dirCache.delete(dir);
460
460
  } else {
461
- this.dirCache.clear();
461
+ this.#dirCache.clear();
462
462
  }
463
463
  }
464
464
 
465
465
  // Get file/directory suggestions for a given path prefix
466
- private async getFileSuggestions(prefix: string): Promise<AutocompleteItem[]> {
466
+ async #getFileSuggestions(prefix: string): Promise<AutocompleteItem[]> {
467
467
  try {
468
468
  let searchDir: string;
469
469
  let searchPrefix: string;
@@ -472,7 +472,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
472
472
 
473
473
  // Handle home directory expansion
474
474
  if (expandedPrefix.startsWith("~")) {
475
- expandedPrefix = this.expandHomePath(expandedPrefix);
475
+ expandedPrefix = this.#expandHomePath(expandedPrefix);
476
476
  }
477
477
 
478
478
  const isRootPrefix =
@@ -489,7 +489,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
489
489
  if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) {
490
490
  searchDir = expandedPrefix;
491
491
  } else {
492
- searchDir = path.join(this.basePath, expandedPrefix);
492
+ searchDir = path.join(this.#basePath, expandedPrefix);
493
493
  }
494
494
  searchPrefix = "";
495
495
  } else if (rawPrefix.endsWith("/")) {
@@ -497,7 +497,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
497
497
  if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) {
498
498
  searchDir = expandedPrefix;
499
499
  } else {
500
- searchDir = path.join(this.basePath, expandedPrefix);
500
+ searchDir = path.join(this.#basePath, expandedPrefix);
501
501
  }
502
502
  searchPrefix = "";
503
503
  } else {
@@ -507,12 +507,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
507
507
  if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) {
508
508
  searchDir = dir;
509
509
  } else {
510
- searchDir = path.join(this.basePath, dir);
510
+ searchDir = path.join(this.#basePath, dir);
511
511
  }
512
512
  searchPrefix = file;
513
513
  }
514
514
 
515
- const entries = await this.getCachedDirEntries(searchDir);
515
+ const entries = await this.#getCachedDirEntries(searchDir);
516
516
  const suggestions: AutocompleteItem[] = [];
517
517
 
518
518
  for (const entry of entries) {
@@ -600,7 +600,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
600
600
 
601
601
  // Score an entry against the query (higher = better match)
602
602
  // isDirectory adds bonus to prioritize folders
603
- private scoreEntry(filePath: string, query: string, isDirectory: boolean): number {
603
+ #scoreEntry(filePath: string, query: string, isDirectory: boolean): number {
604
604
  const fileName = path.basename(filePath);
605
605
  const lowerFileName = fileName.toLowerCase();
606
606
  const lowerQuery = query.toLowerCase();
@@ -622,14 +622,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
622
622
  return score;
623
623
  }
624
624
 
625
- private async getFuzzyFileSuggestions(
626
- query: string,
627
- options: { isQuotedPrefix: boolean },
628
- ): Promise<AutocompleteItem[]> {
625
+ async #getFuzzyFileSuggestions(query: string, options: { isQuotedPrefix: boolean }): Promise<AutocompleteItem[]> {
629
626
  try {
630
627
  const result = await fuzzyFind({
631
628
  query,
632
- path: this.basePath,
629
+ path: this.#basePath,
633
630
  maxResults: 100,
634
631
  hidden: true,
635
632
  gitignore: true,
@@ -647,7 +644,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
647
644
  .map(entry => ({
648
645
  path: entry.path,
649
646
  isDirectory: entry.isDirectory,
650
- score: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,
647
+ score: query ? this.#scoreEntry(entry.path, query, entry.isDirectory) : 1,
651
648
  }))
652
649
  .filter(entry => entry.score > 0);
653
650
 
@@ -692,9 +689,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
692
689
  }
693
690
 
694
691
  // Force extract path prefix - this will always return something
695
- const pathMatch = this.extractPathPrefix(textBeforeCursor, true);
692
+ const pathMatch = this.#extractPathPrefix(textBeforeCursor, true);
696
693
  if (pathMatch !== null) {
697
- const suggestions = await this.getFileSuggestions(pathMatch);
694
+ const suggestions = await this.#getFileSuggestions(pathMatch);
698
695
  if (suggestions.length === 0) return null;
699
696
 
700
697
  return {
@@ -11,66 +11,66 @@ type Cache = {
11
11
  */
12
12
  export class Box implements Component {
13
13
  children: Component[] = [];
14
- private paddingX: number;
15
- private paddingY: number;
16
- private bgFn?: (text: string) => string;
14
+ #paddingX: number;
15
+ #paddingY: number;
16
+ #bgFn?: (text: string) => string;
17
17
 
18
18
  // Cache for rendered output
19
- private cached?: Cache;
19
+ #cached?: Cache;
20
20
 
21
21
  constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
22
- this.paddingX = paddingX;
23
- this.paddingY = paddingY;
24
- this.bgFn = bgFn;
22
+ this.#paddingX = paddingX;
23
+ this.#paddingY = paddingY;
24
+ this.#bgFn = bgFn;
25
25
  }
26
26
 
27
27
  addChild(component: Component): void {
28
28
  this.children.push(component);
29
- this.invalidateCache();
29
+ this.#invalidateCache();
30
30
  }
31
31
 
32
32
  removeChild(component: Component): void {
33
33
  const index = this.children.indexOf(component);
34
34
  if (index !== -1) {
35
35
  this.children.splice(index, 1);
36
- this.invalidateCache();
36
+ this.#invalidateCache();
37
37
  }
38
38
  }
39
39
 
40
40
  clear(): void {
41
41
  this.children = [];
42
- this.invalidateCache();
42
+ this.#invalidateCache();
43
43
  }
44
44
 
45
45
  setBgFn(bgFn?: (text: string) => string): void {
46
- this.bgFn = bgFn;
46
+ this.#bgFn = bgFn;
47
47
  // Don't invalidate here - we'll detect bgFn changes by sampling output
48
48
  }
49
49
 
50
- private invalidateCache(): void {
51
- this.cached = undefined;
50
+ #invalidateCache(): void {
51
+ this.#cached = undefined;
52
52
  }
53
53
 
54
- private static _tmp = new Uint32Array(2);
55
- private computeCacheKey(width: number, childLines: string[], bgSample: string | undefined): bigint {
56
- Box._tmp[0] = width;
57
- Box._tmp[1] = childLines.length;
58
- let h = Bun.hash.xxHash64(Box._tmp);
54
+ static #tmp = new Uint32Array(2);
55
+ #computeCacheKey(width: number, childLines: string[], bgSample: string | undefined): bigint {
56
+ Box.#tmp[0] = width;
57
+ Box.#tmp[1] = childLines.length;
58
+ let h = Bun.hash.xxHash64(Box.#tmp);
59
59
  for (const line of childLines) {
60
- Box._tmp[0] = line.length;
61
- h = Bun.hash.xxHash64(Box._tmp, h);
60
+ Box.#tmp[0] = line.length;
61
+ h = Bun.hash.xxHash64(Box.#tmp, h);
62
62
  h = Bun.hash.xxHash64(line, h);
63
63
  }
64
64
  h = Bun.hash.xxHash64(bgSample ?? "", h);
65
65
  return h;
66
66
  }
67
67
 
68
- private matchCache(cacheKey: bigint): boolean {
69
- return this.cached?.key === cacheKey;
68
+ #matchCache(cacheKey: bigint): boolean {
69
+ return this.#cached?.key === cacheKey;
70
70
  }
71
71
 
72
72
  invalidate(): void {
73
- this.invalidateCache();
73
+ this.#invalidateCache();
74
74
  for (const child of this.children) {
75
75
  child.invalidate?.();
76
76
  }
@@ -81,8 +81,8 @@ export class Box implements Component {
81
81
  return [];
82
82
  }
83
83
 
84
- const contentWidth = Math.max(1, width - this.paddingX * 2);
85
- const leftPad = padding(this.paddingX);
84
+ const contentWidth = Math.max(1, width - this.#paddingX * 2);
85
+ const leftPad = padding(this.#paddingX);
86
86
 
87
87
  // Render all children
88
88
  const childLines: string[] = [];
@@ -98,46 +98,46 @@ export class Box implements Component {
98
98
  }
99
99
 
100
100
  // Check if bgFn output changed by sampling
101
- const bgSample = this.bgFn ? this.bgFn("test") : undefined;
101
+ const bgSample = this.#bgFn ? this.#bgFn("test") : undefined;
102
102
 
103
- const cacheKey = this.computeCacheKey(width, childLines, bgSample);
103
+ const cacheKey = this.#computeCacheKey(width, childLines, bgSample);
104
104
 
105
105
  // Check cache validity
106
- if (this.matchCache(cacheKey)) {
107
- return this.cached!.result;
106
+ if (this.#matchCache(cacheKey)) {
107
+ return this.#cached!.result;
108
108
  }
109
109
 
110
110
  // Apply background and padding
111
111
  const result: string[] = [];
112
112
 
113
113
  // Top padding
114
- for (let i = 0; i < this.paddingY; i++) {
115
- result.push(this.applyBg("", width));
114
+ for (let i = 0; i < this.#paddingY; i++) {
115
+ result.push(this.#applyBg("", width));
116
116
  }
117
117
 
118
118
  // Content
119
119
  for (const line of childLines) {
120
- result.push(this.applyBg(line, width));
120
+ result.push(this.#applyBg(line, width));
121
121
  }
122
122
 
123
123
  // Bottom padding
124
- for (let i = 0; i < this.paddingY; i++) {
125
- result.push(this.applyBg("", width));
124
+ for (let i = 0; i < this.#paddingY; i++) {
125
+ result.push(this.#applyBg("", width));
126
126
  }
127
127
 
128
128
  // Update cache
129
- this.cached = { key: cacheKey, result };
129
+ this.#cached = { key: cacheKey, result };
130
130
 
131
131
  return result;
132
132
  }
133
133
 
134
- private applyBg(line: string, width: number): string {
134
+ #applyBg(line: string, width: number): string {
135
135
  const visLen = visibleWidth(line);
136
136
  const padNeeded = Math.max(0, width - visLen);
137
137
  const padded = line + padding(padNeeded);
138
138
 
139
- if (this.bgFn) {
140
- return applyBackgroundToLine(padded, width, this.bgFn);
139
+ if (this.#bgFn) {
140
+ return applyBackgroundToLine(padded, width, this.#bgFn);
141
141
  }
142
142
  return padded;
143
143
  }
@@ -11,24 +11,24 @@ import { Loader } from "./loader";
11
11
  * doWork(loader.signal).then(done);
12
12
  */
13
13
  export class CancellableLoader extends Loader {
14
- private abortController = new AbortController();
14
+ #abortController = new AbortController();
15
15
 
16
16
  /** Called when user presses Escape */
17
17
  onAbort?: () => void;
18
18
 
19
19
  /** AbortSignal that is aborted when user presses Escape */
20
20
  get signal(): AbortSignal {
21
- return this.abortController.signal;
21
+ return this.#abortController.signal;
22
22
  }
23
23
 
24
24
  /** Whether the loader was aborted */
25
25
  get aborted(): boolean {
26
- return this.abortController.signal.aborted;
26
+ return this.#abortController.signal.aborted;
27
27
  }
28
28
 
29
29
  handleInput(data: string): void {
30
30
  if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
31
- this.abortController.abort();
31
+ this.#abortController.abort();
32
32
  this.onAbort?.();
33
33
  }
34
34
  }