@oh-my-pi/pi-tui 18.1.17 → 18.1.19
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/dist/types/mouse.d.ts +5 -5
- package/dist/types/tui.d.ts +22 -0
- package/package.json +3 -3
- package/src/mouse.ts +5 -5
- package/src/tui.ts +106 -17
package/dist/types/mouse.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SGR mouse report parsing (`\x1b[<button;col;rowM` / `…m`).
|
|
3
3
|
*
|
|
4
|
-
* Mouse tracking is enabled
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the frame paints from screen row 0, hence
|
|
8
|
-
* 0-based for direct indexing into rendered lines.
|
|
4
|
+
* Mouse tracking is enabled while a fullscreen overlay holds the alternate
|
|
5
|
+
* screen (see tui.ts MOUSE_TRACKING_ON), or — opt-in via `tui.mouse` — on the
|
|
6
|
+
* normal buffer whenever no overlay is visible. Consumers hit-test
|
|
7
|
+
* against their own rendered frame: the frame paints from screen row 0, hence
|
|
8
|
+
* `row`/`col` are exposed 0-based for direct indexing into rendered lines.
|
|
9
9
|
*/
|
|
10
10
|
/** A decoded SGR mouse report. */
|
|
11
11
|
export interface SgrMouseEvent {
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -334,6 +334,28 @@ export declare class TUI extends Container {
|
|
|
334
334
|
hideOverlay(): void;
|
|
335
335
|
/** Check if there are any visible overlays */
|
|
336
336
|
hasOverlay(): boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Mutable normal-buffer viewport from the last provider frame: screen row
|
|
339
|
+
* where it begins plus its row count. Inline click targets are indexed
|
|
340
|
+
* into this window (`screenRow - top`). Empty while the alt screen owns
|
|
341
|
+
* the display, while a resize transaction is settling, and while a Ghostty
|
|
342
|
+
* image paint is deferred — the painted rows predate the latest spans in
|
|
343
|
+
* all three cases, so hits would map to unrelated old rows.
|
|
344
|
+
* The origin is in composer rows: a replay paint replaces leading composer
|
|
345
|
+
* blanks with history rows and prepends blanks for a short viewport, so
|
|
346
|
+
* the painted top is backed out by that net pad.
|
|
347
|
+
*/
|
|
348
|
+
getMutableViewport(): {
|
|
349
|
+
top: number;
|
|
350
|
+
length: number;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Probe for opt-in normal-buffer click capture. The provider is read every
|
|
354
|
+
* frame; while it returns true (and no fullscreen overlay owns the
|
|
355
|
+
* display) the terminal reports button clicks as SGR events for inline
|
|
356
|
+
* click targets. Native text selection becomes Shift+drag while on.
|
|
357
|
+
*/
|
|
358
|
+
setInlineMouseTrackingProvider(provider: (() => boolean) | undefined): void;
|
|
337
359
|
invalidate(): void;
|
|
338
360
|
start(options?: TUIStartOptions): void;
|
|
339
361
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.1.
|
|
4
|
+
"version": "18.1.19",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.1.19",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.1.19"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/mouse.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SGR mouse report parsing (`\x1b[<button;col;rowM` / `…m`).
|
|
3
3
|
*
|
|
4
|
-
* Mouse tracking is enabled
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the frame paints from screen row 0, hence
|
|
8
|
-
* 0-based for direct indexing into rendered lines.
|
|
4
|
+
* Mouse tracking is enabled while a fullscreen overlay holds the alternate
|
|
5
|
+
* screen (see tui.ts MOUSE_TRACKING_ON), or — opt-in via `tui.mouse` — on the
|
|
6
|
+
* normal buffer whenever no overlay is visible. Consumers hit-test
|
|
7
|
+
* against their own rendered frame: the frame paints from screen row 0, hence
|
|
8
|
+
* `row`/`col` are exposed 0-based for direct indexing into rendered lines.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** A decoded SGR mouse report. */
|
package/src/tui.ts
CHANGED
|
@@ -81,13 +81,16 @@ const PAINT_END = `${ENABLE_AUTOWRAP}${SYNC_OUTPUT_END}`;
|
|
|
81
81
|
const PAINT_BEGIN_NO_SYNC = `${HIDE_CURSOR}${DISABLE_AUTOWRAP}`;
|
|
82
82
|
const PAINT_END_NO_SYNC = ENABLE_AUTOWRAP;
|
|
83
83
|
// Mouse reporting is scoped to fullscreen overlays that opt into pointer
|
|
84
|
-
// interaction
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// interaction, plus the opt-in normal-buffer click capture (`tui.mouse`).
|
|
85
|
+
// 1000h = button click tracking, 1003h = any-motion tracking for hover
|
|
86
|
+
// targets, and 1006h = SGR extended coordinates past column/row 223.
|
|
87
|
+
// Selection-first surfaces leave these modes disabled so the terminal retains
|
|
87
88
|
// native text selection.
|
|
88
89
|
const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
89
90
|
const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
90
91
|
|
|
92
|
+
type MouseTrackingState = "off" | "inline" | "full";
|
|
93
|
+
|
|
91
94
|
/**
|
|
92
95
|
* `PI_TUI_RESIZE_IN_PLACE=1|true` forces in-place resize (no alt-buffer borrow).
|
|
93
96
|
* `0|false` forces the alt-buffer path even on Warp. Unset defers to Warp detection:
|
|
@@ -665,6 +668,11 @@ export class TUI extends Container {
|
|
|
665
668
|
// Screen row where the provider's mutable viewport begins (0-based); rows
|
|
666
669
|
// above it hold history still visible on the physical screen.
|
|
667
670
|
#providerViewportTop = 0;
|
|
671
|
+
// Net composer-space offset of the published hit-test origin behind the
|
|
672
|
+
// painted top, from the last paint: replay-replaced rows minus viewport
|
|
673
|
+
// rows the paint prepended for a short viewport. Negative while prepended
|
|
674
|
+
// blanks outweigh replaced rows; zero on ordinary frames.
|
|
675
|
+
#providerViewportPadTop = 0;
|
|
668
676
|
// Viewport-relative row of the hardware cursor after the last normal paint
|
|
669
677
|
// (0 = parked at the viewport top). A resize reflows the normal buffer
|
|
670
678
|
// before the app hears about it; terminals keep the cursor attached to its
|
|
@@ -827,7 +835,9 @@ export class TUI extends Container {
|
|
|
827
835
|
// untouched, so exiting reconciles cleanly against the terminal-restored
|
|
828
836
|
// normal screen. #altPreviousLines is the last alt frame, for repaint-skip.
|
|
829
837
|
#altActive = false;
|
|
830
|
-
#
|
|
838
|
+
#mouseTracking: MouseTrackingState = "off";
|
|
839
|
+
/** Product-owned probe for opt-in normal-buffer click capture (`tui.mouse`). Read every frame. */
|
|
840
|
+
#inlineMouseProvider: (() => boolean) | undefined;
|
|
831
841
|
#altPreviousLines: string[] = [];
|
|
832
842
|
#altEnterWidth = 0;
|
|
833
843
|
#altEnterHeight = 0;
|
|
@@ -1088,6 +1098,54 @@ export class TUI extends Container {
|
|
|
1088
1098
|
return this.overlayStack.some(o => this.#isOverlayVisible(o));
|
|
1089
1099
|
}
|
|
1090
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* Mutable normal-buffer viewport from the last provider frame: screen row
|
|
1103
|
+
* where it begins plus its row count. Inline click targets are indexed
|
|
1104
|
+
* into this window (`screenRow - top`). Empty while the alt screen owns
|
|
1105
|
+
* the display, while a resize transaction is settling, and while a Ghostty
|
|
1106
|
+
* image paint is deferred — the painted rows predate the latest spans in
|
|
1107
|
+
* all three cases, so hits would map to unrelated old rows.
|
|
1108
|
+
* The origin is in composer rows: a replay paint replaces leading composer
|
|
1109
|
+
* blanks with history rows and prepends blanks for a short viewport, so
|
|
1110
|
+
* the painted top is backed out by that net pad.
|
|
1111
|
+
*/
|
|
1112
|
+
getMutableViewport(): { top: number; length: number } {
|
|
1113
|
+
if (
|
|
1114
|
+
this.#altActive ||
|
|
1115
|
+
this.#resizeAltActive ||
|
|
1116
|
+
this.#resizeProbe !== undefined ||
|
|
1117
|
+
this.#resizeInPlaceActive ||
|
|
1118
|
+
this.#ghosttyInitialImageDelayTimer !== undefined
|
|
1119
|
+
) {
|
|
1120
|
+
return { top: 0, length: 0 };
|
|
1121
|
+
}
|
|
1122
|
+
return { top: this.#providerViewportTop - this.#providerViewportPadTop, length: this.#providerWindow.length };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Probe for opt-in normal-buffer click capture. The provider is read every
|
|
1127
|
+
* frame; while it returns true (and no fullscreen overlay owns the
|
|
1128
|
+
* display) the terminal reports button clicks as SGR events for inline
|
|
1129
|
+
* click targets. Native text selection becomes Shift+drag while on.
|
|
1130
|
+
*/
|
|
1131
|
+
setInlineMouseTrackingProvider(provider: (() => boolean) | undefined): void {
|
|
1132
|
+
this.#inlineMouseProvider = provider;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Transition mouse reporting, emitting only the sequences a change needs. */
|
|
1136
|
+
#setMouseTracking(state: MouseTrackingState): void {
|
|
1137
|
+
if (state === this.#mouseTracking) return;
|
|
1138
|
+
const wasOff = this.#mouseTracking === "off";
|
|
1139
|
+
this.#mouseTracking = state;
|
|
1140
|
+
if (state === "off") {
|
|
1141
|
+
if (!wasOff) this.terminal.write(MOUSE_TRACKING_OFF);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
// Inline and fullscreen reporting are the same bytes: moving between
|
|
1145
|
+
// live modes needs no emission, only entering from off does.
|
|
1146
|
+
if (wasOff) this.terminal.write(MOUSE_TRACKING_ON);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1091
1149
|
/** Check if an overlay entry is currently visible */
|
|
1092
1150
|
#isOverlayVisible(entry: (typeof this.overlayStack)[number]): boolean {
|
|
1093
1151
|
if (entry.hidden) return false;
|
|
@@ -1562,6 +1620,9 @@ export class TUI extends Container {
|
|
|
1562
1620
|
fs.appendFileSync(getDebugLogPath(), msg);
|
|
1563
1621
|
}
|
|
1564
1622
|
this.#providerViewportTop = Math.min(top, Math.max(0, height - 1));
|
|
1623
|
+
// Resolved geometry invalidates the replay offset with the old anchor;
|
|
1624
|
+
// the forced repaint recomputes it (usually zero).
|
|
1625
|
+
this.#providerViewportPadTop = 0;
|
|
1565
1626
|
this.#forceViewportRepaintOnNextRender = true;
|
|
1566
1627
|
this.requestRender(true);
|
|
1567
1628
|
}
|
|
@@ -1808,14 +1869,27 @@ export class TUI extends Container {
|
|
|
1808
1869
|
setAltScreenActive(false);
|
|
1809
1870
|
}
|
|
1810
1871
|
if (this.#altActive || this.#pendingAltExit) {
|
|
1811
|
-
|
|
1812
|
-
|
|
1872
|
+
// A pending fused exit may have been built without an OFF write to
|
|
1873
|
+
// keep inline capture alive across the restore — at process quit
|
|
1874
|
+
// nothing continues, so release unconditionally. The pending
|
|
1875
|
+
// sequence itself can re-enable tracking (overlay-close restore),
|
|
1876
|
+
// so the final OFF goes last or the shell keeps reporting.
|
|
1877
|
+
const mouseExit = this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
|
|
1878
|
+
const exitSequence = this.#pendingAltExit
|
|
1879
|
+
? `${this.#pendingAltExit}${mouseExit}`
|
|
1880
|
+
: `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
|
1813
1881
|
this.terminal.write(exitSequence);
|
|
1814
1882
|
setAltScreenActive(false);
|
|
1815
1883
|
this.#altActive = false;
|
|
1816
|
-
this.#
|
|
1884
|
+
this.#mouseTracking = "off";
|
|
1817
1885
|
this.#altPreviousLines = [];
|
|
1818
1886
|
this.#pendingAltExit = "";
|
|
1887
|
+
} else if (this.#mouseTracking !== "off") {
|
|
1888
|
+
// Inline capture with no overlay: still owned by us at quit, so
|
|
1889
|
+
// release it — otherwise the parent shell keeps mouse reporting
|
|
1890
|
+
// and loses native selection until a manual reset.
|
|
1891
|
+
this.terminal.write(MOUSE_TRACKING_OFF);
|
|
1892
|
+
this.#mouseTracking = "off";
|
|
1819
1893
|
}
|
|
1820
1894
|
// A latched destructive reset (settled rebuild-mode resize, /clear) pairs
|
|
1821
1895
|
// ED3 with a complete-ledger replay. Running that pair during stop would
|
|
@@ -2567,9 +2641,11 @@ export class TUI extends Container {
|
|
|
2567
2641
|
|
|
2568
2642
|
let historyRows = history?.rows ?? [];
|
|
2569
2643
|
let replayViewportRows = 0;
|
|
2644
|
+
let replayPrependedBlanks = 0;
|
|
2570
2645
|
if (history?.kind === "replay") {
|
|
2571
2646
|
// Providers may omit unused leading rows from a short viewport. Make
|
|
2572
2647
|
// that logical space explicit before the bottom-first replay split.
|
|
2648
|
+
replayPrependedBlanks = Math.max(0, height - viewport.length);
|
|
2573
2649
|
while (viewport.length < height) viewport.unshift("");
|
|
2574
2650
|
let leadingBlankRows = 0;
|
|
2575
2651
|
while (leadingBlankRows < viewport.length && !/\S/.test(viewport[leadingBlankRows]!)) {
|
|
@@ -2718,6 +2794,7 @@ export class TUI extends Container {
|
|
|
2718
2794
|
else this.#recordHardwareCursorHidden();
|
|
2719
2795
|
this.#providerWindow = mutablePrepared;
|
|
2720
2796
|
this.#providerViewportTop = mutableTop;
|
|
2797
|
+
this.#providerViewportPadTop = replayViewportRows - replayPrependedBlanks;
|
|
2721
2798
|
this.#previousWidth = width;
|
|
2722
2799
|
this.#previousHeight = height;
|
|
2723
2800
|
this.#resizeBurstGrew = false;
|
|
@@ -2765,28 +2842,41 @@ export class TUI extends Container {
|
|
|
2765
2842
|
// modal there; the normal screen and all accounting stay untouched.
|
|
2766
2843
|
const topOverlay = this.#getTopmostVisibleOverlay();
|
|
2767
2844
|
const wantAlt = topOverlay?.options?.fullscreen === true;
|
|
2768
|
-
const
|
|
2845
|
+
const wantMouse: MouseTrackingState =
|
|
2846
|
+
topOverlay === undefined
|
|
2847
|
+
? this.#inlineMouseProvider?.() === true
|
|
2848
|
+
? "inline"
|
|
2849
|
+
: "off"
|
|
2850
|
+
: wantAlt && topOverlay.options?.mouseTracking !== false
|
|
2851
|
+
? "full"
|
|
2852
|
+
: "off";
|
|
2769
2853
|
if (wantAlt && !this.#altActive) {
|
|
2770
2854
|
// Enhanced keyboard modes can be buffer-local: re-push the active
|
|
2771
2855
|
// modified-key reporting sequence on the freshly entered alternate
|
|
2772
2856
|
// screen, or Esc/modified keys revert to legacy encoding inside
|
|
2773
2857
|
// fullscreen overlays (Ghostty/kitty/iTerm2).
|
|
2774
2858
|
this.#noteAltBufferToggle();
|
|
2775
|
-
|
|
2776
|
-
this
|
|
2859
|
+
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}`);
|
|
2860
|
+
this.#setMouseTracking(wantMouse);
|
|
2777
2861
|
setAltScreenActive(true);
|
|
2778
2862
|
this.terminal.hideCursor();
|
|
2779
2863
|
this.#forgetHardwareCursorState();
|
|
2780
2864
|
this.#recordHardwareCursorHidden();
|
|
2781
2865
|
this.#altActive = true;
|
|
2782
|
-
this.#altMouseTrackingActive = wantMouseTracking;
|
|
2783
2866
|
this.#altPreviousLines = [];
|
|
2784
2867
|
this.#altEnterWidth = width;
|
|
2785
2868
|
this.#altEnterHeight = height;
|
|
2786
2869
|
} else if (!wantAlt && this.#altActive) {
|
|
2787
|
-
|
|
2870
|
+
// Leaving reporting on when the normal buffer wants it restores
|
|
2871
|
+
// inline capture the same frame the overlay closes: no later paint
|
|
2872
|
+
// is needed, so an idle session never sits untrackable.
|
|
2873
|
+
const mouseExit = wantMouse === "off" && this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
|
|
2874
|
+
// A fullscreen overlay that disabled reporting leaves tracking off:
|
|
2875
|
+
// restore it in the fused exit or later frames see matching states
|
|
2876
|
+
// and inline click/hover stays dead until the setting toggles.
|
|
2877
|
+
const mouseEnter = wantMouse !== "off" && this.#mouseTracking === "off" ? MOUSE_TRACKING_ON : "";
|
|
2788
2878
|
const enhancementExit = this.#keyboardEnhancementExit();
|
|
2789
|
-
const exitSequence = `${mouseExit}${enhancementExit}\x1b[?1049l`;
|
|
2879
|
+
const exitSequence = `${mouseExit}${mouseEnter}${enhancementExit}\x1b[?1049l`;
|
|
2790
2880
|
// Session replacement finishes while its fullscreen selector still
|
|
2791
2881
|
// covers the old normal buffer. Fuse the restore into the destructive
|
|
2792
2882
|
// repaint so no stale frame can become visible between writes.
|
|
@@ -2799,7 +2889,7 @@ export class TUI extends Container {
|
|
|
2799
2889
|
}
|
|
2800
2890
|
this.#forgetHardwareCursorState();
|
|
2801
2891
|
this.#altActive = false;
|
|
2802
|
-
this.#
|
|
2892
|
+
this.#mouseTracking = wantMouse;
|
|
2803
2893
|
this.#altPreviousLines = [];
|
|
2804
2894
|
// The alt-buffer restore put the pre-overlay normal screen back. If
|
|
2805
2895
|
// that buffer resized while covered, its cursor moved with width
|
|
@@ -2813,9 +2903,8 @@ export class TUI extends Container {
|
|
|
2813
2903
|
}
|
|
2814
2904
|
this.#forceViewportRepaintOnNextRender = true;
|
|
2815
2905
|
}
|
|
2816
|
-
} else if (
|
|
2817
|
-
this
|
|
2818
|
-
this.#altMouseTrackingActive = wantMouseTracking;
|
|
2906
|
+
} else if (wantMouse !== this.#mouseTracking) {
|
|
2907
|
+
this.#setMouseTracking(wantMouse);
|
|
2819
2908
|
}
|
|
2820
2909
|
if (this.#altActive) {
|
|
2821
2910
|
this.#renderAltFrame(width, height);
|