@oh-my-pi/pi-tui 18.1.13 → 18.1.15
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 +18 -1
- package/dist/types/components/markdown.d.ts +1 -1
- package/package.json +3 -3
- package/src/components/markdown.ts +6 -5
- package/src/tui.ts +228 -52
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [18.1.
|
|
5
|
+
## [18.1.15] - 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Stopped long flicker when moving or resizing an omp pane in Warp. Resize repaints in place there after the drag settles (override with `PI_TUI_RESIZE_IN_PLACE=0`), with no alternate-screen borrow, no scrollback replay, blanked live rows so shrink drags cannot archive unfinished rows, and overlay toggle echoes repainting the modal instead of probing ([#11247](https://github.com/can1357/oh-my-pi/pull/11247) by [@H4vC](https://github.com/H4vC)).
|
|
10
|
+
|
|
11
|
+
## [18.1.14] - 2026-09-07
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `extractMarkdownLinks()` now returns one-row visible labels for formatted and multiline links ([#11086](https://github.com/can1357/oh-my-pi/pull/11086) by [@mustafaabidali](https://github.com/mustafaabidali)).
|
|
16
|
+
|
|
17
|
+
## [18.1.13] - 2026-09-07
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
8
20
|
|
|
9
21
|
- Fixed notifications never arriving in a Herdr pane. Herdr multiplexes panes like tmux but swallows bare OSC 9 / OSC 99 and has no passthrough envelope, so a backgrounded pane got no signal at all; delivery now goes through `herdr notification show` (a waiting question or an error rings `request`, a settled turn rings `done`), and the in-band write stays as the fallback when the pane id or the `herdr` binary is missing.
|
|
22
|
+
|
|
23
|
+
## [18.1.12] - 2026-09-06
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
10
27
|
- Avoid inserting a trailing space when auto-completing directory paths with `@`, and keep autocomplete open when accepting a directory with Tab or Enter.
|
|
11
28
|
- Horizontal wheel reports (the sideways drift of a two-finger trackpad scroll) no longer decode as a vertical wheel direction, so fullscreen selectors such as `/copy` and the rewind picker stop jumping up and back down at the end of a scroll gesture.
|
|
12
29
|
|
|
@@ -14,7 +14,7 @@ export declare function resetFastTailSplices(): void;
|
|
|
14
14
|
export declare function fastLineStartHazard(grownLine: string): boolean;
|
|
15
15
|
/** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
|
|
16
16
|
export interface MarkdownLink {
|
|
17
|
-
/**
|
|
17
|
+
/** Flattened visible label with whitespace collapsed to one row; falls back to `href` when empty. */
|
|
18
18
|
text: string;
|
|
19
19
|
/** Destination exactly as marked resolved it (references resolved, no normalization). */
|
|
20
20
|
href: string;
|
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.15",
|
|
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.15",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.1.15"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -1256,7 +1256,7 @@ function lexDocument(text: string): Token[] {
|
|
|
1256
1256
|
|
|
1257
1257
|
/** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
|
|
1258
1258
|
export interface MarkdownLink {
|
|
1259
|
-
/**
|
|
1259
|
+
/** Flattened visible label with whitespace collapsed to one row; falls back to `href` when empty. */
|
|
1260
1260
|
text: string;
|
|
1261
1261
|
/** Destination exactly as marked resolved it (references resolved, no normalization). */
|
|
1262
1262
|
href: string;
|
|
@@ -1276,10 +1276,8 @@ export function extractMarkdownLinks(text: string): MarkdownLink[] {
|
|
|
1276
1276
|
if (token.type === "link") {
|
|
1277
1277
|
const link = token as Tokens.Link;
|
|
1278
1278
|
if (typeof link.href === "string" && link.href.length > 0) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
href: link.href,
|
|
1282
|
-
});
|
|
1279
|
+
const label = plainInlineTokens(link.tokens).replace(/\s+/g, " ").trim();
|
|
1280
|
+
links.push({ text: label || link.href, href: link.href });
|
|
1283
1281
|
}
|
|
1284
1282
|
continue;
|
|
1285
1283
|
}
|
|
@@ -1497,6 +1495,9 @@ function plainInlineTokens(tokens: Token[]): string {
|
|
|
1497
1495
|
case "codespan":
|
|
1498
1496
|
result += token.text;
|
|
1499
1497
|
break;
|
|
1498
|
+
case "br":
|
|
1499
|
+
result += "\n";
|
|
1500
|
+
break;
|
|
1500
1501
|
default:
|
|
1501
1502
|
if ("text" in token && typeof token.text === "string") result += token.text;
|
|
1502
1503
|
break;
|
package/src/tui.ts
CHANGED
|
@@ -88,6 +88,18 @@ const PAINT_END_NO_SYNC = ENABLE_AUTOWRAP;
|
|
|
88
88
|
const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
89
89
|
const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `PI_TUI_RESIZE_IN_PLACE=1|true` forces in-place resize (no alt-buffer borrow).
|
|
93
|
+
* `0|false` forces the alt-buffer path even on Warp. Unset defers to Warp detection:
|
|
94
|
+
* Warp re-reports its size on CSI ?1049h / CSI ?1049l, which the resize alt-borrow
|
|
95
|
+
* turns into a flicker loop.
|
|
96
|
+
*/
|
|
97
|
+
function resizeInPlaceOverride(): boolean | null {
|
|
98
|
+
const override = Bun.env.PI_TUI_RESIZE_IN_PLACE;
|
|
99
|
+
if (override === "1" || override === "true") return true;
|
|
100
|
+
if (override === "0" || override === "false") return false;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
91
103
|
type InputListenerResult = { consume?: boolean; data?: string } | undefined;
|
|
92
104
|
type InputListener = (data: string) => InputListenerResult;
|
|
93
105
|
type StartListener = () => void;
|
|
@@ -822,6 +834,18 @@ export class TUI extends Container {
|
|
|
822
834
|
#resizeAltActive = false;
|
|
823
835
|
#resizeSettleTimer: RenderTimer | undefined;
|
|
824
836
|
#suppressResizeUntil = 0;
|
|
837
|
+
// Baseline geometry at the last alt-buffer toggle, plus whether its echo is
|
|
838
|
+
// still pending. A Warp-only echo is a height-only ±1 SIGWINCH against this
|
|
839
|
+
// baseline while the CPR probe is in flight. The expectation is single-shot:
|
|
840
|
+
// the first SIGWINCH after the toggle consumes it, so a real one-row resize
|
|
841
|
+
// back to the baseline can never be mistaken for the echo.
|
|
842
|
+
#altToggleColumns = 0;
|
|
843
|
+
#altToggleRows = 0;
|
|
844
|
+
#altToggleEchoPending = false;
|
|
845
|
+
// True while an in-place resize transaction (Warp) is inside its settle
|
|
846
|
+
// window: the normal-buffer anchor is stale until the settled CPR probe
|
|
847
|
+
// resolves, so ordinary paints are dropped until then.
|
|
848
|
+
#resizeInPlaceActive = false;
|
|
825
849
|
#resizeScrollbackMode: ResizeScrollbackMode = TUI.#initialResizeScrollbackMode();
|
|
826
850
|
#resizeReplaySize: string | undefined;
|
|
827
851
|
// Holds an alternate-screen exit until its replacement full paint can emit it
|
|
@@ -1124,15 +1148,52 @@ export class TUI extends Container {
|
|
|
1124
1148
|
data => this.#handleInput(data),
|
|
1125
1149
|
() => {
|
|
1126
1150
|
if (this.#resizeProbe) {
|
|
1127
|
-
//
|
|
1151
|
+
// Warp echoes a height-only ±1 SIGWINCH on CSI ?1049l. The echo
|
|
1152
|
+
// must not restart the alt borrow (that is the flicker loop),
|
|
1153
|
+
// but the terminal really did adopt the echoed size, so a CPR
|
|
1154
|
+
// reply already in flight may predate it: retire the probe and
|
|
1155
|
+
// reissue it at the new geometry. DSR-only, no toggle, so this
|
|
1156
|
+
// terminates. A real geometry change restarts the transaction below.
|
|
1157
|
+
if (this.#isWarpAltToggleEcho()) {
|
|
1158
|
+
this.#cancelResizeProbe();
|
|
1159
|
+
this.#trackResizeBurst();
|
|
1160
|
+
this.#beginResizeAnchorProbe();
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1128
1163
|
this.#cancelResizeProbe();
|
|
1129
|
-
this.#
|
|
1164
|
+
if (this.#resizeRepaintsInPlace()) this.#beginResizeInPlacePaint();
|
|
1165
|
+
else this.#beginResizeAltPaint(true);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (this.#altActive) {
|
|
1169
|
+
// A fullscreen overlay owns the alt buffer: repaint the modal at
|
|
1170
|
+
// the new size. Never snapshot the normal window or probe its
|
|
1171
|
+
// anchor against the alternate grid — not even for a toggle echo.
|
|
1172
|
+
this.requestRender(true);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (!this.#resizeAltActive && this.#isWarpAltToggleEcho()) {
|
|
1176
|
+
// Delayed echo that lost the signal-vs-pty race with its own
|
|
1177
|
+
// probe's CPR reply: the probe already resolved, so re-probe at
|
|
1178
|
+
// the echoed size instead of painting on a stale anchor or
|
|
1179
|
+
// suppressing into a forced replay. DSR-only, no borrow.
|
|
1180
|
+
// While the resize borrow is active the echo is swallowed
|
|
1181
|
+
// without probing: a CPR issued now would snapshot the
|
|
1182
|
+
// alternate grid and anchor the normal viewport to its row.
|
|
1183
|
+
this.#resizeProbeWindow = this.#providerWindow;
|
|
1184
|
+
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
1185
|
+
this.#trackResizeBurst();
|
|
1186
|
+
this.#beginResizeAnchorProbe();
|
|
1130
1187
|
return;
|
|
1131
1188
|
}
|
|
1132
1189
|
if (this.#renderScheduler.now() < this.#suppressResizeUntil) {
|
|
1133
1190
|
this.requestRender(true);
|
|
1134
1191
|
return;
|
|
1135
1192
|
}
|
|
1193
|
+
if (this.#resizeRepaintsInPlace()) {
|
|
1194
|
+
this.#beginResizeInPlacePaint();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1136
1197
|
this.#beginResizeAltPaint();
|
|
1137
1198
|
},
|
|
1138
1199
|
() => this.stop(),
|
|
@@ -1156,6 +1217,129 @@ export class TUI extends Container {
|
|
|
1156
1217
|
}
|
|
1157
1218
|
this.requestRender(true, { clearScrollback: options?.clearScrollback === true });
|
|
1158
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Whether a resize repaints the visible window in place — no alternate-screen
|
|
1222
|
+
* borrow. Warp-only: Warp re-reports its size on alt-buffer toggles, so borrowing
|
|
1223
|
+
* there self-sustains. Every other terminal keeps the alt-borrow path. Inside a
|
|
1224
|
+
* multiplexer the mux owns the grid and consumes the toggles itself, so an
|
|
1225
|
+
* inherited Warp marker must not divert the mux-tuned borrow path.
|
|
1226
|
+
*/
|
|
1227
|
+
#resizeRepaintsInPlace(): boolean {
|
|
1228
|
+
const override = resizeInPlaceOverride();
|
|
1229
|
+
if (override !== null) return override;
|
|
1230
|
+
if (isInsideTerminalMultiplexer()) return false;
|
|
1231
|
+
return Bun.env.TERM_PROGRAM?.toLowerCase() === "warpterminal";
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
#noteAltBufferToggle(): void {
|
|
1235
|
+
this.#altToggleColumns = this.terminal.columns;
|
|
1236
|
+
this.#altToggleRows = this.terminal.rows;
|
|
1237
|
+
this.#altToggleEchoPending = true;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Warp-only echo: height-only ±1 SIGWINCH against the pending alt-toggle
|
|
1242
|
+
* baseline. Single-shot: the first SIGWINCH after the toggle consumes the
|
|
1243
|
+
* expectation either way, so at most one signal is ever swallowed per toggle.
|
|
1244
|
+
* Never inside a multiplexer, which consumes the toggles itself.
|
|
1245
|
+
*/
|
|
1246
|
+
#isWarpAltToggleEcho(): boolean {
|
|
1247
|
+
if (!this.#altToggleEchoPending) return false;
|
|
1248
|
+
this.#altToggleEchoPending = false;
|
|
1249
|
+
// Inside a multiplexer the mux consumes alt toggles itself, so no echo is
|
|
1250
|
+
// possible: every ±1 resize is real and must restart the transaction.
|
|
1251
|
+
if (isInsideTerminalMultiplexer()) return false;
|
|
1252
|
+
if (Bun.env.TERM_PROGRAM?.toLowerCase() !== "warpterminal") return false;
|
|
1253
|
+
return (
|
|
1254
|
+
this.terminal.columns === this.#altToggleColumns && Math.abs(this.terminal.rows - this.#altToggleRows) <= 1
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Fold one SIGWINCH step into the coalesced resize-burst accounting shared by
|
|
1260
|
+
* both resize paths: any grow step poisons the multiplexer clip model, the
|
|
1261
|
+
* accumulated pull bounds CPR-less grow anchors, and the epoch retires the
|
|
1262
|
+
* in-flight CPR tag so a rewrap-invalidated reply cannot anchor a new geometry.
|
|
1263
|
+
*/
|
|
1264
|
+
#trackResizeBurst(): void {
|
|
1265
|
+
const burstLastHeight = this.#resizeBurstLastHeight ?? this.#previousHeight;
|
|
1266
|
+
if (this.terminal.rows > burstLastHeight) this.#resizeBurstGrew = true;
|
|
1267
|
+
this.#resizeBurstLastHeight = this.terminal.rows;
|
|
1268
|
+
this.#resizeBurstPull += Math.max(0, this.terminal.rows - burstLastHeight);
|
|
1269
|
+
this.#geometryEpoch++;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Coalesced in-place resize transaction for Warp-class terminals: never
|
|
1274
|
+
* borrows the alt buffer. Drag SIGWINCHes only re-arm the settle window, so a
|
|
1275
|
+
* drag emits no paints and no scrollback replay; once quiet, the transaction
|
|
1276
|
+
* snapshots the live window and runs the CPR anchor probe, and the single
|
|
1277
|
+
* settled repaint lands on the recovered anchor with no ED3 rewrap.
|
|
1278
|
+
*/
|
|
1279
|
+
#beginResizeInPlacePaint(): void {
|
|
1280
|
+
if (this.#altActive) {
|
|
1281
|
+
this.requestRender(true);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.#trackResizeBurst();
|
|
1285
|
+
this.#resizeInPlaceActive = true;
|
|
1286
|
+
this.#resizeSettleTimer?.cancel();
|
|
1287
|
+
this.#forgetHardwareCursorState();
|
|
1288
|
+
this.#recordHardwareCursorHidden();
|
|
1289
|
+
if (this.#eraseLiveViewportForResize()) {
|
|
1290
|
+
// The erase parked the hardware cursor on the viewport's top row;
|
|
1291
|
+
// snapshot the parked offset so the settled probe anchors there.
|
|
1292
|
+
this.#parkedViewportOffset = 0;
|
|
1293
|
+
}
|
|
1294
|
+
this.#resizeSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1295
|
+
this.#resizeSettleTimer = undefined;
|
|
1296
|
+
if (this.#stopped) return;
|
|
1297
|
+
this.#resizeProbeWindow = this.#providerWindow;
|
|
1298
|
+
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
1299
|
+
this.#beginResizeAnchorProbe();
|
|
1300
|
+
}, TUI.#RESIZE_VIEWPORT_SETTLE_MS);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Blank the mutable live viewport on the normal screen before a resize
|
|
1305
|
+
* transaction waits out the drag. The terminal keeps reflowing the normal
|
|
1306
|
+
* buffer during the drag, and a height shrink pushes its top rows into
|
|
1307
|
+
* scrollback; with the live region blanked, only committed history rows
|
|
1308
|
+
* (correct to push) or blanks can leave the screen — never live placeholder
|
|
1309
|
+
* rows such as compact tool dots, whose real blocks must enter scrollback
|
|
1310
|
+
* through the ordered history path. Addressing depends on the resize
|
|
1311
|
+
* direction. Terminals keep the parked cursor attached to its logical line
|
|
1312
|
+
* through width rewrap and height-grow scrollback pull-down, so
|
|
1313
|
+
* cursor-relative movement lands on the viewport's top row. On height shrink
|
|
1314
|
+
* kitty clamps the cursor instead of moving it with pushed rows, so
|
|
1315
|
+
* cursor-relative addressing would start rows late; fall back to the same
|
|
1316
|
+
* bottom-preserving bound as resize-anchor recovery.
|
|
1317
|
+
*
|
|
1318
|
+
* Both erase paths leave the cursor on the viewport's top row. Returns true
|
|
1319
|
+
* when it erased (multiplexers skip it: an immediate erase races the pane
|
|
1320
|
+
* re-layout and blanks pulled-back committed rows).
|
|
1321
|
+
*/
|
|
1322
|
+
#eraseLiveViewportForResize(): boolean {
|
|
1323
|
+
if (!this.#hasEverRendered || this.#providerWindow.length === 0 || isInsideTerminalMultiplexer()) {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
if (this.terminal.rows < this.#previousHeight) {
|
|
1327
|
+
const staleRows = this.#reflowedRowCount(
|
|
1328
|
+
this.#providerWindow,
|
|
1329
|
+
0,
|
|
1330
|
+
this.#providerWindow.length,
|
|
1331
|
+
this.terminal.columns,
|
|
1332
|
+
);
|
|
1333
|
+
const top = Math.max(0, Math.min(this.#providerViewportTop, this.terminal.rows - staleRows));
|
|
1334
|
+
this.terminal.write(`\x1b[?25l${this.#eraseBelowRow(top, this.terminal.rows)}`);
|
|
1335
|
+
} else {
|
|
1336
|
+
const up = this.#reflowedRowCount(this.#providerWindow, 0, this.#parkedViewportOffset, this.terminal.columns);
|
|
1337
|
+
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
1338
|
+
this.terminal.write(`\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`);
|
|
1339
|
+
}
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1159
1343
|
/**
|
|
1160
1344
|
* Borrow the alternate buffer for stable, history-free resize repainting.
|
|
1161
1345
|
* `restartingProbe` marks a transaction restarted by a SIGWINCH that
|
|
@@ -1168,62 +1352,26 @@ export class TUI extends Container {
|
|
|
1168
1352
|
this.requestRender(true);
|
|
1169
1353
|
return;
|
|
1170
1354
|
}
|
|
1171
|
-
|
|
1172
|
-
if (this.terminal.rows > burstLastHeight) this.#resizeBurstGrew = true;
|
|
1173
|
-
this.#resizeBurstLastHeight = this.terminal.rows;
|
|
1174
|
-
this.#resizeBurstPull += Math.max(0, this.terminal.rows - burstLastHeight);
|
|
1175
|
-
this.#geometryEpoch++;
|
|
1355
|
+
this.#trackResizeBurst();
|
|
1176
1356
|
if (!this.#resizeAltActive) {
|
|
1177
1357
|
this.#resizeAltActive = true;
|
|
1178
1358
|
setAltScreenActive(true);
|
|
1179
1359
|
this.#altPreviousLines = [];
|
|
1180
1360
|
this.#forgetHardwareCursorState();
|
|
1181
1361
|
this.#recordHardwareCursorHidden();
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
// the
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
// direction. Terminals keep the parked cursor attached to its logical
|
|
1190
|
-
// line through width rewrap and height-grow scrollback pull-down, so
|
|
1191
|
-
// cursor-relative movement lands on the viewport's top row. On height
|
|
1192
|
-
// shrink kitty clamps the cursor instead of moving it with pushed rows,
|
|
1193
|
-
// so cursor-relative addressing would start rows late; fall back to the
|
|
1194
|
-
// same bottom-preserving bound as resize-anchor recovery. The pre-erase
|
|
1195
|
-
// window is stashed for the settled CPR probe: its reflowed row count
|
|
1196
|
-
// bounds the anchor to `height - staleRows`, so a mis-parked cursor (a
|
|
1197
|
-
// single-step tmux zoom re-lays the pane before SIGWINCH delivery,
|
|
1198
|
-
// moving the park target under us) cannot anchor the settled repaint
|
|
1199
|
-
// over pulled-back history rows or scroll-push the frame into
|
|
1200
|
-
// scrollback again.
|
|
1201
|
-
let erase = "";
|
|
1362
|
+
// Blank the live region up front so a reflow-driven scroll can only push
|
|
1363
|
+
// committed rows into scrollback. The pre-erase window is stashed for
|
|
1364
|
+
// the settled CPR probe: its reflowed row count bounds the anchor to
|
|
1365
|
+
// `height - staleRows`, so a mis-parked cursor (a single-step tmux zoom
|
|
1366
|
+
// re-lays the pane before SIGWINCH delivery, moving the park target
|
|
1367
|
+
// under us) cannot anchor the settled repaint over pulled-back history
|
|
1368
|
+
// rows or scroll-push the frame into scrollback again.
|
|
1202
1369
|
if (!restartingProbe) {
|
|
1203
1370
|
this.#resizeProbeWindow = this.#providerWindow;
|
|
1204
1371
|
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
1205
1372
|
}
|
|
1206
|
-
if (this.#
|
|
1207
|
-
|
|
1208
|
-
const staleRows = this.#reflowedRowCount(
|
|
1209
|
-
this.#providerWindow,
|
|
1210
|
-
0,
|
|
1211
|
-
this.#providerWindow.length,
|
|
1212
|
-
this.terminal.columns,
|
|
1213
|
-
);
|
|
1214
|
-
const top = Math.max(0, Math.min(this.#providerViewportTop, this.terminal.rows - staleRows));
|
|
1215
|
-
erase = `\x1b[?25l${this.#eraseBelowRow(top, this.terminal.rows)}`;
|
|
1216
|
-
} else {
|
|
1217
|
-
const up = this.#reflowedRowCount(
|
|
1218
|
-
this.#providerWindow,
|
|
1219
|
-
0,
|
|
1220
|
-
this.#parkedViewportOffset,
|
|
1221
|
-
this.terminal.columns,
|
|
1222
|
-
);
|
|
1223
|
-
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
1224
|
-
erase = `\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`;
|
|
1225
|
-
}
|
|
1226
|
-
// Both erase paths leave the cursor on the viewport's top row, so the
|
|
1373
|
+
if (this.#eraseLiveViewportForResize()) {
|
|
1374
|
+
// The erase parked the cursor on the viewport's top row, so the
|
|
1227
1375
|
// parked offset no longer applies; carrying a stale nonzero offset
|
|
1228
1376
|
// into the probe would anchor the settled repaint above the real
|
|
1229
1377
|
// viewport top and overwrite visible committed rows.
|
|
@@ -1242,7 +1390,8 @@ export class TUI extends Container {
|
|
|
1242
1390
|
this.#providerWindow = [];
|
|
1243
1391
|
this.#parkedViewportOffset = 0;
|
|
1244
1392
|
}
|
|
1245
|
-
this
|
|
1393
|
+
this.#noteAltBufferToggle();
|
|
1394
|
+
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}`);
|
|
1246
1395
|
}
|
|
1247
1396
|
this.#resizeSettleTimer?.cancel();
|
|
1248
1397
|
this.#resizeSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
@@ -1250,6 +1399,7 @@ export class TUI extends Container {
|
|
|
1250
1399
|
if (this.#stopped || !this.#resizeAltActive) return;
|
|
1251
1400
|
this.#resizeAltActive = false;
|
|
1252
1401
|
this.#suppressResizeUntil = this.#renderScheduler.now() + 100;
|
|
1402
|
+
this.#noteAltBufferToggle();
|
|
1253
1403
|
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1254
1404
|
setAltScreenActive(false);
|
|
1255
1405
|
this.#altPreviousLines = [];
|
|
@@ -1258,8 +1408,8 @@ export class TUI extends Container {
|
|
|
1258
1408
|
this.requestRender(true);
|
|
1259
1409
|
}
|
|
1260
1410
|
/**
|
|
1261
|
-
* Recover the reflowed viewport anchor after the resize
|
|
1262
|
-
*
|
|
1411
|
+
* Recover the reflowed viewport anchor after the resize settle window ends.
|
|
1412
|
+
* The terminal reflowed the restored normal buffer during the drag, so
|
|
1263
1413
|
* `#providerViewportTop` is in stale grid coordinates; a DSR (CSI 6n) round
|
|
1264
1414
|
* trip against the parked cursor reports where the viewport's logical line
|
|
1265
1415
|
* landed. The settled repaint waits for the reply (or a short timeout).
|
|
@@ -1345,6 +1495,7 @@ export class TUI extends Container {
|
|
|
1345
1495
|
if (!probe) return;
|
|
1346
1496
|
probe.timer.cancel();
|
|
1347
1497
|
this.#resizeProbe = undefined;
|
|
1498
|
+
this.#resizeInPlaceActive = false;
|
|
1348
1499
|
// Column tags stay live across resolves: their replies are
|
|
1349
1500
|
// self-identifying and discarded by tag whenever they arrive.
|
|
1350
1501
|
const width = this.terminal.columns;
|
|
@@ -1640,6 +1791,16 @@ export class TUI extends Container {
|
|
|
1640
1791
|
this.#debugServer = undefined;
|
|
1641
1792
|
this.#resizeSettleTimer?.cancel();
|
|
1642
1793
|
this.#resizeSettleTimer = undefined;
|
|
1794
|
+
if (this.#resizeInPlaceActive && this.terminal.rows > 0) {
|
|
1795
|
+
// The hardware cursor sits wherever the drag left it, but tracking still
|
|
1796
|
+
// describes the pre-resize row (no alt-buffer restore replays it back).
|
|
1797
|
+
// The shell handoff below moves relatively from the tracked row, so park
|
|
1798
|
+
// absolutely on the bottom row and record it first.
|
|
1799
|
+
this.terminal.write(`\x1b[${this.terminal.rows};1H`);
|
|
1800
|
+
this.#hardwareCursorRow = this.terminal.rows - 1;
|
|
1801
|
+
}
|
|
1802
|
+
this.#resizeInPlaceActive = false;
|
|
1803
|
+
this.#altToggleEchoPending = false;
|
|
1643
1804
|
this.#cancelResizeProbe();
|
|
1644
1805
|
if (this.#resizeAltActive) {
|
|
1645
1806
|
this.#resizeAltActive = false;
|
|
@@ -2319,7 +2480,11 @@ export class TUI extends Container {
|
|
|
2319
2480
|
!this.#hasEverRendered ||
|
|
2320
2481
|
(this.#previousWidth === width && this.#previousHeight === height) ||
|
|
2321
2482
|
this.#resizeReplaySize === size ||
|
|
2322
|
-
this.#resizeScrollbackMode === "preserve"
|
|
2483
|
+
this.#resizeScrollbackMode === "preserve" ||
|
|
2484
|
+
// In-place resizes (Warp) repaint the settled viewport once the drag
|
|
2485
|
+
// goes quiet: no alt borrow, and no ED3 rewrap or history replay, so a
|
|
2486
|
+
// drag can neither loop on its own echo nor flash destructive repaints.
|
|
2487
|
+
this.#resizeRepaintsInPlace()
|
|
2323
2488
|
) {
|
|
2324
2489
|
return;
|
|
2325
2490
|
}
|
|
@@ -2545,6 +2710,7 @@ export class TUI extends Container {
|
|
|
2545
2710
|
...(target === null ? {} : { cursor: { x: target.col, y: target.row, visible: target.visible } }),
|
|
2546
2711
|
};
|
|
2547
2712
|
if (pendingAltExit) {
|
|
2713
|
+
this.#noteAltBufferToggle();
|
|
2548
2714
|
this.#pendingAltExit = "";
|
|
2549
2715
|
setAltScreenActive(false);
|
|
2550
2716
|
}
|
|
@@ -2585,6 +2751,14 @@ export class TUI extends Container {
|
|
|
2585
2751
|
// use the stale pre-resize anchor.
|
|
2586
2752
|
return;
|
|
2587
2753
|
}
|
|
2754
|
+
if (this.#resizeInPlaceActive && !this.#altActive) {
|
|
2755
|
+
// In-place resize settling (Warp): the normal-buffer anchor is stale until
|
|
2756
|
+
// the settled CPR probe resolves. Painting now would overwrite retained
|
|
2757
|
+
// history and record the new geometry over the pending recovery, so drop
|
|
2758
|
+
// the frame — the resolve repaints. Fullscreen overlay paints are
|
|
2759
|
+
// buffer-isolated and still allowed.
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2588
2762
|
|
|
2589
2763
|
// Fullscreen alt-screen short-circuit. While the topmost visible overlay
|
|
2590
2764
|
// requests it, borrow the terminal's alternate buffer and paint only the
|
|
@@ -2597,6 +2771,7 @@ export class TUI extends Container {
|
|
|
2597
2771
|
// modified-key reporting sequence on the freshly entered alternate
|
|
2598
2772
|
// screen, or Esc/modified keys revert to legacy encoding inside
|
|
2599
2773
|
// fullscreen overlays (Ghostty/kitty/iTerm2).
|
|
2774
|
+
this.#noteAltBufferToggle();
|
|
2600
2775
|
const mouseEnter = wantMouseTracking ? MOUSE_TRACKING_ON : "";
|
|
2601
2776
|
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}${mouseEnter}`);
|
|
2602
2777
|
setAltScreenActive(true);
|
|
@@ -2618,6 +2793,7 @@ export class TUI extends Container {
|
|
|
2618
2793
|
if (this.#clearScrollbackOnNextRender) {
|
|
2619
2794
|
this.#pendingAltExit = exitSequence;
|
|
2620
2795
|
} else {
|
|
2796
|
+
this.#noteAltBufferToggle();
|
|
2621
2797
|
this.terminal.write(exitSequence);
|
|
2622
2798
|
setAltScreenActive(false);
|
|
2623
2799
|
}
|