From 79ce2de8a28bb943caa2d3591676e9eed783453b Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 10 Jul 2026 16:21:41 +0000 Subject: [PATCH] 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 --- packages/tui/CHANGELOG.md | 3 ++ packages/tui/src/terminal.ts | 28 ++++++++++++++++++- packages/tui/test/terminal-appearance.test.ts | 27 ++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1fa463a9e..357326927 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -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 diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index c9125f9e9..4c622c620 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -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; diff --git a/packages/tui/test/terminal-appearance.test.ts b/packages/tui/test/terminal-appearance.test.ts index ea1067c1b..169fec8e6 100644 --- a/packages/tui/test/terminal-appearance.test.ts +++ b/packages/tui/test/terminal-appearance.test.ts @@ -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 });