@gajae-code/tui 0.8.2 → 0.9.0

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/CHANGELOG.md CHANGED
@@ -23,6 +23,9 @@
23
23
  ### Fixed
24
24
 
25
25
  - Kept the terminal stdout error handler armed briefly after TUI shutdown so late `EIO`/closed-PTY errors from SSH or Windows Terminal detach do not crash tmux-backed GJC panes.
26
+ ### Fixed
27
+
28
+ - Added an editor right-gutter render option so bordered input chrome can stay one cell inside the terminal edge without changing component width.
26
29
 
27
30
  ## [0.7.9] - 2026-07-01
28
31
 
@@ -0,0 +1,13 @@
1
+ export type AnimationCadence = 16 | 80;
2
+ type AnimationCallback = (now: number) => void;
3
+ export interface AnimationRegistration {
4
+ unregister(): void;
5
+ }
6
+ export declare function registerAnimationCallback(callback: AnimationCallback, cadence?: AnimationCadence): AnimationRegistration;
7
+ export declare const __animationSchedulerTestHooks: {
8
+ getActiveTimerCount(cadence?: AnimationCadence): number;
9
+ getRegistrantCount(cadence?: AnimationCadence): number;
10
+ getStartedTimerCount(cadence?: AnimationCadence): number;
11
+ reset(): void;
12
+ };
13
+ export {};
@@ -24,6 +24,13 @@ interface HistoryStorage {
24
24
  add(prompt: string, cwd?: string): Promise<void>;
25
25
  getRecent(limit: number, cwd?: string): HistoryEntry[];
26
26
  }
27
+ /** Test-only performance counters for advisory baseline tests. */
28
+ export declare const __editorPerfCounters: {
29
+ layoutTextInvocations: number;
30
+ layoutLogicalLinesProcessed: number;
31
+ visibleWidthMeasurements: number;
32
+ reset(): void;
33
+ };
27
34
  export declare class Editor implements Component, Focusable {
28
35
  #private;
29
36
  /** Focusable interface - set by TUI when focus changes */
@@ -46,6 +53,7 @@ export declare class Editor implements Component, Focusable {
46
53
  onTab?: (text: string) => boolean | undefined;
47
54
  disableSubmit: boolean;
48
55
  constructor(theme: EditorTheme);
56
+ dispose(): void;
49
57
  setAutocompleteProvider(provider: AutocompleteProvider): void;
50
58
  getAutocompleteProvider(): AutocompleteProvider | undefined;
51
59
  /** Whether the autocomplete dropdown is currently open. */
@@ -66,7 +74,7 @@ export declare class Editor implements Component, Focusable {
66
74
  setPlaceholder(placeholder: string | undefined): void;
67
75
  /**
68
76
  * Get the available width for top border content given a total terminal width.
69
- * Accounts for the border characters and horizontal padding when visible.
77
+ * Accounts for right gutter, border characters, and horizontal padding when visible.
70
78
  */
71
79
  getTopBorderAvailableWidth(terminalWidth: number): number;
72
80
  /**
@@ -76,6 +84,7 @@ export declare class Editor implements Component, Focusable {
76
84
  getUseTerminalCursor(): boolean;
77
85
  setMaxHeight(maxHeight: number | undefined): void;
78
86
  setPaddingX(paddingX: number): void;
87
+ setRightGutterWidth(width: number): void;
79
88
  getAutocompleteMaxVisible(): number;
80
89
  setAutocompleteMaxVisible(maxVisible: number): void;
81
90
  setHistoryStorage(storage: HistoryStorage): void;
@@ -1,11 +1,20 @@
1
1
  import type { TUI } from "../tui";
2
2
  import { Text } from "./text";
3
+ export interface LoaderOptions {
4
+ timeDependentColor?: boolean;
5
+ }
6
+ /** Test-only performance counters for advisory baseline tests. */
7
+ export declare const __loaderPerfCounters: {
8
+ liveIntervals: number;
9
+ startedIntervals: number;
10
+ reset(): void;
11
+ };
3
12
  export declare class Loader extends Text {
4
13
  #private;
5
14
  private spinnerColorFn;
6
15
  private messageColorFn;
7
16
  private message;
8
- constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[]);
17
+ constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[], options?: LoaderOptions);
9
18
  render(width: number): string[];
10
19
  start(): void;
11
20
  stop(): void;
@@ -1,8 +1,16 @@
1
1
  import type { SymbolTheme } from "../symbols";
2
2
  import type { Component } from "../tui";
3
+ /** Test-only clock seam for streaming throttle tests. */
4
+ export declare function __setMarkdownNowForTest(now: (() => number) | undefined): void;
3
5
  /** Test/diagnostic seam: number of synchronous highlight invocations since the last reset. */
4
6
  export declare function getMarkdownHighlightCallCount(): number;
5
7
  export declare function resetMarkdownHighlightCallCount(): void;
8
+ /** Test-only performance counters for advisory baseline tests. */
9
+ export declare const __markdownPerfCounters: {
10
+ lexerInvocations: number;
11
+ lexedBytes: number;
12
+ reset(): void;
13
+ };
6
14
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
7
15
  export declare function clearRenderCache(): void;
8
16
  /**
@@ -53,7 +61,12 @@ export interface MarkdownTheme {
53
61
  export declare class Markdown implements Component {
54
62
  #private;
55
63
  constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
56
- setText(text: string): void;
64
+ setOnStaleThrottle(callback: (() => void) | undefined): void;
65
+ setText(text: string, options?: {
66
+ streaming?: boolean;
67
+ }): void;
68
+ setStreaming(streaming: boolean): void;
69
+ dispose(): void;
57
70
  invalidate(): void;
58
71
  render(width: number): string[];
59
72
  }
@@ -1,3 +1,4 @@
1
+ export * from "./animation-scheduler";
1
2
  export * from "./autocomplete";
2
3
  export * from "./components/box";
3
4
  export * from "./components/cancellable-loader";
@@ -130,6 +130,11 @@ export declare class Container implements Component {
130
130
  invalidate(): void;
131
131
  render(width: number): string[];
132
132
  }
133
+ type TuiRenderCounterSnapshot = {
134
+ debugRedrawEnvReads: number;
135
+ debugRedrawAppendWrites: number;
136
+ differentialGuardVisibleWidthCalls: number;
137
+ };
133
138
  /**
134
139
  * TUI - Main class for managing terminal UI with differential rendering
135
140
  */
@@ -138,6 +143,8 @@ export declare class TUI extends Container {
138
143
  terminal: Terminal;
139
144
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
140
145
  onDebug?: () => void;
146
+ static resetRenderCountersForTest(): void;
147
+ static getRenderCountersForTest(): TuiRenderCounterSnapshot;
141
148
  overlayStack: {
142
149
  component: Component;
143
150
  options?: OverlayOptions;
@@ -145,6 +152,7 @@ export declare class TUI extends Container {
145
152
  hidden: boolean;
146
153
  }[];
147
154
  constructor(terminal: Terminal, showHardwareCursor?: boolean);
155
+ dispose(): void;
148
156
  get fullRedraws(): number;
149
157
  getShowHardwareCursor(): boolean;
150
158
  setShowHardwareCursor(enabled: boolean): void;
@@ -175,16 +183,17 @@ export declare class TUI extends Container {
175
183
  removeInputListener(listener: InputListener): void;
176
184
  stop(): void;
177
185
  /**
178
- * Multiplexer-aware resize render request.
186
+ * Viewport-repaint-aware resize render request.
179
187
  *
180
188
  * A forced full redraw (`requestRender(true)`) resets `#previousWidth`/`#previousHeight`
181
189
  * to -1, which makes `#doRender` treat the frame as a width change and fall into the
182
190
  * `fullRender` path. In terminal multiplexers that path skips the scrollback-clearing
183
191
  * `3J` escape (users navigate scrollback history), so replaying every transcript line
184
192
  * piles it back on top of scrollback — the "top of screen scrolls down to the prompt at
185
- * high speed" resize storm. Here we keep force off in multiplexers so `#doRender`'s
186
- * height-change branch takes the viewport-only `multiplexerViewportRepaint` path instead.
187
- * Set `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy forced redraw.
193
+ * high speed" resize storm. Windows Terminal can also visibly jump to the
194
+ * transcript top during streaming redraws, so viewport-repaint sessions keep
195
+ * force off and let `#doRender` repaint only the live viewport. Set
196
+ * `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy tmux redraw.
188
197
  */
189
198
  requestResizeRender(): void;
190
199
  requestRender(force?: boolean, source?: string): void;
@@ -1,9 +1,19 @@
1
1
  import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@gajae-code/natives";
2
2
  export { Ellipsis } from "@gajae-code/natives";
3
3
  export { getDefaultTabWidth, getIndentation } from "@gajae-code/utils";
4
+ /** Test-only performance counters for advisory baseline tests. */
5
+ export declare const __textHelperPerfCounters: {
6
+ truncateToWidthCalls: number;
7
+ wrapTextWithAnsiCalls: number;
8
+ truncateLinesToWidthCalls: number;
9
+ visibleWidthsCalls: number;
10
+ reset(): void;
11
+ };
12
+ export declare function invalidateTabWidthCache(): void;
4
13
  export declare function isPrintableAscii(text: string): boolean;
5
14
  export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
6
15
  export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string;
16
+ export declare function truncateLinesToWidth(lines: readonly string[], maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string[];
7
17
  export declare function wrapTextWithAnsi(text: string, width: number): string[];
8
18
  export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
9
19
  /**
@@ -24,6 +34,8 @@ export declare function visibleWidthRaw(str: string): number;
24
34
  * Calculate the visible width of a string in terminal columns.
25
35
  */
26
36
  export declare function visibleWidth(str: string): number;
37
+ export declare function visibleWidthsNative(lines: readonly string[]): number[];
38
+ export declare function visibleWidths(lines: readonly string[]): number[];
27
39
  /**
28
40
  * Normalize text for terminal output without changing logical editor content.
29
41
  * Some terminals render canonically decomposed Hangul jamo or precomposed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@gajae-code/natives": "0.8.2",
42
- "@gajae-code/utils": "0.8.2",
41
+ "@gajae-code/natives": "0.9.0",
42
+ "@gajae-code/utils": "0.9.0",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -0,0 +1,99 @@
1
+ export type AnimationCadence = 16 | 80;
2
+
3
+ type TimerHandle = ReturnType<typeof setInterval>;
4
+ type AnimationCallback = (now: number) => void;
5
+
6
+ interface CadenceBucket {
7
+ callbacks: Set<AnimationCallback>;
8
+ timer?: TimerHandle;
9
+ startedTimers: number;
10
+ }
11
+
12
+ const buckets = new Map<AnimationCadence, CadenceBucket>();
13
+
14
+ function getBucket(cadence: AnimationCadence): CadenceBucket {
15
+ let bucket = buckets.get(cadence);
16
+ if (!bucket) {
17
+ bucket = { callbacks: new Set(), startedTimers: 0 };
18
+ buckets.set(cadence, bucket);
19
+ }
20
+ return bucket;
21
+ }
22
+
23
+ function startBucket(cadence: AnimationCadence, bucket: CadenceBucket): void {
24
+ if (bucket.timer) return;
25
+ bucket.timer = setInterval(() => {
26
+ const now = performance.now();
27
+ // Snapshot so re-entrant register/unregister during a tick is safe, and
28
+ // isolate each callback so one throwing registrant cannot starve siblings
29
+ // or surface as an uncaught exception that kills the shared timer.
30
+ for (const callback of [...bucket.callbacks]) {
31
+ try {
32
+ callback(now);
33
+ } catch (err) {
34
+ console.error("[animation-scheduler] callback threw:", err);
35
+ }
36
+ }
37
+ }, cadence);
38
+ bucket.startedTimers += 1;
39
+ bucket.timer?.unref?.();
40
+ }
41
+
42
+ function stopBucket(bucket: CadenceBucket): void {
43
+ if (!bucket.timer) return;
44
+ clearInterval(bucket.timer);
45
+ bucket.timer = undefined;
46
+ }
47
+
48
+ export interface AnimationRegistration {
49
+ unregister(): void;
50
+ }
51
+
52
+ export function registerAnimationCallback(
53
+ callback: AnimationCallback,
54
+ cadence: AnimationCadence = 80,
55
+ ): AnimationRegistration {
56
+ const bucket = getBucket(cadence);
57
+ bucket.callbacks.add(callback);
58
+ startBucket(cadence, bucket);
59
+ let registered = true;
60
+
61
+ return {
62
+ unregister(): void {
63
+ if (!registered) return;
64
+ registered = false;
65
+ bucket.callbacks.delete(callback);
66
+ if (bucket.callbacks.size === 0) stopBucket(bucket);
67
+ },
68
+ };
69
+ }
70
+
71
+ export const __animationSchedulerTestHooks = {
72
+ getActiveTimerCount(cadence?: AnimationCadence): number {
73
+ if (cadence !== undefined) return getBucket(cadence).timer ? 1 : 0;
74
+ let count = 0;
75
+ for (const bucket of buckets.values()) {
76
+ if (bucket.timer) count += 1;
77
+ }
78
+ return count;
79
+ },
80
+ getRegistrantCount(cadence?: AnimationCadence): number {
81
+ if (cadence !== undefined) return getBucket(cadence).callbacks.size;
82
+ let count = 0;
83
+ for (const bucket of buckets.values()) count += bucket.callbacks.size;
84
+ return count;
85
+ },
86
+ getStartedTimerCount(cadence?: AnimationCadence): number {
87
+ if (cadence !== undefined) return getBucket(cadence).startedTimers;
88
+ let count = 0;
89
+ for (const bucket of buckets.values()) count += bucket.startedTimers;
90
+ return count;
91
+ },
92
+ reset(): void {
93
+ for (const bucket of buckets.values()) {
94
+ stopBucket(bucket);
95
+ bucket.callbacks.clear();
96
+ bucket.startedTimers = 0;
97
+ }
98
+ },
99
+ };