@oh-my-pi/pi-tui 1.338.0 → 2.0.1337

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "1.338.0",
3
+ "version": "2.0.1337",
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",
@@ -1,6 +1,6 @@
1
- import { readdirSync, statSync } from "fs";
2
- import { homedir } from "os";
3
- import { basename, dirname, join } from "path";
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
4
 
5
5
  // Use fd to walk directory tree (fast, respects .gitignore)
6
6
  function walkDirectoryWithFd(
@@ -387,7 +387,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
387
387
  const fullPath = join(searchDir, entry.name);
388
388
  isDirectory = statSync(fullPath).isDirectory();
389
389
  } catch {
390
- // Broken symlink or permission error - treat as file
390
+ // Broken symlink, file deleted between readdir and stat, or permission error
391
+ continue;
391
392
  }
392
393
  }
393
394
 
@@ -459,7 +460,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
459
460
  });
460
461
 
461
462
  return suggestions;
462
- } catch (_e) {
463
+ } catch {
463
464
  // Directory doesn't exist or not accessible
464
465
  return [];
465
466
  }
@@ -527,6 +528,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
527
528
 
528
529
  return suggestions;
529
530
  } catch {
531
+ // Directory doesn't exist or not accessible
530
532
  return [];
531
533
  }
532
534
  }
@@ -1,5 +1,5 @@
1
- import type { Component } from "../tui.js";
2
- import { applyBackgroundToLine, visibleWidth } from "../utils.js";
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, visibleWidth } from "../utils";
3
3
 
4
4
  /**
5
5
  * Box component - a container that applies padding and background to all children
@@ -1,5 +1,5 @@
1
- import { isEscape } from "../keys.js";
2
- import { Loader } from "./loader.js";
1
+ import { isEscape } from "../keys";
2
+ import { Loader } from "./loader";
3
3
 
4
4
  /**
5
5
  * Loader that can be cancelled with Escape.
@@ -1,4 +1,4 @@
1
- import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js";
1
+ import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
2
2
  import {
3
3
  isAltBackspace,
4
4
  isAltEnter,
@@ -24,10 +24,10 @@ import {
24
24
  isHome,
25
25
  isShiftEnter,
26
26
  isTab,
27
- } from "../keys.js";
28
- import type { Component } from "../tui.js";
29
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
30
- import { SelectList, type SelectListTheme } from "./select-list.js";
27
+ } from "../keys";
28
+ import type { Component } from "../tui";
29
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
30
+ import { SelectList, type SelectListTheme } from "./select-list";
31
31
 
32
32
  const segmenter = getSegmenter();
33
33
 
@@ -4,8 +4,8 @@ import {
4
4
  type ImageDimensions,
5
5
  imageFallback,
6
6
  renderImage,
7
- } from "../terminal-image.js";
8
- import type { Component } from "../tui.js";
7
+ } from "../terminal-image";
8
+ import type { Component } from "../tui";
9
9
 
10
10
  export interface ImageTheme {
11
11
  fallbackColor: (str: string) => string;
@@ -14,9 +14,9 @@ import {
14
14
  isCtrlW,
15
15
  isDelete,
16
16
  isEnter,
17
- } from "../keys.js";
18
- import type { Component } from "../tui.js";
19
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
17
+ } from "../keys";
18
+ import type { Component } from "../tui";
19
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
20
20
 
21
21
  const segmenter = getSegmenter();
22
22
 
@@ -1,5 +1,5 @@
1
- import type { TUI } from "../tui.js";
2
- import { Text } from "./text.js";
1
+ import type { TUI } from "../tui";
2
+ import { Text } from "./text";
3
3
 
4
4
  /**
5
5
  * Loader component that updates every 80ms with spinning animation
@@ -1,6 +1,6 @@
1
1
  import { marked, type Token } from "marked";
2
- import type { Component } from "../tui.js";
3
- import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
2
+ import type { Component } from "../tui";
3
+ import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
4
4
 
5
5
  /**
6
6
  * Default text styling for markdown content.
@@ -1,6 +1,6 @@
1
- import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys.js";
2
- import type { Component } from "../tui.js";
3
- import { truncateToWidth } from "../utils.js";
1
+ import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
2
+ import type { Component } from "../tui";
3
+ import { truncateToWidth } from "../utils";
4
4
 
5
5
  export interface SelectItem {
6
6
  value: string;
@@ -1,6 +1,6 @@
1
- import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys.js";
2
- import type { Component } from "../tui.js";
3
- import { truncateToWidth, visibleWidth } from "../utils.js";
1
+ import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
2
+ import type { Component } from "../tui";
3
+ import { truncateToWidth, visibleWidth } from "../utils";
4
4
 
5
5
  export interface SettingItem {
6
6
  /** Unique identifier for this setting */
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui.js";
1
+ import type { Component } from "../tui";
2
2
 
3
3
  /**
4
4
  * Spacer component that renders empty lines
@@ -9,8 +9,8 @@
9
9
  * - Shift+Tab / Arrow Left: Previous tab (wraps around)
10
10
  */
11
11
 
12
- import { isArrowLeft, isArrowRight, isShiftTab, isTab } from "../keys.js";
13
- import type { Component } from "../tui.js";
12
+ import { isArrowLeft, isArrowRight, isShiftTab, isTab } from "../keys";
13
+ import type { Component } from "../tui";
14
14
 
15
15
  /** Tab definition */
16
16
  export interface Tab {
@@ -1,5 +1,5 @@
1
- import type { Component } from "../tui.js";
2
- import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
3
3
 
4
4
  /**
5
5
  * Text component - displays multi-line text with word wrapping
@@ -22,6 +22,10 @@ export class Text implements Component {
22
22
  this.customBgFn = customBgFn;
23
23
  }
24
24
 
25
+ getText(): string {
26
+ return this.text;
27
+ }
28
+
25
29
  setText(text: string): void {
26
30
  this.text = text;
27
31
  this.cachedText = undefined;
@@ -1,5 +1,5 @@
1
- import type { Component } from "../tui.js";
2
- import { truncateToWidth, visibleWidth } from "../utils.js";
1
+ import type { Component } from "../tui";
2
+ import { truncateToWidth, visibleWidth } from "../utils";
3
3
 
4
4
  /**
5
5
  * Text component that truncates to fit viewport width
package/src/index.ts CHANGED
@@ -6,21 +6,21 @@ export {
6
6
  type AutocompleteProvider,
7
7
  CombinedAutocompleteProvider,
8
8
  type SlashCommand,
9
- } from "./autocomplete.js";
9
+ } from "./autocomplete";
10
10
  // Components
11
- export { Box } from "./components/box.js";
12
- export { CancellableLoader } from "./components/cancellable-loader.js";
13
- export { Editor, type EditorTheme } from "./components/editor.js";
14
- export { Image, type ImageOptions, type ImageTheme } from "./components/image.js";
15
- export { Input } from "./components/input.js";
16
- export { Loader } from "./components/loader.js";
17
- export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.js";
18
- export { type SelectItem, SelectList, type SelectListTheme } from "./components/select-list.js";
19
- export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list.js";
20
- export { Spacer } from "./components/spacer.js";
21
- export { type Tab, TabBar, type TabBarTheme } from "./components/tab-bar.js";
22
- export { Text } from "./components/text.js";
23
- export { TruncatedText } from "./components/truncated-text.js";
11
+ export { Box } from "./components/box";
12
+ export { CancellableLoader } from "./components/cancellable-loader";
13
+ export { Editor, type EditorTheme } from "./components/editor";
14
+ export { Image, type ImageOptions, type ImageTheme } from "./components/image";
15
+ export { Input } from "./components/input";
16
+ export { Loader } from "./components/loader";
17
+ export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown";
18
+ export { type SelectItem, SelectList, type SelectListTheme } from "./components/select-list";
19
+ export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list";
20
+ export { Spacer } from "./components/spacer";
21
+ export { type Tab, TabBar, type TabBarTheme } from "./components/tab-bar";
22
+ export { Text } from "./components/text";
23
+ export { TruncatedText } from "./components/truncated-text";
24
24
  // Kitty keyboard protocol helpers
25
25
  export {
26
26
  isAltBackspace,
@@ -60,9 +60,9 @@ export {
60
60
  isShiftTab,
61
61
  isTab,
62
62
  Keys,
63
- } from "./keys.js";
63
+ } from "./keys";
64
64
  // Terminal interface and implementations
65
- export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal.js";
65
+ export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal";
66
66
  // Terminal image support
67
67
  export {
68
68
  type CellDimensions,
@@ -85,7 +85,7 @@ export {
85
85
  resetCapabilitiesCache,
86
86
  setCellDimensions,
87
87
  type TerminalCapabilities,
88
- } from "./terminal-image.js";
89
- export { type Component, Container, TUI } from "./tui.js";
88
+ } from "./terminal-image";
89
+ export { type Component, Container, TUI } from "./tui";
90
90
  // Utilities
91
- export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js";
91
+ export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils";
package/src/tui.ts CHANGED
@@ -5,10 +5,10 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
- import { isShiftCtrlD } from "./keys.js";
9
- import type { Terminal } from "./terminal.js";
10
- import { getCapabilities, setCellDimensions } from "./terminal-image.js";
11
- import { visibleWidth } from "./utils.js";
8
+ import { isShiftCtrlD } from "./keys";
9
+ import type { Terminal } from "./terminal";
10
+ import { getCapabilities, setCellDimensions } from "./terminal-image";
11
+ import { visibleWidth } from "./utils";
12
12
 
13
13
  /**
14
14
  * Component interface - all components must implement this
@@ -86,6 +86,7 @@ export class TUI extends Container {
86
86
  private cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)
87
87
  private inputBuffer = ""; // Buffer for parsing terminal responses
88
88
  private cellSizeQueryPending = false;
89
+ private inputQueue: string[] = []; // Queue input during cell size query to avoid interleaving
89
90
 
90
91
  constructor(terminal: Terminal) {
91
92
  super();
@@ -142,9 +143,24 @@ export class TUI extends Container {
142
143
  this.inputBuffer += data;
143
144
  const filtered = this.parseCellSizeResponse();
144
145
  if (filtered.length === 0) return;
145
- data = filtered;
146
+ if (filtered.length > 0) {
147
+ this.inputQueue.push(filtered);
148
+ }
149
+ // Process queued input after cell size response completes
150
+ if (!this.cellSizeQueryPending && this.inputQueue.length > 0) {
151
+ const queued = this.inputQueue;
152
+ this.inputQueue = [];
153
+ for (const item of queued) {
154
+ this.processInput(item);
155
+ }
156
+ }
157
+ return;
146
158
  }
147
159
 
160
+ this.processInput(data);
161
+ }
162
+
163
+ private processInput(data: string): void {
148
164
  // Global debug key handler (Shift+Ctrl+D)
149
165
  if (isShiftCtrlD(data) && this.onDebug) {
150
166
  this.onDebug();
@@ -169,16 +185,17 @@ export class TUI extends Container {
169
185
  const heightPx = parseInt(match[1], 10);
170
186
  const widthPx = parseInt(match[2], 10);
171
187
 
188
+ // Remove the response from buffer first
189
+ this.inputBuffer = this.inputBuffer.replace(responsePattern, "");
190
+ this.cellSizeQueryPending = false;
191
+
172
192
  if (heightPx > 0 && widthPx > 0) {
173
193
  setCellDimensions({ widthPx, heightPx });
174
194
  // Invalidate all components so images re-render with correct dimensions
195
+ // This is safe now because cellSizeQueryPending=false prevents race with render
175
196
  this.invalidate();
176
197
  this.requestRender();
177
198
  }
178
-
179
- // Remove the response from buffer
180
- this.inputBuffer = this.inputBuffer.replace(responsePattern, "");
181
- this.cellSizeQueryPending = false;
182
199
  }
183
200
 
184
201
  // Check if we have a partial cell size response starting (wait for more data)
@@ -206,8 +223,11 @@ export class TUI extends Container {
206
223
  }
207
224
 
208
225
  private doRender(): void {
226
+ // Capture terminal dimensions at start to ensure consistency throughout render
209
227
  const width = this.terminal.columns;
210
228
  const height = this.terminal.rows;
229
+ // Snapshot cursor position at start of render for consistent viewport calculations
230
+ const currentCursorRow = this.cursorRow;
211
231
 
212
232
  // Render all components to get new lines
213
233
  const newLines = this.render(width);
@@ -267,10 +287,10 @@ export class TUI extends Container {
267
287
  }
268
288
 
269
289
  // Check if firstChanged is outside the viewport
270
- // cursorRow is the line where cursor is (0-indexed)
271
- // Viewport shows lines from (cursorRow - height + 1) to cursorRow
290
+ // Use snapshotted cursor position for consistent viewport calculation
291
+ // Viewport shows lines from (currentCursorRow - height + 1) to currentCursorRow
272
292
  // If firstChanged < viewportTop, we need full re-render
273
- const viewportTop = this.cursorRow - height + 1;
293
+ const viewportTop = currentCursorRow - height + 1;
274
294
  if (firstChanged < viewportTop) {
275
295
  // First change is above viewport - need full re-render
276
296
  let buffer = "\x1b[?2026h"; // Begin synchronized output
@@ -291,8 +311,8 @@ export class TUI extends Container {
291
311
  // Build buffer with all updates wrapped in synchronized output
292
312
  let buffer = "\x1b[?2026h"; // Begin synchronized output
293
313
 
294
- // Move cursor to first changed line
295
- const lineDiff = firstChanged - this.cursorRow;
314
+ // Move cursor to first changed line using snapshotted position
315
+ const lineDiff = firstChanged - currentCursorRow;
296
316
  if (lineDiff > 0) {
297
317
  buffer += `\x1b[${lineDiff}B`; // Move down
298
318
  } else if (lineDiff < 0) {
@@ -323,7 +343,9 @@ export class TUI extends Container {
323
343
  try {
324
344
  fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
325
345
  fs.writeFileSync(crashLogPath, crashData);
326
- } catch {}
346
+ } catch {
347
+ // Ignore - crash log is best-effort
348
+ }
327
349
  throw new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);
328
350
  }
329
351
  buffer += line;