fix(tui): restored windows terminal appearance polling
Added the scoped OSC 11 polling fallback for native Windows Terminal when Mode 2031 appearance notifications are unsupported, preserving the no-poll behavior for other terminals. Fixes #5091
This commit is contained in:
@@ -58,6 +58,9 @@
|
||||
|
||||
- Improved row alignment and spacing for `align`, `gather`, and `array` environments
|
||||
- Updated matrix environments to render as baseline-aligned grids with stretched brackets
|
||||
### Fixed
|
||||
|
||||
- Fixed native Windows Terminal sessions missing mid-run light/dark theme changes when Mode 2031 appearance notifications are unavailable by polling OSC 11 only on that host path ([#5091](https://github.com/can1357/oh-my-pi/issues/5091)).
|
||||
|
||||
## [16.4.0] - 2026-07-10
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { type HangulCompatibilityJamoWidth, setHangulCompatibilityJamoWidth } fr
|
||||
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
||||
const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
||||
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
||||
const WINDOWS_TERMINAL_OSC11_POLL_MS = 30_000;
|
||||
// Hangul Compatibility Jamo (U+3131..=U+318E) render width is terminal-dependent:
|
||||
// Ghostty follows UAX#11 (2 cells); Terminal.app and iTerm2 render narrow (1),
|
||||
// matching the macOS platform default. Override only for terminals known to
|
||||
@@ -48,6 +49,11 @@ function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env):
|
||||
return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor";
|
||||
}
|
||||
|
||||
function shouldPollWindowsTerminalAppearance(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
||||
if (process.platform !== "win32") return false;
|
||||
if (!env.WT_SESSION) return false;
|
||||
return !env.TERM_PROGRAM || env.TERM_PROGRAM.toLowerCase() === "windows_terminal";
|
||||
}
|
||||
/**
|
||||
* Maximum encoded UTF-8 bytes per `process.stdout.write` call on Windows.
|
||||
*
|
||||
@@ -499,6 +505,7 @@ export class ProcessTerminal implements Terminal {
|
||||
#reportedColumns?: number;
|
||||
#reportedRows?: number;
|
||||
#mode2031DebounceTimer?: Timer;
|
||||
#windowsTerminalAppearancePollTimer?: Timer;
|
||||
#progressTimer?: Timer;
|
||||
|
||||
get kittyProtocolActive(): boolean {
|
||||
@@ -627,7 +634,8 @@ export class ProcessTerminal implements Terminal {
|
||||
// WezTerm) detect the appearance once at startup and pick up later OS
|
||||
// theme changes on next launch. Earlier builds polled OSC 11 every 30 s
|
||||
// here for those terminals, but each poll's OSC 11/DA1 write wiped the
|
||||
// user's active text selection on several of them (#3297).
|
||||
// user's active text selection on several of them (#3297). Native Windows
|
||||
// Terminal gets a scoped fallback after DECRQM confirms 2031 is unsupported.
|
||||
|
||||
// Probe DEC private-mode support via DECRQM. 2026 (synchronized output)
|
||||
// gates the renderer's begin/end markers; 2048 (in-band resize) is enabled
|
||||
@@ -1167,8 +1175,25 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
}
|
||||
if (mode === 2048 && supported) this.#enableInBandResize();
|
||||
if (mode === 2031) this.#syncWindowsTerminalAppearancePolling(supported);
|
||||
}
|
||||
|
||||
#syncWindowsTerminalAppearancePolling(mode2031Supported: boolean): void {
|
||||
if (mode2031Supported || !shouldPollWindowsTerminalAppearance() || this.#dead) {
|
||||
this.#clearWindowsTerminalAppearancePoll();
|
||||
return;
|
||||
}
|
||||
if (this.#windowsTerminalAppearancePollTimer) return;
|
||||
this.#windowsTerminalAppearancePollTimer = setInterval(() => {
|
||||
this.#queryBackgroundColor();
|
||||
}, WINDOWS_TERMINAL_OSC11_POLL_MS);
|
||||
}
|
||||
|
||||
#clearWindowsTerminalAppearancePoll(): void {
|
||||
if (!this.#windowsTerminalAppearancePollTimer) return;
|
||||
clearInterval(this.#windowsTerminalAppearancePollTimer);
|
||||
this.#windowsTerminalAppearancePollTimer = undefined;
|
||||
}
|
||||
#disableXtermScrollToBottomMode(mode: number): void {
|
||||
if (this.#xtermScrollToBottomRestoreModes.has(mode) || this.#dead) return;
|
||||
this.#xtermScrollToBottomRestoreModes.add(mode);
|
||||
@@ -1326,6 +1351,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
this.#appearanceCallbacks = [];
|
||||
this.#osc11Pending = false;
|
||||
this.#clearWindowsTerminalAppearancePoll();
|
||||
this.#osc11QueryQueued = false;
|
||||
this.#osc11ResponseBuffer = "";
|
||||
this.#osc99PendingId = undefined;
|
||||
|
||||
@@ -17,6 +17,8 @@ const stdoutRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "ro
|
||||
const stdinSetRawModeDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "setRawMode");
|
||||
const originalWslDistroName = Bun.env.WSL_DISTRO_NAME;
|
||||
const originalWslInterop = Bun.env.WSL_INTEROP;
|
||||
const originalWtSession = Bun.env.WT_SESSION;
|
||||
const originalTermProgram = Bun.env.TERM_PROGRAM;
|
||||
|
||||
// These suites drive the real ProcessTerminal start()/probe pipeline, so they
|
||||
// opt out of the test-default headless suppression and restore it per case.
|
||||
@@ -56,6 +58,8 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
restoreProperty(process, "platform", processPlatformDescriptor);
|
||||
restoreEnv("WSL_INTEROP", originalWslInterop);
|
||||
restoreEnv("WSL_DISTRO_NAME", originalWslDistroName);
|
||||
restoreEnv("WT_SESSION", originalWtSession);
|
||||
restoreEnv("TERM_PROGRAM", originalTermProgram);
|
||||
});
|
||||
|
||||
function setupTerminal() {
|
||||
@@ -232,6 +236,29 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("periodically re-queries OSC 11 under native Windows Terminal when Mode 2031 is unavailable (#5091)", () => {
|
||||
vi.useFakeTimers();
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
Bun.env.WT_SESSION = "test-wt-session";
|
||||
Bun.env.TERM_PROGRAM = "Windows_Terminal";
|
||||
const { terminal, queryCount } = setupTerminal();
|
||||
|
||||
process.stdin.emit("data", "\x1b]11;rgb:0000/0000/0000\x07");
|
||||
process.stdin.emit("data", "\x1b[?2031;0$y");
|
||||
// Drain startup sentinels in send order: keyboard, OSC 11, DEC 2026,
|
||||
// DEC 2048, DEC 2031, and xterm ?1010/?1011.
|
||||
for (let i = 0; i < 7; i++) {
|
||||
process.stdin.emit("data", "\x1b[?1;2c");
|
||||
}
|
||||
const afterStartup = queryCount();
|
||||
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
expect(queryCount()).toBe(afterStartup + 1);
|
||||
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("does not periodically re-query OSC 11 under WSL either (#3297)", () => {
|
||||
vi.useFakeTimers();
|
||||
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
||||
|
||||
Reference in New Issue
Block a user