diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 9ad975f6..51d40224 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 5ba2342a..fc02715e 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 1bb923e6..016f39b7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -160,6 +160,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, @@ -2034,6 +2036,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; @@ -2279,6 +2286,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; @@ -2782,6 +2794,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 137174e8..908cae0c 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 0d19a030..5d7c1b18 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -269,6 +269,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 f2a15d8e..d8962ec8 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 30ad6048..341c0bc2 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 a0cbc7c4..a7d1bc77 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 35bfac07..99c20be4 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 604e9504..19ab3da0 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; } /**