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:
roboomp
2026-07-10 16:21:41 +00:00
committed by can1357
parent d1c88dd89c
commit 79ce2de8a2
3 changed files with 57 additions and 1 deletions
+3
View File
@@ -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
+27 -1
View File
@@ -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 });