@oh-my-pi/pi-tui 17.1.0 → 17.1.2
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 +12 -0
- package/dist/types/terminal-capabilities.d.ts +14 -1
- package/dist/types/terminal.d.ts +0 -2
- package/package.json +3 -3
- package/src/terminal-capabilities.ts +20 -3
- package/src/terminal.ts +2 -21
- package/src/tui.ts +52 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.1.2] - 2026-07-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the terminal flickering when leaving a fullscreen overlay (e.g. `/settings`, the models page) or resizing on terminals that re-report their size whenever the alternate screen buffer toggles: the alt-toggle size echo no longer arms a destructive full-screen (ED3) rebuild, and the in-place repaint path is now auto-detected for any such terminal instead of only Warp ([#6511](https://github.com/can1357/oh-my-pi/issues/6511)).
|
|
10
|
+
|
|
11
|
+
## [17.1.1] - 2026-07-24
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed WarpTerminal wrapping streamed Compatibility Jamo at the platform-default width instead of its rendered one-cell width ([#6461](https://github.com/can1357/oh-my-pi/issues/6461)).
|
|
16
|
+
|
|
5
17
|
## [17.1.0] - 2026-07-24
|
|
6
18
|
|
|
7
19
|
### Added
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HangulCompatibilityJamoWidth } from "./utils.js";
|
|
1
2
|
export { isInsideTmux, wrapTmuxPassthrough } from "./tmux.js";
|
|
2
3
|
export declare enum ImageProtocol {
|
|
3
4
|
Kitty = "\u001B_G",
|
|
@@ -21,9 +22,21 @@ export declare class TerminalInfo {
|
|
|
21
22
|
readonly supportsScreenToScrollback: boolean;
|
|
22
23
|
/** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
|
|
23
24
|
readonly textSizing: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
|
|
27
|
+
* UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
|
|
28
|
+
* (macOS narrow, otherwise UAX#11).
|
|
29
|
+
*/
|
|
30
|
+
readonly hangulJamoWidth: HangulCompatibilityJamoWidth;
|
|
24
31
|
constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, deccara?: boolean, supportsScreenToScrollback?: boolean,
|
|
25
32
|
/** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
|
|
26
|
-
textSizing?: boolean
|
|
33
|
+
textSizing?: boolean,
|
|
34
|
+
/**
|
|
35
|
+
* Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
|
|
36
|
+
* UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
|
|
37
|
+
* (macOS narrow, otherwise UAX#11).
|
|
38
|
+
*/
|
|
39
|
+
hangulJamoWidth?: HangulCompatibilityJamoWidth);
|
|
27
40
|
/**
|
|
28
41
|
* Mutable clone for the {@link TERMINAL} singleton: copies every field and
|
|
29
42
|
* keeps the prototype methods, so the builder and runtime setters flip
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { type HangulCompatibilityJamoWidth } from "./utils.js";
|
|
2
|
-
export declare function resolveHangulCompatibilityJamoWidthFromTerminalIdentity(env?: NodeJS.ProcessEnv): HangulCompatibilityJamoWidth;
|
|
3
1
|
/**
|
|
4
2
|
* Split `data` into chunks whose encoded UTF-8 byte length is no greater than
|
|
5
3
|
* `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "17.1.
|
|
4
|
+
"version": "17.1.2",
|
|
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": "17.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.1.
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.1.2",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.1.2",
|
|
42
42
|
"lru-cache": "11.5.2",
|
|
43
43
|
"marked": "^18.0.6"
|
|
44
44
|
},
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setKittyGraphics,
|
|
11
11
|
} from "./kitty-graphics";
|
|
12
12
|
import { isInsideTmux, wrapTmuxPassthrough, wrapTmuxPassthroughIfNeeded } from "./tmux";
|
|
13
|
+
import type { HangulCompatibilityJamoWidth } from "./utils";
|
|
13
14
|
|
|
14
15
|
export { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
|
|
15
16
|
|
|
@@ -105,6 +106,12 @@ export class TerminalInfo {
|
|
|
105
106
|
readonly supportsScreenToScrollback: boolean = false,
|
|
106
107
|
/** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
|
|
107
108
|
public readonly textSizing: boolean = false,
|
|
109
|
+
/**
|
|
110
|
+
* Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
|
|
111
|
+
* UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
|
|
112
|
+
* (macOS narrow, otherwise UAX#11).
|
|
113
|
+
*/
|
|
114
|
+
public readonly hangulJamoWidth: HangulCompatibilityJamoWidth = "platform",
|
|
108
115
|
) {}
|
|
109
116
|
|
|
110
117
|
/**
|
|
@@ -447,7 +454,17 @@ export function resolveWarpImageProtocol(
|
|
|
447
454
|
}
|
|
448
455
|
|
|
449
456
|
function getWarpTerminalInfo(platform: NodeJS.Platform, env: NodeJS.ProcessEnv = Bun.env): TerminalInfo {
|
|
450
|
-
return new TerminalInfo(
|
|
457
|
+
return new TerminalInfo(
|
|
458
|
+
"warp",
|
|
459
|
+
resolveWarpImageProtocol(platform, env),
|
|
460
|
+
true,
|
|
461
|
+
false,
|
|
462
|
+
NotifyProtocol.Osc9,
|
|
463
|
+
false,
|
|
464
|
+
false,
|
|
465
|
+
false,
|
|
466
|
+
1,
|
|
467
|
+
);
|
|
451
468
|
}
|
|
452
469
|
const KNOWN_TERMINALS = Object.freeze({
|
|
453
470
|
// Fallback terminals
|
|
@@ -455,7 +472,7 @@ const KNOWN_TERMINALS = Object.freeze({
|
|
|
455
472
|
trueColor: new TerminalInfo("trueColor", null, true, false, NotifyProtocol.Bell),
|
|
456
473
|
// Recognized terminals
|
|
457
474
|
kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99, true, true, true),
|
|
458
|
-
ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
|
|
475
|
+
ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9, false, false, false, 2),
|
|
459
476
|
wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
|
|
460
477
|
iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9),
|
|
461
478
|
vscode: new TerminalInfo("vscode", null, true, true, NotifyProtocol.Bell),
|
|
@@ -465,7 +482,7 @@ const KNOWN_TERMINALS = Object.freeze({
|
|
|
465
482
|
// detectKittyUnicodePlaceholdersSupport correctly excludes it). It does not
|
|
466
483
|
// honor OSC 8 yet (the escape renders as visible text), so hyperlinks stay off,
|
|
467
484
|
// but it does support OSC 9 notifications.
|
|
468
|
-
warp: new TerminalInfo("warp", ImageProtocol.Kitty, true, false, NotifyProtocol.Osc9),
|
|
485
|
+
warp: new TerminalInfo("warp", ImageProtocol.Kitty, true, false, NotifyProtocol.Osc9, false, false, false, 1),
|
|
469
486
|
});
|
|
470
487
|
|
|
471
488
|
/** Resolve terminal identity from environment markers used by common emulators. */
|
package/src/terminal.ts
CHANGED
|
@@ -19,31 +19,12 @@ import {
|
|
|
19
19
|
TERMINAL,
|
|
20
20
|
} from "./terminal-capabilities";
|
|
21
21
|
import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
|
|
22
|
-
import {
|
|
22
|
+
import { setHangulCompatibilityJamoWidth } from "./utils";
|
|
23
23
|
|
|
24
24
|
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
|
25
25
|
const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
|
26
26
|
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
|
27
27
|
const WINDOWS_TERMINAL_OSC11_POLL_MS = 30_000;
|
|
28
|
-
// Hangul Compatibility Jamo (U+3131..=U+318E) render width is terminal-dependent:
|
|
29
|
-
// Ghostty follows UAX#11 (2 cells); Terminal.app and iTerm2 render narrow (1),
|
|
30
|
-
// matching the macOS platform default. Override only for terminals known to
|
|
31
|
-
// disagree — the rest keep the platform default (macOS narrow, otherwise UAX#11),
|
|
32
|
-
// so this is a no-op everywhere except Ghostty. A runtime DSR/CPR probe that
|
|
33
|
-
// auto-detects the width on unknown terminals is tracked separately.
|
|
34
|
-
export function resolveHangulCompatibilityJamoWidthFromTerminalIdentity(
|
|
35
|
-
env: NodeJS.ProcessEnv = Bun.env,
|
|
36
|
-
): HangulCompatibilityJamoWidth {
|
|
37
|
-
if (
|
|
38
|
-
env.GHOSTTY_RESOURCES_DIR ||
|
|
39
|
-
env.TERM_PROGRAM?.toLowerCase() === "ghostty" ||
|
|
40
|
-
env.TERM?.toLowerCase().includes("ghostty")
|
|
41
|
-
) {
|
|
42
|
-
return 2;
|
|
43
|
-
}
|
|
44
|
-
return "platform";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
28
|
function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
48
29
|
if (!env.SSH_CONNECTION && !env.SSH_TTY && !env.SSH_CLIENT) return true;
|
|
49
30
|
return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor";
|
|
@@ -659,7 +640,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
659
640
|
// The query handler intercepts input temporarily, then installs the user's handler
|
|
660
641
|
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
661
642
|
this.#queryAndEnableKittyProtocol();
|
|
662
|
-
setHangulCompatibilityJamoWidth(
|
|
643
|
+
setHangulCompatibilityJamoWidth(TERMINAL.hangulJamoWidth);
|
|
663
644
|
|
|
664
645
|
// Query terminal background color via OSC 11 for dark/light detection.
|
|
665
646
|
// Uses DA1 (Primary Device Attributes) as a sentinel: terminals process
|
package/src/tui.ts
CHANGED
|
@@ -1091,6 +1091,13 @@ export class TUI extends Container {
|
|
|
1091
1091
|
// resize frames so width changes truncate the transient viewport instead of
|
|
1092
1092
|
// pushing wrapped fragments into native scrollback.
|
|
1093
1093
|
#resizeAltActive = false;
|
|
1094
|
+
// Latched once this terminal is observed re-reporting its size across an
|
|
1095
|
+
// alternate-screen toggle (a pure height change between alt-buffer enter and
|
|
1096
|
+
// exit). That is the Warp-class quirk {@link reportsSizeOnAltScreenToggle}
|
|
1097
|
+
// hardcodes: without it, leaving a fullscreen overlay flashes a destructive
|
|
1098
|
+
// ED3 full paint and the revert SIGWINCH flashes another (#6511). Once set,
|
|
1099
|
+
// {@link #resizeRepaintsInPlace} routes resizes through the in-place path.
|
|
1100
|
+
#altToggleResizesInPlace = false;
|
|
1094
1101
|
#stopped = false;
|
|
1095
1102
|
// Always-on event-loop lag probe. The high default threshold keeps it quiet;
|
|
1096
1103
|
// it only logs `ui.loop-blocked` (with the current loop phase) when a frame
|
|
@@ -1590,8 +1597,26 @@ export class TUI extends Container {
|
|
|
1590
1597
|
// window) into a single render once the pane is quiet —
|
|
1591
1598
|
// `#resizeEventPending` is set first so the eventual render still
|
|
1592
1599
|
// classifies as a resize.
|
|
1600
|
+
// A SIGWINCH while a fullscreen overlay covers the transcript is
|
|
1601
|
+
// either a genuine resize behind the overlay or the alt-toggle size
|
|
1602
|
+
// echo (a terminal re-reporting its size whenever the alternate
|
|
1603
|
+
// screen buffer toggles). The transcript is not visible either way,
|
|
1604
|
+
// so arming the drag/settle would only queue a destructive ED3
|
|
1605
|
+
// rebuild that fires as a flash when the overlay closes (#6511). A
|
|
1606
|
+
// pure height change is the alt-toggle-echo signature — latch the
|
|
1607
|
+
// in-place resize path — and just repaint the overlay at the new
|
|
1608
|
+
// size. #resizeEventPending carries to the overlay-exit render so it
|
|
1609
|
+
// still classifies as a resize.
|
|
1610
|
+
if (this.#altActive) {
|
|
1611
|
+
if (this.#altEnterWidth === this.terminal.columns && this.#altEnterHeight !== this.terminal.rows) {
|
|
1612
|
+
this.#altToggleResizesInPlace = true;
|
|
1613
|
+
}
|
|
1614
|
+
this.#resizeEventPending = true;
|
|
1615
|
+
this.requestRender();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1593
1618
|
this.#resizeEventPending = true;
|
|
1594
|
-
if (!resizeRepaintsInPlace()) {
|
|
1619
|
+
if (!this.#resizeRepaintsInPlace()) {
|
|
1595
1620
|
// Enter the viewport fast path and (re)arm the settle timer, then
|
|
1596
1621
|
// request the cheap viewport-only paint. The authoritative full
|
|
1597
1622
|
// replay fires from the settle timer once the drag goes quiet.
|
|
@@ -2815,9 +2840,15 @@ export class TUI extends Container {
|
|
|
2815
2840
|
this.#altPreviousLines = [];
|
|
2816
2841
|
// A resize while on the alt buffer reflowed the terminal's saved
|
|
2817
2842
|
// normal screen; it no longer matches our accounting, so force the
|
|
2818
|
-
// geometry rebuild path instead of a stale diff.
|
|
2843
|
+
// geometry rebuild path instead of a stale diff. A pure height change
|
|
2844
|
+
// across the alt-buffer boundary (width unchanged) is the signature of
|
|
2845
|
+
// a terminal that re-reports its size whenever the alternate screen
|
|
2846
|
+
// toggles — the Warp-class quirk. Latch the in-place resize path so
|
|
2847
|
+
// this exit and the revert SIGWINCH repaint without an ED3 scrollback
|
|
2848
|
+
// rewrap instead of flashing a destructive full paint (#6511).
|
|
2819
2849
|
if (width !== this.#altEnterWidth || height !== this.#altEnterHeight) {
|
|
2820
2850
|
this.#resizeEventPending = true;
|
|
2851
|
+
if (width === this.#altEnterWidth) this.#altToggleResizesInPlace = true;
|
|
2821
2852
|
}
|
|
2822
2853
|
} else if (wantMouseTracking !== this.#altMouseTrackingActive) {
|
|
2823
2854
|
this.terminal.write(wantMouseTracking ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
@@ -2870,7 +2901,7 @@ export class TUI extends Container {
|
|
|
2870
2901
|
// count too: both enter the geometry rebuild path below.
|
|
2871
2902
|
const replayFullHistory =
|
|
2872
2903
|
this.#hasEverRendered &&
|
|
2873
|
-
!resizeRepaintsInPlace() &&
|
|
2904
|
+
!this.#resizeRepaintsInPlace() &&
|
|
2874
2905
|
(this.#clearScrollbackOnNextRender ||
|
|
2875
2906
|
this.#resizeEventPending ||
|
|
2876
2907
|
(this.#previousWidth > 0 && this.#previousWidth !== width) ||
|
|
@@ -3018,7 +3049,7 @@ export class TUI extends Container {
|
|
|
3018
3049
|
// feedback loop), so committed history keeps its old wrap.
|
|
3019
3050
|
const firstPaint = !this.#hasEverRendered;
|
|
3020
3051
|
const replaceRequested = this.#clearScrollbackOnNextRender;
|
|
3021
|
-
const geometryRebuild = geometryChanged && !resizeRepaintsInPlace();
|
|
3052
|
+
const geometryRebuild = geometryChanged && !this.#resizeRepaintsInPlace();
|
|
3022
3053
|
// Committed history no longer matches the frame: a finalized block
|
|
3023
3054
|
// replaced its scrolled-off live render, or the frame collapsed into
|
|
3024
3055
|
// recorded rows. Native scrollback is a render cache, not a court
|
|
@@ -3175,7 +3206,8 @@ export class TUI extends Container {
|
|
|
3175
3206
|
windowTop,
|
|
3176
3207
|
prevWindowTop,
|
|
3177
3208
|
prevHardwareCursorRow,
|
|
3178
|
-
forceWindowRewrite:
|
|
3209
|
+
forceWindowRewrite:
|
|
3210
|
+
this.#forceViewportRepaintOnNextRender || (geometryChanged && this.#resizeRepaintsInPlace()),
|
|
3179
3211
|
repaintVirtualScrollInPlace: hasVisibleOverlay,
|
|
3180
3212
|
cursorTrackingLineCount,
|
|
3181
3213
|
});
|
|
@@ -3812,6 +3844,21 @@ export class TUI extends Container {
|
|
|
3812
3844
|
return `${enhancementExit}${ALT_SCREEN_EXIT}`;
|
|
3813
3845
|
}
|
|
3814
3846
|
|
|
3847
|
+
/**
|
|
3848
|
+
* Whether a resize repaints the visible window in place — no alternate-screen
|
|
3849
|
+
* borrow, no ED3 scrollback rewrap. Combines the static host detection
|
|
3850
|
+
* ({@link resizeRepaintsInPlace}) with the runtime {@link #altToggleResizesInPlace}
|
|
3851
|
+
* latch, so a terminal that re-reports its size on alt-screen toggles is
|
|
3852
|
+
* treated like Warp once observed, breaking the overlay-exit ED3 flash loop.
|
|
3853
|
+
* An explicit `PI_TUI_RESIZE_IN_PLACE=0|false` suppresses the runtime latch;
|
|
3854
|
+
* multiplexer handling remains authoritative through the static predicate.
|
|
3855
|
+
*/
|
|
3856
|
+
#resizeRepaintsInPlace(): boolean {
|
|
3857
|
+
const override = Bun.env.PI_TUI_RESIZE_IN_PLACE;
|
|
3858
|
+
const allowAutoDetection = override !== "0" && override !== "false";
|
|
3859
|
+
return resizeRepaintsInPlace() || (allowAutoDetection && this.#altToggleResizesInPlace);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3815
3862
|
/**
|
|
3816
3863
|
* Emit a throwaway viewport repaint for the resize fast path as a per-row
|
|
3817
3864
|
* overwrite. A width change can make the terminal's normal buffer reflow
|