diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index f8b8b3e47f..06ac57f31c 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -39,7 +39,7 @@ import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, IAnsiColorChangeEvent } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; @@ -53,6 +53,7 @@ import { Linkifier2 } from 'browser/Linkifier2'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { CoreTerminal } from 'common/CoreTerminal'; import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; +import { rgba } from 'browser/Color'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -148,6 +149,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._inputHandler.onRequestReset(() => this.reset())); this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined))); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); + this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event))); this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); @@ -157,6 +159,19 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); } + private _changeAnsiColor(event: IAnsiColorChangeEvent): void { + if (!this._colorManager) { return; } + + event.colors.forEach(ansiColor => { + const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue); + + this._colorManager!.colors.ansi[ansiColor.colorIndex] = color; + }); + + this._renderService?.setColors(this._colorManager!.colors); + this.viewport?.onThemeChange(this._colorManager!.colors); + } + public dispose(): void { if (this._isDisposed) { return; diff --git a/src/browser/renderer/atlas/CharAtlasUtils.ts b/src/browser/renderer/atlas/CharAtlasUtils.ts index 346b35f8e9..20695d3c40 100644 --- a/src/browser/renderer/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/atlas/CharAtlasUtils.ts @@ -16,9 +16,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number cursor: undefined, cursorAccent: undefined, selection: undefined, - // For the static char atlas, we only use the first 16 colors, but we need all 256 for the - // dynamic character atlas. - ansi: colors.ansi.slice(0, 16) + ansi: colors.ansi }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 46f8eaccc7..4c0d8559fc 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -5,7 +5,7 @@ import { assert, expect } from 'chai'; import { InputHandler } from 'common/InputHandler'; -import { IBufferLine, IAttributeData } from 'common/Types'; +import { IBufferLine, IAttributeData, IAnsiColorChangeEvent } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; @@ -40,6 +40,7 @@ class TestInputHandler extends InputHandler { public get curAttrData(): IAttributeData { return (this as any)._curAttrData; } public get windowTitleStack(): string[] { return this._windowTitleStack; } public get iconNameStack(): string[] { return this._iconNameStack; } + public parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { return this._parseAnsiColorChange(data); } } describe('InputHandler', () => { @@ -1692,4 +1693,55 @@ describe('InputHandler', () => { assert.equal(coreService.decPrivateModes.origin, false); }); }); + describe('OSC', () => { + it('4: should parse correct Ansi color change data', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3'); + + assert.isNotNull(event); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + }), + it('4: should ignore incorrect Ansi color change data', () => { + // this is testing a private method + assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:a/b/c')); + assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:#aabbcc')); + assert.isNull(inputHandler.parseAnsiColorChange('17;rgba:aa/bb/cc')); + assert.isNull(inputHandler.parseAnsiColorChange('rgb:aa/bb/cc')); + }); + it('4: should parse a list of Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:00/11/22;255;rgb:01/ef/2d'); + + assert.isNotNull(event); + assert.equal(event!.colors.length, 3); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + assert.deepEqual(event!.colors[1], { colorIndex: 17, red: 0x00, green: 0x11, blue: 0x22 }); + assert.deepEqual(event!.colors[2], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d }); + }); + it('4: should ignore incorrect colors in a list of Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:WR/ON/G;255;rgb:01/ef/2d'); + + assert.equal(event!.colors.length, 2); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + assert.deepEqual(event!.colors[1], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d }); + }); + it('4: should be case insensitive when parsing Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rGb:A1/b2/C3'); + + assert.equal(event!.colors.length, 1); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + }); + it('4: should fire event on Ansi color change', (done) => { + inputHandler.onAnsiColorChange(e => { + assert.isNotNull(e); + assert.isNotNull(e!.colors); + assert.deepEqual(e!.colors[0], { colorIndex: 17, red: 0x1a, green: 0x2b, blue: 0x3c }); + assert.deepEqual(e!.colors[1], { colorIndex: 12, red: 0x11, green: 0x22, blue: 0x33 }); + done(); + }); + inputHandler.parse('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\'); + }); + }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index dbf695e607..626e1fe2a0 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions } from 'common/Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -250,6 +250,8 @@ export class InputHandler extends Disposable implements IInputHandler { public get onScroll(): IEvent { return this._onScroll.event; } private _onTitleChange = new EventEmitter(); public get onTitleChange(): IEvent { return this._onTitleChange.event; } + private _onAnsiColorChange = new EventEmitter(); + public get onAnsiColorChange(): IEvent { return this._onAnsiColorChange.event; } constructor( private readonly _bufferService: IBufferService, @@ -372,6 +374,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data))); // 3 - set property X in the form "prop=value" // 4 - Change Color Number + this._parser.setOscHandler(4, new OscHandler((data: string) => this.setAnsiColor(data))); // 5 - Change Special Color Number // 6 - Enable/disable Special Color Number c // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) @@ -2711,6 +2714,45 @@ export class InputHandler extends Disposable implements IInputHandler { this._iconName = data; } + protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { + const result: IAnsiColorChangeEvent = { colors: [] }; + // example data: 5;rgb:aa/bb/cc + const regex = /(\d+);rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/gi; + let match; + + while ((match = regex.exec(data)) !== null) { + result.colors.push({ + colorIndex: parseInt(match[1]), + red: parseInt(match[2], 16), + green: parseInt(match[3], 16), + blue: parseInt(match[4], 16) + }); + } + + if (result.colors.length === 0) { + return null; + } + + return result; + } + + /** + * OSC 4; ; ST (set ANSI color to ) + * + * @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`." + * `c` is the color index between 0 and 255. `spec` color format is 'rgb:hh/hh/hh' where `h` are hexadecimal digits. + * There may be multipe c ; spec elements present in the same instruction, e.g. 1;rgb:10/20/30;2;rgb:a0/b0/c0. + */ + public setAnsiColor(data: string): void { + const event = this._parseAnsiColorChange(data); + if (event) { + this._onAnsiColorChange.fire(event); + } + else { + this._logService.warn(`Expected format ;rgb:// but got data: ${data}`); + } + } + /** * ESC E * C1.NEL diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index bd0d11c606..1ea26c2a70 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -328,6 +328,20 @@ export interface IWindowOptions { setWinLines?: boolean; } +export interface IAnsiColorChangeEventColor { + colorIndex: number; + red: number; + green: number; + blue: number; +} + +/** + * Event fired for OSC 4 command - to change ANSI color based on its index. + */ +export interface IAnsiColorChangeEvent { + colors: IAnsiColorChangeEventColor[]; +} + /** * Calls the parser and handles actions generated by the parser. */ @@ -389,6 +403,7 @@ export interface IInputHandler { /** CSI ' ~ */ deleteColumns(params: IParams): void; /** OSC 0 OSC 2 */ setTitle(data: string): void; + /** OSC 4 */ setAnsiColor(data: string): void; /** ESC E */ nextLine(): void; /** ESC = */ keypadApplicationMode(): void; /** ESC > */ keypadNumericMode(): void;