diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index c26e670515..42da63db1d 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -42,7 +42,7 @@ import { SelectionService } from 'browser/services/SelectionService'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ThemeService } from 'browser/services/ThemeService'; import { KeyboardService } from 'browser/services/KeyboardService'; -import { channels, color } from 'common/Color'; +import { channels, color, rgb } from 'common/Color'; import { CoreTerminal } from 'common/CoreTerminal'; import * as Browser from 'common/Platform'; import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types'; @@ -252,6 +252,20 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } } + /** + * Reports the current color scheme (dark or light) based on the relative luminance + * of the background and foreground theme colors. + * Sends CSI ? 997 ; 1 n for dark mode or CSI ? 997 ; 2 n for light mode. + */ + private _reportColorScheme(): void { + if (!this._themeService) return; + const bgLuminance = rgb.relativeLuminance(this._themeService.colors.background.rgba >> 8); + const fgLuminance = rgb.relativeLuminance(this._themeService.colors.foreground.rgba >> 8); + // Dark mode = background is darker than foreground (lower luminance) + const colorSchemeMode = bgLuminance < fgLuminance ? 1 : 2; + this.coreService.triggerDataEvent(`${C0.ESC}[?997;${colorSchemeMode}n`); + } + protected _setup(): void { super._setup(); @@ -495,6 +509,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._themeService = this._instantiationService.createInstance(ThemeService); this._instantiationService.setService(IThemeService, this._themeService); + // CSI ? 996 n - color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + this._register(this._inputHandler.onRequestColorSchemeQuery(() => this._reportColorScheme())); + + // Emit unsolicited color scheme notification on theme change when DECSET 2031 is enabled + this._register(this._themeService.onChangeColors(() => { + if (this.coreService.decPrivateModes.colorSchemeUpdates) { + this._reportColorScheme(); + } + })); + this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 5ba2342a38..fc02715ec5 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -269,6 +269,26 @@ describe('InputHandler', () => { inputHandler.resetModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, false); }); + it('should toggle colorSchemeUpdates (DECSET 2031)', () => { + const coreService = new MockCoreService(); + const optionsService = new MockOptionsService(); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + // Set color scheme updates mode (default colorSchemeQuery=true) + inputHandler.setModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true); + // Reset color scheme updates mode + inputHandler.resetModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false); + }); + it('should not toggle colorSchemeUpdates when colorSchemeQuery is disabled', () => { + const coreService = new MockCoreService(); + const optionsService = new MockOptionsService(); + optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false }; + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + // Attempt to set color scheme updates mode + inputHandler.setModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false); + }); }); describe('regression tests', function (): void { function termContent(bufferService: IBufferService, trim: boolean): string[] { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b2b75143e9..8f2ee26cbb 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -159,6 +159,8 @@ export class InputHandler extends Disposable implements IInputHandler { public readonly onTitleChange = this._onTitleChange.event; private readonly _onColor = this._register(new Emitter()); public readonly onColor = this._onColor.event; + private readonly _onRequestColorSchemeQuery = this._register(new Emitter()); + public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event; private _parseStack: IParseStack = { paused: false, @@ -2026,6 +2028,11 @@ export class InputHandler extends Disposable implements IInputHandler { case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md) this._coreService.decPrivateModes.synchronizedOutput = true; break; + case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._coreService.decPrivateModes.colorSchemeUpdates = true; + } + break; case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md) if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { this._coreService.decPrivateModes.win32InputMode = true; @@ -2271,6 +2278,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.synchronizedOutput = false; this._onRequestRefreshRows.fire(undefined); break; + case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._coreService.decPrivateModes.colorSchemeUpdates = false; + } + break; case 9001: // win32-input-mode if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { this._coreService.decPrivateModes.win32InputMode = false; @@ -2774,6 +2786,12 @@ export class InputHandler extends Disposable implements IInputHandler { // no dec locator/mouse // this.handler(C0.ESC + '[?50n'); break; + case 996: + // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._onRequestColorSchemeQuery.fire(); + } + break; } return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 2d44c32efe..363d34d4fb 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -106,6 +106,7 @@ export class MockCoreService implements ICoreService { applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + colorSchemeUpdates: false, cursorBlink: undefined, cursorStyle: undefined, origin: false, diff --git a/src/common/Types.ts b/src/common/Types.ts index 269b30c85e..34c60d7f2e 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -268,6 +268,7 @@ export interface IDecPrivateModes { applicationCursorKeys: boolean; applicationKeypad: boolean; bracketedPasteMode: boolean; + colorSchemeUpdates: boolean; cursorBlink: boolean | undefined; cursorStyle: CursorStyle | undefined; origin: boolean; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 9981ba2171..f80f25b1e5 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -17,6 +17,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + colorSchemeUpdates: false, cursorBlink: undefined, cursorStyle: undefined, origin: false, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index bbf36771a2..a233c6935b 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -313,6 +313,7 @@ export interface IVtExtensions { kittyKeyboard?: boolean; kittySgrBoldFaintControl?: boolean; win32InputMode?: boolean; + colorSchemeQuery?: boolean; } export const IOscLinkService = createDecorator('OscLinkService'); diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index a0cbc7c469..a7d1bc774c 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -1234,8 +1234,30 @@ test.describe('InputHandler Integration Tests', () => { test.skip('CSI > Ps n - Disable key modifier options, xterm', () => { // TODO: Implement }); - test.describe.skip('CSI ? Ps n - DSR: Device Status Report (DEC-specific).', () => { - // TODO: Implement + test.describe('CSI ? Ps n - DECDSR: Device Status Report (DEC-specific)', () => { + test('Color Scheme Query - CSI ? 996 n (dark theme)', async () => { + // Default theme has dark background (#000000) and light foreground (#ffffff) + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, ['\x1b[?997;1n']); + }); + + test('Color Scheme Query - CSI ? 996 n (light theme)', async () => { + recordedData.length = 0; + await ctx.page.evaluate(`window.term.options.theme = { background: '#ffffff', foreground: '#000000' }`); + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, ['\x1b[?997;2n']); + // Restore default theme + await ctx.page.evaluate(`window.term.options.theme = { background: '#000000', foreground: '#ffffff' }`); + }); + + test('Color Scheme Query disabled via vtExtensions.colorSchemeQuery', async () => { + recordedData.length = 0; + await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: false }`); + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, []); + // Re-enable + await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: true }`); + }); }); test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => { // TODO: Implement diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index c12d6ba9fb..bf06e01488 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -233,7 +233,7 @@ declare module '@xterm/headless' { windowOptions?: IWindowOptions; /** - * Enable various VT extensions. All extensions are disabled by default. + * Enable various VT extensions. */ vtExtensions?: IVtExtensions; } @@ -356,6 +356,18 @@ declare module '@xterm/headless' { * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md */ win32InputMode?: boolean; + + /** + * Whether [color scheme query and notification][0] (`CSI ? 996 n` and + * `DECSET 2031`) is enabled. When enabled, the terminal will respond to + * color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n` + * (light) based on the relative luminance of the background and foreground + * theme colors. Programs can enable unsolicited notifications via + * `CSI ? 2031 h`. The default is true. + * + * [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/ + */ + colorSchemeQuery?: boolean; } /** diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ee3364dd8a..53362f5f5a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -291,7 +291,7 @@ declare module '@xterm/xterm' { theme?: ITheme; /** - * Enable various VT extensions. All extensions are disabled by default. + * Enable various VT extensions. */ vtExtensions?: IVtExtensions; @@ -473,6 +473,18 @@ declare module '@xterm/xterm' { * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md */ win32InputMode?: boolean; + + /** + * Whether [color scheme query and notification][0] (`CSI ? 996 n` and + * `DECSET 2031`) is enabled. When enabled, the terminal will respond to + * color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n` + * (light) based on the relative luminance of the background and foreground + * theme colors. Programs can enable unsolicited notifications via + * `CSI ? 2031 h`. The default is true. + * + * [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/ + */ + colorSchemeQuery?: boolean; } /**