apply PR #5490: fix(tui): refresh dark/light appearance on explicit ctrl+l reset
Cherry-picked 69c9fe8d4; resolved terminal.ts against the newer onPrivateModeReport signature and unioned appearance tests with the Windows Terminal polling regression.
This commit is contained in:
@@ -362,6 +362,7 @@
|
||||
|
||||
- Removed the `--prewalk-boomerang` feature and its associated configuration setting.
|
||||
- Removed the unreliable Bing and Yahoo HTML-scraping web search providers.
|
||||
- Fixed Ctrl+L (`app.display.reset`) not refreshing the dark/light theme on terminals without an end-to-end DEC Mode 2031 notification path (e.g. iTerm2 under tmux): the explicit reset gesture now issues one bounded OSC 11 background re-query before repainting, so a mid-session appearance switch is picked up without restarting. No timers or periodic polling are reintroduced ([#5352](https://github.com/can1357/oh-my-pi/issues/5352))
|
||||
|
||||
## [16.4.8] - 2026-07-12
|
||||
|
||||
|
||||
@@ -391,7 +391,16 @@ export class InputController {
|
||||
this.ctx.editor.onClear = () => this.handleCtrlC();
|
||||
this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
|
||||
this.ctx.editor.setActionKeys("app.display.reset", this.ctx.keybindings.getKeys("app.display.reset"));
|
||||
this.ctx.editor.onDisplayReset = () => this.ctx.ui.resetDisplay();
|
||||
this.ctx.editor.onDisplayReset = () => {
|
||||
// Explicit user gesture (Ctrl+L): re-query the terminal background once
|
||||
// so a mid-session light/dark switch is picked up even on terminals
|
||||
// without an end-to-end Mode 2031 notification path (#5352). The
|
||||
// appearance callback re-evaluates the auto theme; the repaint below
|
||||
// then renders the resolved palette. Bounded to one OSC 11 probe per
|
||||
// gesture — no timers, no periodic polling.
|
||||
this.ctx.ui.terminal.refreshAppearance?.();
|
||||
this.ctx.ui.resetDisplay();
|
||||
};
|
||||
this.ctx.editor.onExit = () => this.handleCtrlD();
|
||||
this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
|
||||
this.ctx.editor.onSuspend = () => this.handleCtrlZ();
|
||||
|
||||
@@ -79,6 +79,7 @@ async function createContext() {
|
||||
});
|
||||
const addStartListener = vi.fn();
|
||||
const terminalWrite = vi.fn();
|
||||
const refreshAppearance = vi.fn();
|
||||
const prompt = vi.fn(async () => {});
|
||||
const retry = vi.fn(async () => true);
|
||||
const abort = vi.fn(async () => {});
|
||||
@@ -135,7 +136,7 @@ async function createContext() {
|
||||
addInputListener,
|
||||
addStartListener,
|
||||
getFocused: vi.fn(() => focused),
|
||||
terminal: { write: terminalWrite },
|
||||
terminal: { write: terminalWrite, refreshAppearance },
|
||||
} as unknown as InteractiveModeContext["ui"],
|
||||
loadingAnimation: undefined,
|
||||
autoCompactionLoader: undefined,
|
||||
@@ -217,6 +218,7 @@ async function createContext() {
|
||||
retry,
|
||||
abort,
|
||||
resetDisplay,
|
||||
refreshAppearance,
|
||||
handleBtwBranchKey,
|
||||
addInputListener,
|
||||
canBranchBtw,
|
||||
@@ -249,6 +251,12 @@ describe("InputController keybinding setup", () => {
|
||||
expect(spies.showModelSelector).toHaveBeenNthCalledWith(1, { temporaryOnly: true });
|
||||
expect(spies.showModelSelector).toHaveBeenNthCalledWith(2);
|
||||
expect(spies.resetDisplay).toHaveBeenCalledTimes(1);
|
||||
expect(spies.refreshAppearance).toHaveBeenCalledTimes(1);
|
||||
// The background re-query must run before the repaint so the appearance
|
||||
// callback re-evaluates the auto theme against the fresh classification.
|
||||
expect(spies.refreshAppearance.mock.invocationCallOrder[0]!).toBeLessThan(
|
||||
spies.resetDisplay.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not mark pasted shell prompts as Python mode while editing", async () => {
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed a rendering issue where resizing the terminal during forced renders (such as tool finalization or image reconciliation) caused the entire transcript to visibly replay and flicker. Forced renders are now consolidated into a single paint once the resize settles.
|
||||
### Added
|
||||
|
||||
- Added an optional `Terminal.refreshAppearance()` that issues a single bounded OSC 11 background re-query through the existing query/DA1 pipeline, letting consumers refresh the detected dark/light appearance on an explicit user gesture without reintroducing periodic polling ([#5352](https://github.com/can1357/oh-my-pi/issues/5352))
|
||||
|
||||
## [16.4.7] - 2026-07-12
|
||||
|
||||
|
||||
@@ -402,6 +402,16 @@ export interface Terminal {
|
||||
* already-detected appearance so late subscribers never miss it.
|
||||
*/
|
||||
onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
|
||||
/**
|
||||
* Issue a single OSC 11 background-color re-query, driving the appearance
|
||||
* callbacks through the same parse/dedup pipeline used at startup and on Mode
|
||||
* 2031 notifications. Bounded: one probe per call, no timers. Invoked on the
|
||||
* user's explicit display-reset gesture (Ctrl+L) so terminals that cannot
|
||||
* deliver end-to-end Mode 2031 notifications still pick up a light/dark switch
|
||||
* without a restart. Optional so custom Terminals built against older pi-tui
|
||||
* versions keep working.
|
||||
*/
|
||||
refreshAppearance?(): void;
|
||||
/** The last detected terminal appearance, or undefined if not yet known. */
|
||||
get appearance(): TerminalAppearance | undefined;
|
||||
/**
|
||||
@@ -550,6 +560,19 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-query the terminal background via a single OSC 11 probe. Reuses the
|
||||
* startup query path — same DA1-sentinel FIFO, pending/queued gating, parsing,
|
||||
* dedup, and appearance callbacks — so a light/dark switch is picked up
|
||||
* without a restart on terminals lacking end-to-end Mode 2031 notifications.
|
||||
* Bounded to one probe per call; no timers are armed. Suppressed while headless
|
||||
* or after the terminal is torn down.
|
||||
*/
|
||||
refreshAppearance(): void {
|
||||
if (this.#headless || this.#dead) return;
|
||||
this.#queryBackgroundColor();
|
||||
}
|
||||
|
||||
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void {
|
||||
this.#privateModeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
@@ -259,6 +259,70 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("refreshAppearance() issues exactly one OSC 11 re-query per call (#5352)", () => {
|
||||
const { terminal, queryCount } = setupTerminal();
|
||||
|
||||
// Drain the OSC 11 reply and all seven startup DA1 sentinels (keyboard,
|
||||
// OSC 11, and the DECRQM probes for 2026/2048/2031/1010/1011) so the
|
||||
// probe FIFO is empty before the refresh gesture.
|
||||
process.stdin.emit("data", "\x1b]11;rgb:ffff/ffff/ffff\x07");
|
||||
for (let i = 0; i < 7; i++) process.stdin.emit("data", "\x1b[?1;2c");
|
||||
const afterInitial = queryCount();
|
||||
|
||||
// An explicit refresh gesture (Ctrl+L) issues one bounded probe.
|
||||
terminal.refreshAppearance?.();
|
||||
expect(queryCount()).toBe(afterInitial + 1);
|
||||
|
||||
// Complete that query's cycle, then refresh again: still one probe each.
|
||||
process.stdin.emit("data", "\x1b]11;rgb:0000/0000/0000\x07");
|
||||
process.stdin.emit("data", "\x1b[?1;2c");
|
||||
terminal.refreshAppearance?.();
|
||||
expect(queryCount()).toBe(afterInitial + 2);
|
||||
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("refreshAppearance() re-evaluates a changed background through the callback pipeline", () => {
|
||||
const { terminal } = setupTerminal();
|
||||
const appearances: string[] = [];
|
||||
terminal.onAppearanceChange(a => appearances.push(a));
|
||||
|
||||
// Startup classifies the terminal as light.
|
||||
process.stdin.emit("data", "\x1b]11;rgb:ffff/ffff/ffff\x07");
|
||||
for (let i = 0; i < 7; i++) process.stdin.emit("data", "\x1b[?1;2c");
|
||||
expect(terminal.appearance).toBe("light");
|
||||
expect(appearances).toEqual(["light"]);
|
||||
|
||||
// User switches the OS/terminal to dark and presses Ctrl+L. The bounded
|
||||
// re-query picks up the new background and fires the appearance callback.
|
||||
terminal.refreshAppearance?.();
|
||||
process.stdin.emit("data", "\x1b]11;rgb:0000/0000/0000\x07");
|
||||
process.stdin.emit("data", "\x1b[?1;2c");
|
||||
|
||||
expect(terminal.appearance).toBe("dark");
|
||||
expect(appearances).toEqual(["light", "dark"]);
|
||||
|
||||
terminal.stop();
|
||||
});
|
||||
|
||||
it("refreshAppearance() still probes through a Mode 2031-capable bridge (#5352)", () => {
|
||||
const { terminal, queryCount } = setupTerminal();
|
||||
|
||||
// Drain startup, then have the bridge (e.g. tmux) advertise Mode 2031
|
||||
// support via DECRQM — CSI ? 2031 ; 2 $ y (reset/supported). The outer
|
||||
// terminal may still never emit an appearance notification, so an explicit
|
||||
// refresh must not be gated on advertised 2031 support.
|
||||
process.stdin.emit("data", "\x1b]11;rgb:ffff/ffff/ffff\x07");
|
||||
for (let i = 0; i < 7; i++) process.stdin.emit("data", "\x1b[?1;2c");
|
||||
process.stdin.emit("data", "\x1b[?2031;2$y");
|
||||
const afterInitial = queryCount();
|
||||
|
||||
terminal.refreshAppearance?.();
|
||||
expect(queryCount()).toBe(afterInitial + 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