feat(coding-agent): add automatic theme mode (#5874)
This commit is contained in:
@@ -62,7 +62,12 @@ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "
|
||||
// Terminal interface and implementations
|
||||
export { ProcessTerminal, type Terminal } from "./terminal.ts";
|
||||
// Terminal colors
|
||||
export { parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts";
|
||||
export {
|
||||
parseOsc11BackgroundColor,
|
||||
parseTerminalColorSchemeReport,
|
||||
type RgbColor,
|
||||
type TerminalColorScheme,
|
||||
} from "./terminal-colors.ts";
|
||||
// Terminal image support
|
||||
export {
|
||||
allocateImageId,
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface RgbColor {
|
||||
b: number;
|
||||
}
|
||||
|
||||
export type TerminalColorScheme = "dark" | "light";
|
||||
|
||||
function hexToRgb(hex: string): RgbColor {
|
||||
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
@@ -24,6 +26,7 @@ function parseOscHexChannel(channel: string): number | undefined {
|
||||
}
|
||||
|
||||
const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i;
|
||||
const COLOR_SCHEME_REPORT_PATTERN = /^\x1b\[\?997;(1|2)n$/;
|
||||
|
||||
export function isOsc11BackgroundColorResponse(data: string): boolean {
|
||||
return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data);
|
||||
@@ -60,3 +63,11 @@ export function parseOsc11BackgroundColor(data: string): RgbColor | undefined {
|
||||
const b = parseOscHexChannel(blue);
|
||||
return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;
|
||||
}
|
||||
|
||||
export function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined {
|
||||
const match = data.match(COLOR_SCHEME_REPORT_PATTERN);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
return match[1] === "2" ? "light" : "dark";
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ import * as path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { isKeyRelease, matchesKey } from "./keys.ts";
|
||||
import type { Terminal } from "./terminal.ts";
|
||||
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts";
|
||||
import {
|
||||
isOsc11BackgroundColorResponse,
|
||||
parseOsc11BackgroundColor,
|
||||
parseTerminalColorSchemeReport,
|
||||
type RgbColor,
|
||||
type TerminalColorScheme,
|
||||
} from "./terminal-colors.ts";
|
||||
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts";
|
||||
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts";
|
||||
|
||||
@@ -311,6 +317,8 @@ export class TUI extends Container {
|
||||
private stopped = false;
|
||||
private pendingOsc11BackgroundReplies = 0;
|
||||
private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = [];
|
||||
private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
|
||||
private terminalColorSchemeNotificationsEnabled = false;
|
||||
|
||||
// Overlay stack for modal components rendered on top of base content
|
||||
private focusOrderCounter = 0;
|
||||
@@ -631,6 +639,9 @@ export class TUI extends Container {
|
||||
() => this.requestRender(),
|
||||
);
|
||||
this.terminal.hideCursor();
|
||||
if (this.terminalColorSchemeNotificationsEnabled) {
|
||||
this.terminal.write("\x1b[?2031h");
|
||||
}
|
||||
this.queryCellSize();
|
||||
this.requestRender();
|
||||
}
|
||||
@@ -646,6 +657,23 @@ export class TUI extends Container {
|
||||
this.inputListeners.delete(listener);
|
||||
}
|
||||
|
||||
onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void {
|
||||
this.terminalColorSchemeListeners.add(listener);
|
||||
return () => {
|
||||
this.terminalColorSchemeListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
setTerminalColorSchemeNotifications(enabled: boolean): void {
|
||||
if (this.terminalColorSchemeNotificationsEnabled === enabled) {
|
||||
return;
|
||||
}
|
||||
this.terminalColorSchemeNotificationsEnabled = enabled;
|
||||
if (!this.stopped) {
|
||||
this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l");
|
||||
}
|
||||
}
|
||||
|
||||
private queryCellSize(): void {
|
||||
// Only query if terminal supports images (cell size is only used for image rendering)
|
||||
if (!getCapabilities().images) {
|
||||
@@ -662,6 +690,9 @@ export class TUI extends Container {
|
||||
clearTimeout(this.renderTimer);
|
||||
this.renderTimer = undefined;
|
||||
}
|
||||
if (this.terminalColorSchemeNotificationsEnabled) {
|
||||
this.terminal.write("\x1b[?2031l");
|
||||
}
|
||||
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
||||
if (this.previousLines.length > 0) {
|
||||
const targetRow = this.previousLines.length; // Line after the last content
|
||||
@@ -731,6 +762,9 @@ export class TUI extends Container {
|
||||
if (this.consumeOsc11BackgroundResponse(data)) {
|
||||
return;
|
||||
}
|
||||
if (this.consumeTerminalColorSchemeReport(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inputListeners.size > 0) {
|
||||
let current = data;
|
||||
@@ -824,6 +858,18 @@ export class TUI extends Container {
|
||||
return true;
|
||||
}
|
||||
|
||||
private consumeTerminalColorSchemeReport(data: string): boolean {
|
||||
const scheme = parseTerminalColorSchemeReport(data);
|
||||
if (!scheme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const listener of this.terminalColorSchemeListeners) {
|
||||
listener(scheme);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private consumeCellSizeResponse(data: string): boolean {
|
||||
// Response format: ESC [ 6 ; height ; width t
|
||||
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
|
||||
@@ -1638,4 +1684,31 @@ export class TUI extends Container {
|
||||
this.terminal.write("\x1b]11;?\x07");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
|
||||
* Terminals that support the color palette notification protocol reply with
|
||||
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
|
||||
*/
|
||||
queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise<TerminalColorScheme | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
let unsubscribe: () => void = () => {};
|
||||
const settle = (scheme: TerminalColorScheme | undefined) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
unsubscribe();
|
||||
resolve(scheme);
|
||||
};
|
||||
|
||||
unsubscribe = this.onTerminalColorSchemeChange(settle);
|
||||
timer = setTimeout(() => settle(undefined), timeoutMs);
|
||||
this.terminal.write("\x1b[?996n");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { type Component, parseOsc11BackgroundColor, type Terminal, TUI } from "../src/index.ts";
|
||||
import {
|
||||
type Component,
|
||||
parseOsc11BackgroundColor,
|
||||
parseTerminalColorSchemeReport,
|
||||
type Terminal,
|
||||
TUI,
|
||||
} from "../src/index.ts";
|
||||
|
||||
class TestTerminal implements Terminal {
|
||||
private inputHandler?: (data: string) => void;
|
||||
@@ -104,6 +110,16 @@ describe("parseOsc11BackgroundColor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTerminalColorSchemeReport", () => {
|
||||
it("parses color scheme reports", () => {
|
||||
assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;1n"), "dark");
|
||||
assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;2n"), "light");
|
||||
assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;3n"), undefined);
|
||||
assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?996n"), undefined);
|
||||
assert.strictEqual(parseTerminalColorSchemeReport("x\x1b[?997;1n"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TUI.queryTerminalBackgroundColor", () => {
|
||||
it("writes OSC 11 query and resolves with the parsed RGB reply", async () => {
|
||||
const terminal = new TestTerminal();
|
||||
|
||||
Reference in New Issue
Block a user