@oh-my-pi/pi-tui 15.10.0 → 15.10.1
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 +44 -0
- package/dist/types/components/editor.d.ts +2 -0
- package/dist/types/components/loader.d.ts +8 -1
- package/dist/types/components/scroll-view.d.ts +22 -0
- package/dist/types/kitty-graphics.d.ts +16 -31
- package/dist/types/terminal-capabilities.d.ts +47 -1
- package/dist/types/tui.d.ts +38 -11
- package/dist/types/utils.d.ts +5 -2
- package/package.json +3 -3
- package/src/components/editor.ts +5 -0
- package/src/components/loader.ts +27 -17
- package/src/components/markdown.ts +14 -12
- package/src/components/scroll-view.ts +62 -1
- package/src/components/text.ts +3 -0
- package/src/index.ts +1 -1
- package/src/keybindings.ts +69 -11
- package/src/kitty-graphics.ts +2 -101
- package/src/terminal-capabilities.ts +136 -65
- package/src/terminal.ts +3 -61
- package/src/tui.ts +392 -123
- package/src/utils.ts +92 -60
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.1] - 2026-06-07
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Removed Kitty temp-file image transmission, its startup support probe, the `PI_KITTY_IMAGE_TRANSMISSION` override, and the temp-file helper exports. Kitty/Ghostty image payloads now stay on in-band base64 before placeholder/direct placement, avoiding blank first renders from temp-file load races.
|
|
9
|
+
- Renamed `RenderRequestOptions.allowUnknownViewportMutation` → `allowUnknownViewportTransientRepaint`. The option only permits a transient live-viewport repaint (autocomplete/IME/focused-editor chrome) on hosts that cannot report viewport position; it never authorizes a settled transcript commit. The old name implied any offscreen mutation was safe to push into native scrollback, which led callers to emit duplicate transcript copies.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `TUI.addStartListener()` so feature hooks can re-enable terminal modes after temporary stop/start cycles such as external-editor handoffs.
|
|
14
|
+
- Added `Editor.pasteText()` to apply terminal-style paste handling for text inserted from non-bracketed paste transports
|
|
15
|
+
- Added an optional `dispose()` lifecycle method to `Component` so components can release timers and subscriptions during permanent teardown
|
|
16
|
+
- Added `Container.dispose()` to propagate teardown to child components when a component tree is permanently discarded
|
|
17
|
+
- Added `Loader.dispose()` to stop the loader animation timer when the component is disposed
|
|
18
|
+
- Added a `ScrollView` `ellipsis` option (defaults to `Ellipsis.Unicode`) so callers that pre-wrap content to width can pass `Ellipsis.Omit` and suppress the stray per-line `…` that lands on trailing padding.
|
|
19
|
+
- Added `ScrollView.handleScrollKey()` plus a `fastScrollLines` option so every scroll view gets shared navigation keys, including Shift+Arrow to scroll faster.
|
|
20
|
+
- Added `OverlayOptions.fullscreen`: while the topmost visible overlay sets it, the engine borrows the terminal's alternate screen buffer for the overlay's lifetime and paints only the modal there — no ED3, no transcript re-commit — so the transcript stays untouched on the normal screen and is not scrollable behind the modal. Mouse tracking (`?1000h`/`?1006h`) is enabled for the modal's lifetime and disabled on exit, so the rest of the app keeps the terminal's native text selection.
|
|
21
|
+
- Added the `submitPinsViewportToTail` terminal capability and `detectSubmitPinsViewportToTail()`: genuine local terminals where a submit keystroke scrolls the host to its tail reconcile deferred native scrollback at the prompt-submit checkpoint even when the viewport position is unprobeable (Ghostty/kitty/iTerm/WezTerm/Alacritty). Restores the pre-regression submit reconciliation without re-enabling it for Windows Terminal/ConPTY, SSH, or multiplexers, where a submit is not proof the host is at the tail.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Changed static `Loader` messages to repaint only at the spinner's 80 ms cadence; time-dependent message colorizers can opt into 16 ms redraws with `animated: true`.
|
|
26
|
+
- Changed keybinding matching to precompute canonical key sets so each input sequence is parsed once per binding check instead of once per candidate key.
|
|
27
|
+
- Made `Component.invalidate()` optional so leaf components without render caches no longer need no-op invalidation hooks.
|
|
28
|
+
- `TERMINAL` is now a `RuntimeTerminal` whose post-construction capabilities (image protocol and the probe-driven flags) are writable, replacing the `as unknown as MutableTerminalInfo` cast pattern and the positional `withTerminalOverrides` rebuild with a prototype-preserving `clone()`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed `Loader` text updates to skip identical messages and preserve the rendered `Text` cache instead of invalidating it every timer tick.
|
|
33
|
+
|
|
34
|
+
- Fixed fullscreen overlay alt-frame rendering to reuse the current line-preparation path instead of calling removed fitting helpers.
|
|
35
|
+
- Reduced TUI render-path line fitting by deferring overlay base-frame fitting until an overlay rebuild and by reusing already-fitted lines in emitters.
|
|
36
|
+
- Reduced live-region pinned repaint output by diffing unchanged viewport rows when no sealed rows are being committed to native scrollback.
|
|
37
|
+
- Fixed no-append live-region pinned repaints to re-anchor the hardware cursor when the logical viewport shifts.
|
|
38
|
+
- Fixed keybinding matching so printable uppercase input preserves `Shift` for bindings such as `shift+a`.
|
|
39
|
+
- Optimized terminal image-line detection and Thai/Lao AM normalization checks to avoid hot-path regex scans and substring allocations.
|
|
40
|
+
- Fixed `Markdown.render()` cache hits returning the cache's mutable backing array, which let callers that append extra rows corrupt cached Markdown and duplicate those rows on every redraw.
|
|
41
|
+
- Fixed first-paint full replays for callers that intentionally replace terminal history by allowing `TUI.start({ clearScrollback: true })`, so they do not briefly append an entire initial frame before the first clean replay.
|
|
42
|
+
- Fixed ED3-risk streaming cap accounting to preserve the native scrollback high-water mark for rows that were already physically committed before transient frames were viewport-capped.
|
|
43
|
+
- Fixed terminal stop and restore cleanup to disable enhanced paste mode so it does not remain enabled after shutdown
|
|
44
|
+
- Removed the per-frame line-fit `Map` cache from the render timer path to avoid forcing JSC rope-string hashing during scheduled viewport repaints.
|
|
45
|
+
- Fixed `visibleWidth()` so terminal column measurements for ANSI and OSC text now match the native truncation/wrapping helpers, including OSC 66 text-sizing spans being counted at their scaled payload width
|
|
46
|
+
- Fixed cursor, padding, and line-fit behavior when strings contain tabs or OSC escapes by aligning `visibleWidth()` with the native text-width model
|
|
47
|
+
- Fixed the transcript — or a re-appearing prior view such as the welcome screen — duplicating itself on terminals without a scroll-position oracle (Ghostty/kitty/iTerm/WezTerm) when a foreground tool completes by rewriting a partly-committed block, or when the transcript is reset. A non-destructive viewport repaint no longer re-paints rows that are byte-identical to what is already committed to native scrollback into the active grid; the repaint anchor is clamped to the committed-and-unchanged prefix (`min(firstChanged, scrollbackHighWater)`).
|
|
48
|
+
|
|
5
49
|
## [15.10.0] - 2026-06-06
|
|
6
50
|
|
|
7
51
|
### Changed
|
|
@@ -100,6 +100,8 @@ export declare class Editor implements Component, Focusable {
|
|
|
100
100
|
setText(text: string): void;
|
|
101
101
|
/** Insert text at the current cursor position */
|
|
102
102
|
insertText(text: string): void;
|
|
103
|
+
/** Apply terminal paste semantics to text from non-bracketed paste transports. */
|
|
104
|
+
pasteText(text: string): void;
|
|
103
105
|
isShowingAutocomplete(): boolean;
|
|
104
106
|
}
|
|
105
107
|
export {};
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { TUI } from "../tui";
|
|
2
2
|
import { Text } from "./text";
|
|
3
|
+
type ColorFn = (str: string) => string;
|
|
4
|
+
export type LoaderMessageColorFn = ColorFn & {
|
|
5
|
+
readonly animated?: true;
|
|
6
|
+
};
|
|
3
7
|
export declare class Loader extends Text {
|
|
4
8
|
#private;
|
|
5
9
|
private spinnerColorFn;
|
|
6
10
|
private messageColorFn;
|
|
7
11
|
private message;
|
|
8
|
-
constructor(ui: TUI, spinnerColorFn:
|
|
12
|
+
constructor(ui: TUI, spinnerColorFn: ColorFn, messageColorFn: LoaderMessageColorFn, message?: string, spinnerFrames?: string[]);
|
|
9
13
|
render(width: number): string[];
|
|
10
14
|
start(): void;
|
|
11
15
|
stop(): void;
|
|
16
|
+
/** Lifecycle teardown: stop the animation timer. Idempotent. */
|
|
17
|
+
dispose(): void;
|
|
12
18
|
setMessage(message: string): void;
|
|
13
19
|
}
|
|
20
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component } from "../tui";
|
|
2
|
+
import { Ellipsis } from "../utils";
|
|
2
3
|
type ScrollbarMode = "auto" | "always" | "never";
|
|
3
4
|
export interface ScrollViewTheme {
|
|
4
5
|
track?: (text: string) => string;
|
|
@@ -13,6 +14,18 @@ export interface ScrollViewOptions {
|
|
|
13
14
|
theme?: ScrollViewTheme;
|
|
14
15
|
trackChar?: string;
|
|
15
16
|
thumbChar?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Indicator appended when a row overflows `contentWidth`. Defaults to
|
|
19
|
+
* {@link Ellipsis.Unicode}. Pass {@link Ellipsis.Omit} when callers wrap
|
|
20
|
+
* lines to width themselves and only trailing padding can overflow (e.g.
|
|
21
|
+
* the plan-review overlay), so no stray `…` lands on every padded row.
|
|
22
|
+
*/
|
|
23
|
+
ellipsis?: Ellipsis;
|
|
24
|
+
/**
|
|
25
|
+
* Rows moved per keystroke when {@link ScrollView.handleScrollKey} sees a
|
|
26
|
+
* Shift+Arrow (the "scroll faster" affordance). Defaults to 5.
|
|
27
|
+
*/
|
|
28
|
+
fastScrollLines?: number;
|
|
16
29
|
}
|
|
17
30
|
/**
|
|
18
31
|
* Fixed-height viewport over pre-rendered lines, with optional right-edge scrollbar.
|
|
@@ -34,6 +47,15 @@ export declare class ScrollView implements Component {
|
|
|
34
47
|
page(delta: number): void;
|
|
35
48
|
scrollToTop(): void;
|
|
36
49
|
scrollToBottom(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Apply a standard navigation key to the viewport. Shift+Arrow scrolls by
|
|
52
|
+
* {@link ScrollViewOptions.fastScrollLines} (the "scroll faster" affordance);
|
|
53
|
+
* plain Arrow by one line; PageUp/PageDown by a page; Home/End to the ends.
|
|
54
|
+
* Returns true when the key was consumed, so callers can fall through to
|
|
55
|
+
* their own (e.g. vim-style) bindings. Generic on purpose: every ScrollView
|
|
56
|
+
* consumer gets the same scroll keys, including Shift-to-go-faster.
|
|
57
|
+
*/
|
|
58
|
+
handleScrollKey(data: string): boolean;
|
|
37
59
|
invalidate(): void;
|
|
38
60
|
render(width: number): string[];
|
|
39
61
|
}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kitty graphics: Unicode placeholder placement (`U=1` + U+10EEEE), with
|
|
3
|
+
* runtime feature state and env overrides.
|
|
4
|
+
*
|
|
5
|
+
* Unicode placeholders let a transmitted image be displayed by writing ordinary
|
|
6
|
+
* text cells — the placeholder char U+10EEEE plus row/column combining
|
|
7
|
+
* diacritics — instead of a cursor-positioned `a=p` direct placement. The image
|
|
8
|
+
* then participates in the normal text grid, so it survives horizontal slicing,
|
|
9
|
+
* reflow and overlapping draws (each cell names its own row+column, so a sliced
|
|
10
|
+
* row still maps to the correct sub-region). See kitty
|
|
11
|
+
* `docs/graphics-protocol.rst` "Unicode placeholders for relative placements".
|
|
12
|
+
*
|
|
13
|
+
* This module is intentionally free of `./terminal-capabilities` imports so the
|
|
14
|
+
* dependency stays one-way (capabilities → kitty-graphics) and no import cycle
|
|
15
|
+
* forms. Protocol gating (`imageProtocol === Kitty`) lives in the caller.
|
|
16
|
+
*/
|
|
1
17
|
/** Kitty Unicode placeholder base character (U+10EEEE, Plane 16 PUA). */
|
|
2
18
|
export declare const KITTY_PLACEHOLDER = "\uDBFB\uDEEE";
|
|
3
19
|
/** Largest row/column index expressible with the diacritic table (one cell each). */
|
|
4
20
|
export declare const KITTY_PLACEHOLDER_MAX_CELLS: number;
|
|
5
|
-
export type KittyTransmissionMedium = "direct" | "temp-file";
|
|
6
21
|
export interface KittyGraphicsFeatures {
|
|
7
22
|
/** Display images via Unicode placeholders instead of direct `a=p` placement. */
|
|
8
23
|
unicodePlaceholders: boolean;
|
|
9
|
-
/** How image data reaches the terminal: in-band base64 or a temp file. */
|
|
10
|
-
transmissionMedium: KittyTransmissionMedium;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
26
|
* Whether the detected terminal renders Kitty Unicode placeholders (`U=1` +
|
|
@@ -29,16 +42,8 @@ export interface KittyGraphicsFeatures {
|
|
|
29
42
|
export declare function detectKittyUnicodePlaceholdersSupport(terminalId: string, env?: NodeJS.ProcessEnv): boolean;
|
|
30
43
|
export declare function getKittyGraphics(): Readonly<KittyGraphicsFeatures>;
|
|
31
44
|
export declare function setKittyGraphics(partial: Partial<KittyGraphicsFeatures>): void;
|
|
32
|
-
/**
|
|
33
|
-
* Whether temp-file transmission may be promoted at runtime: forced via env,
|
|
34
|
-
* disabled via env, otherwise auto (local sessions only — a temp file written
|
|
35
|
-
* locally is not readable by a terminal on the far side of an SSH link).
|
|
36
|
-
*/
|
|
37
|
-
export declare function kittyTempFileAllowed(): boolean;
|
|
38
45
|
/** Whether a `columns`×`rows` placeholder grid fits within the diacritic table. */
|
|
39
46
|
export declare function kittyPlaceholdersFit(columns: number, rows: number): boolean;
|
|
40
|
-
/** True when the base64 payload is a PNG (kitty `f=100` / temp-file path only). */
|
|
41
|
-
export declare function isPngBase64(base64Data: string): boolean;
|
|
42
47
|
/**
|
|
43
48
|
* Virtual placement APC (`a=p,U=1`): tells the terminal that placeholder cells
|
|
44
49
|
* carrying image id `i` should display the transmitted image, scaled to fit the
|
|
@@ -72,23 +77,3 @@ export declare function renderKittyPlaceholderLines(opts: {
|
|
|
72
77
|
columns: number;
|
|
73
78
|
rows: number;
|
|
74
79
|
}): string[];
|
|
75
|
-
/**
|
|
76
|
-
* Transmit a PNG via a temp file (`t=t`): decode the base64 to bytes once, write
|
|
77
|
-
* them to a temp file, and send the base64-encoded file path as payload. Returns
|
|
78
|
-
* the APC string, or `null` on any failure (caller falls back to direct base64).
|
|
79
|
-
*
|
|
80
|
-
* Synchronous filesystem writes are mandated by the synchronous render pipeline
|
|
81
|
-
* (`Image.render` → `renderImage` are sync); there is no async seam here.
|
|
82
|
-
*/
|
|
83
|
-
export declare function encodeKittyTempFileTransmit(base64Png: string, imageId: number): string | null;
|
|
84
|
-
/**
|
|
85
|
-
* Encode a temp-file support probe: write a tiny PNG to a temp file and ask the
|
|
86
|
-
* terminal to query it (`a=q,t=t`). A conforming terminal replies
|
|
87
|
-
* `ESC _ G i=<probeId>;OK ESC \`. Returns the query APC plus a `cleanup` that
|
|
88
|
-
* removes the probe file (best-effort; kitty self-deletes the magic-named file).
|
|
89
|
-
* Returns `null` if the temp file cannot be written.
|
|
90
|
-
*/
|
|
91
|
-
export declare function encodeKittyTempFileProbe(probeId: number): {
|
|
92
|
-
sequence: string;
|
|
93
|
-
cleanup: () => void;
|
|
94
|
-
} | null;
|
|
@@ -24,6 +24,21 @@ export declare class TerminalInfo {
|
|
|
24
24
|
constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, eagerEraseScrollbackRisk?: boolean, deccara?: boolean, supportsScreenToScrollback?: boolean,
|
|
25
25
|
/** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
|
|
26
26
|
textSizing?: boolean);
|
|
27
|
+
/**
|
|
28
|
+
* Whether a prompt-submit keystroke scrolls this host to its tail, so the
|
|
29
|
+
* native-scrollback reconciliation checkpoint may ED3-rebuild even when the
|
|
30
|
+
* viewport position is unprobeable. Assigned by the TERMINAL builder from
|
|
31
|
+
* {@link detectSubmitPinsViewportToTail}; readonly but tests opt in via the
|
|
32
|
+
* {@link setTerminalSubmitPinsViewportToTail} mutable-cast setter.
|
|
33
|
+
*/
|
|
34
|
+
readonly submitPinsViewportToTail: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Mutable clone for the {@link TERMINAL} singleton: copies every field and
|
|
37
|
+
* keeps the prototype methods, so the builder and runtime setters flip
|
|
38
|
+
* runtime-resolved {@link RuntimeTerminal} capabilities in place instead of
|
|
39
|
+
* reconstructing positional constructor args.
|
|
40
|
+
*/
|
|
41
|
+
clone(): RuntimeTerminal;
|
|
27
42
|
isImageLine(line: string): boolean;
|
|
28
43
|
formatNotification(message: string | TerminalNotification): string;
|
|
29
44
|
sendNotification(message: string | TerminalNotification): void;
|
|
@@ -51,6 +66,20 @@ export declare function isWindowsTerminalPreviewSixelSupported(env?: NodeJS.Proc
|
|
|
51
66
|
* outer host fronting WSL, where the same ED3 yank applies. See #1610/#1682/#1799.
|
|
52
67
|
*/
|
|
53
68
|
export declare function detectTerminalEagerEraseScrollbackRisk(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a prompt-submit keystroke scrolls this terminal to its tail, making the
|
|
71
|
+
* native-scrollback reconciliation checkpoint (`refreshNativeScrollbackIfDirty`)
|
|
72
|
+
* safe to ED3-rebuild even when the viewport position cannot be probed.
|
|
73
|
+
*
|
|
74
|
+
* True only for recognized genuine *local* terminals where typing into the prompt
|
|
75
|
+
* brings the host viewport to the bottom. False — the checkpoint keeps deferring
|
|
76
|
+
* until a positive at-tail probe — for hosts whose scrollback a keystroke does not
|
|
77
|
+
* move: Windows consoles/ConPTY, Windows Terminal (incl. WSL), SSH, multiplexers,
|
|
78
|
+
* and unrecognized profiles. This is the per-terminal counterpart to the blanket
|
|
79
|
+
* block from #1610/#1682/#1746: those hosts genuinely cannot treat a submit as
|
|
80
|
+
* proof of at-tail, but genuine local terminals can.
|
|
81
|
+
*/
|
|
82
|
+
export declare function detectSubmitPinsViewportToTail(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
54
83
|
/**
|
|
55
84
|
* Resolve an explicit user override for DEC 2026 synchronized output. Returns
|
|
56
85
|
* `false` for an opt-out, `true` for a force-on, or `null` when the user has
|
|
@@ -96,7 +125,22 @@ export declare function shouldEnableSynchronizedOutputByDefault(env?: NodeJS.Pro
|
|
|
96
125
|
*/
|
|
97
126
|
export declare function detectRectangularSgrSupport(terminalId: TerminalId, env?: NodeJS.ProcessEnv): boolean;
|
|
98
127
|
export declare const TERMINAL_ID: TerminalId;
|
|
99
|
-
|
|
128
|
+
/**
|
|
129
|
+
* The process-wide {@link TERMINAL} singleton: a {@link TerminalInfo} whose
|
|
130
|
+
* post-construction capabilities — the image protocol and the probe-driven
|
|
131
|
+
* flags — are writable, so the runtime setters and tests mutate them directly
|
|
132
|
+
* instead of through an unsound cast. Every other field stays readonly.
|
|
133
|
+
*/
|
|
134
|
+
export interface RuntimeTerminal extends TerminalInfo {
|
|
135
|
+
imageProtocol: ImageProtocol | null;
|
|
136
|
+
hyperlinks: boolean;
|
|
137
|
+
eagerEraseScrollbackRisk: boolean;
|
|
138
|
+
deccara: boolean;
|
|
139
|
+
supportsScreenToScrollback: boolean;
|
|
140
|
+
textSizing: boolean;
|
|
141
|
+
submitPinsViewportToTail: boolean;
|
|
142
|
+
}
|
|
143
|
+
export declare const TERMINAL: RuntimeTerminal;
|
|
100
144
|
/**
|
|
101
145
|
* Override terminal image protocol at runtime after capability probes complete.
|
|
102
146
|
*/
|
|
@@ -109,6 +153,8 @@ export declare function setTerminalImageProtocol(imageProtocol: ImageProtocol |
|
|
|
109
153
|
export declare function setTerminalDeccara(enabled: boolean): void;
|
|
110
154
|
/** Override screen-to-scrollback clear support for targeted renderer tests. */
|
|
111
155
|
export declare function setTerminalScreenToScrollback(enabled: boolean): void;
|
|
156
|
+
/** Override submit-pins-viewport-to-tail for checkpoint reconciliation tests. */
|
|
157
|
+
export declare function setTerminalSubmitPinsViewportToTail(enabled: boolean): void;
|
|
112
158
|
/**
|
|
113
159
|
* Enable/disable OSC 66 text-sizing at runtime. The coding-agent calls this from
|
|
114
160
|
* the `tui.textSizing` setting (gated on the terminal's static `textSizing`
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ type InputListenerResult = {
|
|
|
6
6
|
data?: string;
|
|
7
7
|
} | undefined;
|
|
8
8
|
type InputListener = (data: string) => InputListenerResult;
|
|
9
|
+
type StartListener = () => void;
|
|
9
10
|
export interface RenderTimer {
|
|
10
11
|
cancel(): void;
|
|
11
12
|
}
|
|
@@ -17,6 +18,10 @@ export interface RenderScheduler {
|
|
|
17
18
|
export interface TUIOptions {
|
|
18
19
|
renderScheduler?: RenderScheduler;
|
|
19
20
|
}
|
|
21
|
+
export interface TUIStartOptions {
|
|
22
|
+
/** Clear saved native scrollback before the first paint. */
|
|
23
|
+
clearScrollback?: boolean;
|
|
24
|
+
}
|
|
20
25
|
/**
|
|
21
26
|
* Component interface - all components must implement this
|
|
22
27
|
*/
|
|
@@ -37,10 +42,17 @@ export interface Component {
|
|
|
37
42
|
*/
|
|
38
43
|
wantsKeyRelease?: boolean;
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
45
|
+
* Optional hook to invalidate any cached rendering state.
|
|
41
46
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
42
47
|
*/
|
|
43
|
-
invalidate(): void;
|
|
48
|
+
invalidate?(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Optional teardown. Called when the component is permanently removed from
|
|
51
|
+
* the live tree (e.g. a transcript reset). Release timers, intervals, and
|
|
52
|
+
* subscriptions here. Must be idempotent. Containers propagate dispose to
|
|
53
|
+
* their children; leaf components without resources may omit it.
|
|
54
|
+
*/
|
|
55
|
+
dispose?(): void;
|
|
44
56
|
}
|
|
45
57
|
/**
|
|
46
58
|
* Optional component seam for native-scrollback pinning. A component that
|
|
@@ -86,15 +98,14 @@ export interface RenderRequestOptions {
|
|
|
86
98
|
/** Clear terminal scrollback for intentional transcript replacement. */
|
|
87
99
|
clearScrollback?: boolean;
|
|
88
100
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* `Terminal#isNativeViewportAtBottom()` cannot answer.
|
|
101
|
+
* Allow a transient live-viewport repaint when the terminal cannot report
|
|
102
|
+
* whether its native viewport is at the tail.
|
|
92
103
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
104
|
+
* This is **not** a settled transcript commit and must not be used for tool
|
|
105
|
+
* completion, session replay, or other background/offscreen rewrites. On
|
|
106
|
+
* ED3-risk terminals it may deliberately choose a viewport repaint/deferred
|
|
107
|
+
* shrink without clearing native scrollback so autocomplete, IME, and focused
|
|
108
|
+
* editor chrome stay responsive without yanking a scrolled reader.
|
|
98
109
|
*/
|
|
99
110
|
allowUnknownViewportMutation?: boolean;
|
|
100
111
|
}
|
|
@@ -156,6 +167,15 @@ export interface OverlayOptions {
|
|
|
156
167
|
* Called each render cycle with current terminal dimensions.
|
|
157
168
|
*/
|
|
158
169
|
visible?: (termWidth: number, termHeight: number) => boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Borrow the terminal's alternate screen buffer for this overlay's lifetime
|
|
172
|
+
* (vim/less idiom). While the topmost visible overlay sets this, the engine
|
|
173
|
+
* paints only the modal on the alt screen and emits no ED3 / scrollback
|
|
174
|
+
* bytes, so the transcript on the normal screen stays untouched and is not
|
|
175
|
+
* scrollable behind the modal. Defaults off — all other overlays are
|
|
176
|
+
* unchanged and still draw over the transcript on the normal screen.
|
|
177
|
+
*/
|
|
178
|
+
fullscreen?: boolean;
|
|
159
179
|
}
|
|
160
180
|
/**
|
|
161
181
|
* Handle returned by showOverlay for controlling the overlay
|
|
@@ -177,6 +197,12 @@ export declare class Container implements Component {
|
|
|
177
197
|
removeChild(component: Component): void;
|
|
178
198
|
clear(): void;
|
|
179
199
|
invalidate(): void;
|
|
200
|
+
/**
|
|
201
|
+
* Propagate teardown to children. Call when the container's children are
|
|
202
|
+
* being permanently discarded (not when they are detached for reuse — use
|
|
203
|
+
* {@link clear} for that). Idempotent per child via each child's own dispose.
|
|
204
|
+
*/
|
|
205
|
+
dispose(): void;
|
|
180
206
|
render(width: number): string[];
|
|
181
207
|
}
|
|
182
208
|
/**
|
|
@@ -255,7 +281,8 @@ export declare class TUI extends Container {
|
|
|
255
281
|
/** Check if there are any visible overlays */
|
|
256
282
|
hasOverlay(): boolean;
|
|
257
283
|
invalidate(): void;
|
|
258
|
-
start(): void;
|
|
284
|
+
start(options?: TUIStartOptions): void;
|
|
285
|
+
addStartListener(listener: StartListener): () => void;
|
|
259
286
|
addInputListener(listener: InputListener): () => void;
|
|
260
287
|
removeInputListener(listener: InputListener): void;
|
|
261
288
|
stop(): void;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -33,9 +33,12 @@ export declare function padding(n: number): string;
|
|
|
33
33
|
* Get the shared grapheme segmenter instance.
|
|
34
34
|
*/
|
|
35
35
|
export declare function getSegmenter(): Intl.Segmenter;
|
|
36
|
-
export declare function visibleWidthRaw(str: string): number;
|
|
37
36
|
/**
|
|
38
|
-
*
|
|
37
|
+
* Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
|
|
38
|
+
*
|
|
39
|
+
* `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
|
|
40
|
+
* stripping); this adds the two corrections it omits — tabs (expanded to
|
|
41
|
+
* `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
|
|
39
42
|
*/
|
|
40
43
|
export declare function visibleWidth(str: string): number;
|
|
41
44
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.10.
|
|
4
|
+
"version": "15.10.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.10.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.10.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.10.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.10.1",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -1495,6 +1495,11 @@ export class Editor implements Component, Focusable {
|
|
|
1495
1495
|
this.#insertTextAtCursor(text);
|
|
1496
1496
|
}
|
|
1497
1497
|
|
|
1498
|
+
/** Apply terminal paste semantics to text from non-bracketed paste transports. */
|
|
1499
|
+
pasteText(text: string): void {
|
|
1500
|
+
this.#handlePaste(text);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1498
1503
|
// All the editor methods from before...
|
|
1499
1504
|
#insertCharacter(char: string): void {
|
|
1500
1505
|
this.#exitHistoryForEditing();
|
package/src/components/loader.ts
CHANGED
|
@@ -3,21 +3,20 @@ import { sliceByColumn, visibleWidth } from "../utils";
|
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Loader component
|
|
7
|
-
* message colorizer is time-dependent (e.g. shimmer/KITT) animate smoothly.
|
|
6
|
+
* Loader component. Spinner frames advance at `SPINNER_ADVANCE_MS`.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* The TUI already throttles at 16ms (`MIN_RENDER_INTERVAL_MS`), so this
|
|
12
|
-
* is the natural upper bound; static messageColorFns produce identical
|
|
13
|
-
* output and the differ drops the no-op redraw at ~zero cost.
|
|
14
|
-
* - **Spinner advance** (every `SPINNER_ADVANCE_MS`) → bumps the spinner
|
|
15
|
-
* frame index. Decoupled from the render cadence so the spinner keeps
|
|
16
|
-
* its classic ~12.5fps step pace regardless of shimmer state.
|
|
8
|
+
* Message colorizers that are time-dependent can opt into 30fps redraws by
|
|
9
|
+
* setting `animated` to `true` on the function object.
|
|
17
10
|
*/
|
|
18
|
-
const RENDER_INTERVAL_MS =
|
|
11
|
+
const RENDER_INTERVAL_MS = 1000 / 30;
|
|
19
12
|
const SPINNER_ADVANCE_MS = 80;
|
|
20
13
|
|
|
14
|
+
type ColorFn = (str: string) => string;
|
|
15
|
+
|
|
16
|
+
export type LoaderMessageColorFn = ColorFn & {
|
|
17
|
+
readonly animated?: true;
|
|
18
|
+
};
|
|
19
|
+
|
|
21
20
|
export class Loader extends Text {
|
|
22
21
|
#frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
22
|
#currentFrame = 0;
|
|
@@ -27,8 +26,8 @@ export class Loader extends Text {
|
|
|
27
26
|
|
|
28
27
|
constructor(
|
|
29
28
|
ui: TUI,
|
|
30
|
-
private spinnerColorFn:
|
|
31
|
-
private messageColorFn:
|
|
29
|
+
private spinnerColorFn: ColorFn,
|
|
30
|
+
private messageColorFn: LoaderMessageColorFn,
|
|
32
31
|
private message: string = "Loading...",
|
|
33
32
|
spinnerFrames?: string[],
|
|
34
33
|
) {
|
|
@@ -54,14 +53,17 @@ export class Loader extends Text {
|
|
|
54
53
|
start() {
|
|
55
54
|
this.#lastSpinnerTick = performance.now();
|
|
56
55
|
this.#updateDisplay();
|
|
56
|
+
const intervalMs = this.messageColorFn.animated === true ? RENDER_INTERVAL_MS : SPINNER_ADVANCE_MS;
|
|
57
57
|
this.#intervalId = setInterval(() => {
|
|
58
58
|
const now = performance.now();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const elapsed = now - this.#lastSpinnerTick;
|
|
60
|
+
if (elapsed >= SPINNER_ADVANCE_MS) {
|
|
61
|
+
const steps = Math.floor(elapsed / SPINNER_ADVANCE_MS);
|
|
62
|
+
this.#currentFrame = (this.#currentFrame + steps) % this.#frames.length;
|
|
63
|
+
this.#lastSpinnerTick += steps * SPINNER_ADVANCE_MS;
|
|
62
64
|
}
|
|
63
65
|
this.#updateDisplay();
|
|
64
|
-
},
|
|
66
|
+
}, intervalMs);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
stop() {
|
|
@@ -71,7 +73,15 @@ export class Loader extends Text {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/** Lifecycle teardown: stop the animation timer. Idempotent. */
|
|
77
|
+
dispose() {
|
|
78
|
+
this.stop();
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
setMessage(message: string) {
|
|
82
|
+
if (message === this.message) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
75
85
|
this.message = message;
|
|
76
86
|
this.#updateDisplay();
|
|
77
87
|
}
|
|
@@ -58,7 +58,8 @@ markdownParser.setOptions({
|
|
|
58
58
|
// (Rust FFI) work for content/layout combinations already seen this session.
|
|
59
59
|
|
|
60
60
|
const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
|
|
61
|
-
const
|
|
61
|
+
const EMPTY_RENDER_LINES: readonly string[] = [];
|
|
62
|
+
const renderCache = new LRUCache<string, readonly string[]>({ max: RENDER_CACHE_MAX });
|
|
62
63
|
|
|
63
64
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
64
65
|
export function clearRenderCache(): void {
|
|
@@ -288,10 +289,11 @@ export class Markdown implements Component {
|
|
|
288
289
|
/** Number of spaces used to indent code block content. */
|
|
289
290
|
#codeBlockIndent: number;
|
|
290
291
|
|
|
291
|
-
// Cache for rendered output
|
|
292
|
+
// Cache for rendered output. Cached arrays are internal snapshots; render()
|
|
293
|
+
// returns caller-owned arrays because several renderers append surrounding rows.
|
|
292
294
|
#cachedText?: string;
|
|
293
295
|
#cachedWidth?: number;
|
|
294
|
-
#cachedLines?: string[];
|
|
296
|
+
#cachedLines?: readonly string[];
|
|
295
297
|
|
|
296
298
|
constructor(
|
|
297
299
|
text: string,
|
|
@@ -324,7 +326,7 @@ export class Markdown implements Component {
|
|
|
324
326
|
// L1: per-instance cache — fastest path for repeated renders of the same
|
|
325
327
|
// instance at the same width (e.g. resize debounce, repeated redraws).
|
|
326
328
|
if (this.#cachedLines && this.#cachedText === this.#text && this.#cachedWidth === width) {
|
|
327
|
-
return this.#cachedLines;
|
|
329
|
+
return this.#cachedLines.slice();
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
// Calculate available width for content (subtract horizontal padding)
|
|
@@ -332,12 +334,10 @@ export class Markdown implements Component {
|
|
|
332
334
|
|
|
333
335
|
// Don't render anything if there's no actual text
|
|
334
336
|
if (!this.#text || this.#text.trim() === "") {
|
|
335
|
-
const result: string[] = [];
|
|
336
|
-
// Update per-instance cache
|
|
337
337
|
this.#cachedText = this.#text;
|
|
338
338
|
this.#cachedWidth = width;
|
|
339
|
-
this.#cachedLines =
|
|
340
|
-
return
|
|
339
|
+
this.#cachedLines = EMPTY_RENDER_LINES;
|
|
340
|
+
return [];
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
// Replace tabs with 3 spaces for consistent rendering
|
|
@@ -364,7 +364,7 @@ export class Markdown implements Component {
|
|
|
364
364
|
this.#cachedText = this.#text;
|
|
365
365
|
this.#cachedWidth = width;
|
|
366
366
|
this.#cachedLines = cached;
|
|
367
|
-
return cached;
|
|
367
|
+
return cached.slice();
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
// Parse markdown to HTML-like tokens
|
|
@@ -445,14 +445,16 @@ export class Markdown implements Component {
|
|
|
445
445
|
const rawResult = [...emptyLines, ...contentLines, ...emptyLines];
|
|
446
446
|
const result = rawResult.length > 0 ? rawResult : [""];
|
|
447
447
|
|
|
448
|
-
// Update
|
|
448
|
+
// Update caches with a private snapshot. The returned array remains owned by
|
|
449
|
+
// the caller, so push/splice by tool renderers cannot poison future redraws.
|
|
450
|
+
const cachedLines = result.slice();
|
|
449
451
|
this.#cachedText = this.#text;
|
|
450
452
|
this.#cachedWidth = width;
|
|
451
|
-
this.#cachedLines =
|
|
453
|
+
this.#cachedLines = cachedLines;
|
|
452
454
|
|
|
453
455
|
// Update L2 module-level LRU so future instances with the same key skip
|
|
454
456
|
// the marked.lexer + highlightCode (Rust FFI) work entirely.
|
|
455
|
-
renderCache.set(cacheKey,
|
|
457
|
+
renderCache.set(cacheKey, cachedLines);
|
|
456
458
|
|
|
457
459
|
return result;
|
|
458
460
|
}
|