Skip to content

Commit bc3c376

Browse files
authored
Merge pull request #5628 from Tyriar/5626
Support color scheme reporting
2 parents 8eed7b4 + 9fca3d8 commit bc3c376

10 files changed

Lines changed: 117 additions & 5 deletions

File tree

src/browser/CoreBrowserTerminal.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { SelectionService } from 'browser/services/SelectionService';
4242
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
4343
import { ThemeService } from 'browser/services/ThemeService';
4444
import { KeyboardService } from 'browser/services/KeyboardService';
45-
import { channels, color } from 'common/Color';
45+
import { channels, color, rgb } from 'common/Color';
4646
import { CoreTerminal } from 'common/CoreTerminal';
4747
import * as Browser from 'common/Platform';
4848
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
@@ -252,6 +252,20 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
252252
}
253253
}
254254

255+
/**
256+
* Reports the current color scheme (dark or light) based on the relative luminance
257+
* of the background and foreground theme colors.
258+
* Sends CSI ? 997 ; 1 n for dark mode or CSI ? 997 ; 2 n for light mode.
259+
*/
260+
private _reportColorScheme(): void {
261+
if (!this._themeService) return;
262+
const bgLuminance = rgb.relativeLuminance(this._themeService.colors.background.rgba >> 8);
263+
const fgLuminance = rgb.relativeLuminance(this._themeService.colors.foreground.rgba >> 8);
264+
// Dark mode = background is darker than foreground (lower luminance)
265+
const colorSchemeMode = bgLuminance < fgLuminance ? 1 : 2;
266+
this.coreService.triggerDataEvent(`${C0.ESC}[?997;${colorSchemeMode}n`);
267+
}
268+
255269
protected _setup(): void {
256270
super._setup();
257271

@@ -495,6 +509,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
495509
this._themeService = this._instantiationService.createInstance(ThemeService);
496510
this._instantiationService.setService(IThemeService, this._themeService);
497511

512+
// CSI ? 996 n - color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
513+
this._register(this._inputHandler.onRequestColorSchemeQuery(() => this._reportColorScheme()));
514+
515+
// Emit unsolicited color scheme notification on theme change when DECSET 2031 is enabled
516+
this._register(this._themeService.onChangeColors(() => {
517+
if (this.coreService.decPrivateModes.colorSchemeUpdates) {
518+
this._reportColorScheme();
519+
}
520+
}));
521+
498522
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
499523
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
500524

src/common/InputHandler.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,26 @@ describe('InputHandler', () => {
269269
inputHandler.resetModePrivate(Params.fromArray([2004]));
270270
assert.equal(coreService.decPrivateModes.bracketedPasteMode, false);
271271
});
272+
it('should toggle colorSchemeUpdates (DECSET 2031)', () => {
273+
const coreService = new MockCoreService();
274+
const optionsService = new MockOptionsService();
275+
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
276+
// Set color scheme updates mode (default colorSchemeQuery=true)
277+
inputHandler.setModePrivate(Params.fromArray([2031]));
278+
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true);
279+
// Reset color scheme updates mode
280+
inputHandler.resetModePrivate(Params.fromArray([2031]));
281+
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false);
282+
});
283+
it('should not toggle colorSchemeUpdates when colorSchemeQuery is disabled', () => {
284+
const coreService = new MockCoreService();
285+
const optionsService = new MockOptionsService();
286+
optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false };
287+
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
288+
// Attempt to set color scheme updates mode
289+
inputHandler.setModePrivate(Params.fromArray([2031]));
290+
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false);
291+
});
272292
});
273293
describe('regression tests', function (): void {
274294
function termContent(bufferService: IBufferService, trim: boolean): string[] {

src/common/InputHandler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ export class InputHandler extends Disposable implements IInputHandler {
160160
public readonly onTitleChange = this._onTitleChange.event;
161161
private readonly _onColor = this._register(new Emitter<IColorEvent>());
162162
public readonly onColor = this._onColor.event;
163+
private readonly _onRequestColorSchemeQuery = this._register(new Emitter<void>());
164+
public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event;
163165

164166
private _parseStack: IParseStack = {
165167
paused: false,
@@ -2034,6 +2036,11 @@ export class InputHandler extends Disposable implements IInputHandler {
20342036
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)
20352037
this._coreService.decPrivateModes.synchronizedOutput = true;
20362038
break;
2039+
case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
2040+
if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {
2041+
this._coreService.decPrivateModes.colorSchemeUpdates = true;
2042+
}
2043+
break;
20372044
case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md)
20382045
if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {
20392046
this._coreService.decPrivateModes.win32InputMode = true;
@@ -2279,6 +2286,11 @@ export class InputHandler extends Disposable implements IInputHandler {
22792286
this._coreService.decPrivateModes.synchronizedOutput = false;
22802287
this._onRequestRefreshRows.fire(undefined);
22812288
break;
2289+
case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
2290+
if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {
2291+
this._coreService.decPrivateModes.colorSchemeUpdates = false;
2292+
}
2293+
break;
22822294
case 9001: // win32-input-mode
22832295
if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) {
22842296
this._coreService.decPrivateModes.win32InputMode = false;
@@ -2782,6 +2794,12 @@ export class InputHandler extends Disposable implements IInputHandler {
27822794
// no dec locator/mouse
27832795
// this.handler(C0.ESC + '[?50n');
27842796
break;
2797+
case 996:
2798+
// color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
2799+
if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) {
2800+
this._onRequestColorSchemeQuery.fire();
2801+
}
2802+
break;
27852803
}
27862804
return true;
27872805
}

src/common/TestUtils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class MockCoreService implements ICoreService {
106106
applicationCursorKeys: false,
107107
applicationKeypad: false,
108108
bracketedPasteMode: false,
109+
colorSchemeUpdates: false,
109110
cursorBlink: undefined,
110111
cursorStyle: undefined,
111112
origin: false,

src/common/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export interface IDecPrivateModes {
269269
applicationCursorKeys: boolean;
270270
applicationKeypad: boolean;
271271
bracketedPasteMode: boolean;
272+
colorSchemeUpdates: boolean;
272273
cursorBlink: boolean | undefined;
273274
cursorStyle: CursorStyle | undefined;
274275
origin: boolean;

src/common/services/CoreService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
1717
applicationCursorKeys: false,
1818
applicationKeypad: false,
1919
bracketedPasteMode: false,
20+
colorSchemeUpdates: false,
2021
cursorBlink: undefined,
2122
cursorStyle: undefined,
2223
origin: false,

src/common/services/Services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export interface IVtExtensions {
313313
kittyKeyboard?: boolean;
314314
kittySgrBoldFaintControl?: boolean;
315315
win32InputMode?: boolean;
316+
colorSchemeQuery?: boolean;
316317
}
317318

318319
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');

test/playwright/InputHandler.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,8 +1234,30 @@ test.describe('InputHandler Integration Tests', () => {
12341234
test.skip('CSI > Ps n - Disable key modifier options, xterm', () => {
12351235
// TODO: Implement
12361236
});
1237-
test.describe.skip('CSI ? Ps n - DSR: Device Status Report (DEC-specific).', () => {
1238-
// TODO: Implement
1237+
test.describe('CSI ? Ps n - DECDSR: Device Status Report (DEC-specific)', () => {
1238+
test('Color Scheme Query - CSI ? 996 n (dark theme)', async () => {
1239+
// Default theme has dark background (#000000) and light foreground (#ffffff)
1240+
await ctx.proxy.write('\x1b[?996n');
1241+
deepStrictEqual(recordedData, ['\x1b[?997;1n']);
1242+
});
1243+
1244+
test('Color Scheme Query - CSI ? 996 n (light theme)', async () => {
1245+
recordedData.length = 0;
1246+
await ctx.page.evaluate(`window.term.options.theme = { background: '#ffffff', foreground: '#000000' }`);
1247+
await ctx.proxy.write('\x1b[?996n');
1248+
deepStrictEqual(recordedData, ['\x1b[?997;2n']);
1249+
// Restore default theme
1250+
await ctx.page.evaluate(`window.term.options.theme = { background: '#000000', foreground: '#ffffff' }`);
1251+
});
1252+
1253+
test('Color Scheme Query disabled via vtExtensions.colorSchemeQuery', async () => {
1254+
recordedData.length = 0;
1255+
await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: false }`);
1256+
await ctx.proxy.write('\x1b[?996n');
1257+
deepStrictEqual(recordedData, []);
1258+
// Re-enable
1259+
await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: true }`);
1260+
});
12391261
});
12401262
test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => {
12411263
// TODO: Implement

typings/xterm-headless.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ declare module '@xterm/headless' {
233233
windowOptions?: IWindowOptions;
234234

235235
/**
236-
* Enable various VT extensions. All extensions are disabled by default.
236+
* Enable various VT extensions.
237237
*/
238238
vtExtensions?: IVtExtensions;
239239
}
@@ -356,6 +356,18 @@ declare module '@xterm/headless' {
356356
* [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md
357357
*/
358358
win32InputMode?: boolean;
359+
360+
/**
361+
* Whether [color scheme query and notification][0] (`CSI ? 996 n` and
362+
* `DECSET 2031`) is enabled. When enabled, the terminal will respond to
363+
* color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n`
364+
* (light) based on the relative luminance of the background and foreground
365+
* theme colors. Programs can enable unsolicited notifications via
366+
* `CSI ? 2031 h`. The default is true.
367+
*
368+
* [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
369+
*/
370+
colorSchemeQuery?: boolean;
359371
}
360372

361373
/**

typings/xterm.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ declare module '@xterm/xterm' {
291291
theme?: ITheme;
292292

293293
/**
294-
* Enable various VT extensions. All extensions are disabled by default.
294+
* Enable various VT extensions.
295295
*/
296296
vtExtensions?: IVtExtensions;
297297

@@ -473,6 +473,18 @@ declare module '@xterm/xterm' {
473473
* [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md
474474
*/
475475
win32InputMode?: boolean;
476+
477+
/**
478+
* Whether [color scheme query and notification][0] (`CSI ? 996 n` and
479+
* `DECSET 2031`) is enabled. When enabled, the terminal will respond to
480+
* color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n`
481+
* (light) based on the relative luminance of the background and foreground
482+
* theme colors. Programs can enable unsolicited notifications via
483+
* `CSI ? 2031 h`. The default is true.
484+
*
485+
* [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
486+
*/
487+
colorSchemeQuery?: boolean;
476488
}
477489

478490
/**

0 commit comments

Comments
 (0)