From 01a732274c40cc87e88d4eeb7f0d518c43d11c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 22 Oct 2021 22:48:57 +0200 Subject: [PATCH] OSC 10/11 working, OSC 4 partially fixed --- src/browser/Color.ts | 17 +++++++++ src/browser/Terminal.ts | 64 +++++++++++++++++++++++++++------ src/common/InputHandler.test.ts | 34 +++++++++--------- src/common/InputHandler.ts | 30 +++++++++------- src/common/Types.d.ts | 12 +++---- 5 files changed, 109 insertions(+), 48 deletions(-) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index c43c5eb50..42a65876f 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -5,6 +5,8 @@ import { IColor } from 'browser/Types'; +// FIXME: Move Color.ts lib to common? + /** * Helper functions where the source type is "channels" (individual color channels as numbers). */ @@ -17,6 +19,8 @@ export namespace channels { } export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number { + // Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32 + // on LE systems, before it can be used for direct 32-bit buffer writes. // >>> 0 forces an unsigned int return (r << 24 | g << 16 | b << 8 | a) >>> 0; } @@ -81,6 +85,11 @@ export namespace color { rgba: channels.toRgba(r, g, b, a) }; } + + export function toXColorName(color: IColor): string { + const [r, g, b] = rgba.toChannels(color.rgba); + return `rgb:${toPaddedHex(r)}/${toPaddedHex(g)}/${toPaddedHex(b)}`; + } } /** @@ -197,6 +206,7 @@ export namespace rgba { return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } + // FIXME: Move this to channels NS? export function toChannels(value: number): [number, number, number, number] { return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; } @@ -207,6 +217,13 @@ export namespace rgba { rgba: channels.toRgba(r, g, b) }; } + + /** + * convert 0xRRGGBBAA to 0xAABBGGRR (32-bit representation on LE systems) + */ + export function toABGR32(rgba: number): number { + return ((rgba & 0xFF) << 24 | (rgba >>> 8 & 0xFF) << 16 | (rgba >>> 16 & 0xFF) << 8 | rgba >>> 24 & 0xFF) >>> 0; + } } export function toPaddedHex(c: number): string { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 2cd9bf99c..4dcf8460b 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler, IColor } from 'browser/Types'; import { IRenderer } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; @@ -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, ScrollSource, IAnsiColorChangeEvent } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent } 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,7 +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'; +import { color, rgba } from 'browser/Color'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; // Let it work inside Node.js for automated testing purposes. @@ -167,7 +167,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus())); this.register(this._inputHandler.onRequestReset(() => this.reset())); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); - this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event))); + this.register(this._inputHandler.onColor((event) => this._handleColorEvent(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)); @@ -177,17 +177,59 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); } - private _changeAnsiColor(event: IAnsiColorChangeEvent): void { + private _handleColorEvent(event: IColorEvent): void { if (!this._colorManager) { return; } - for (const ansiColor of event.colors) { - const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue); - - this._colorManager!.colors.ansi[ansiColor.colorIndex] = color; + let hasSet = false; + const query: string[] = []; + for (const req of event.requests) { + if (req.color === '?') { + // query color + let ident = ''; + let c: IColor; + switch (req.index) { + case 256: + ident = '10'; + c = this._colorManager.colors.foreground; + break; + case 257: + ident = '11'; + c = this._colorManager.colors.background; + break; + default: + if (0 <= req.index && req.index < 256) { + ident = '4;' + req.index; + c = this._colorManager.colors.ansi[req.index]; + } + } + if (ident) { + query.push(`${C0.ESC}]${ident};${color.toXColorName(c!)}${C0.BEL}`); + } + } else { + // set color + hasSet = true; + switch (req.index) { + case 256: + this._colorManager.colors.foreground = rgba.toColor(...req.color); + break; + case 257: + this._colorManager.colors.background = rgba.toColor(...req.color); + break; + default: + if (0 <= req.index && req.index < 256) { + this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); + } + } + } } - this._renderService?.setColors(this._colorManager!.colors); - this.viewport?.onThemeChange(this._colorManager!.colors); + if (query.length) { + this.coreService.triggerDataEvent(query.join('')); + } + if (hasSet) { + this._renderService?.setColors(this._colorManager.colors); + this.viewport?.onThemeChange(this._colorManager.colors); + } } public dispose(): void { diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 8d1c3b4f2..f3b0f41fd 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { InputHandler } from 'common/InputHandler'; -import { IBufferLine, IAttributeData, IAnsiColorChangeEvent } from 'common/Types'; +import { IBufferLine, IAttributeData, IColorEvent } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; @@ -41,7 +41,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); } + public parseAnsiColorChange(data: string): IColorEvent | null { return this._parseAnsiColorChange(data); } /** * Promise based parse call to await the full resolve of given input data. @@ -1924,14 +1924,12 @@ describe('InputHandler', () => { }); - - 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 }); + assert.deepEqual(event!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] }); }); it('4: should ignore incorrect Ansi color change data', () => { @@ -1947,33 +1945,33 @@ describe('InputHandler', () => { 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 }); + assert.equal(event!.requests.length, 3); + assert.deepEqual(event!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] }); + assert.deepEqual(event!.requests[1], { index: 17, color: [0x00, 0x11, 0x22] }); + assert.deepEqual(event!.requests[2], { index: 255, color: [0x01, 0xef, 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 }); + assert.equal(event!.requests.length, 2); + assert.deepEqual(event!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] }); + assert.deepEqual(event!.requests[1], { index: 255, color: [0x01, 0xef, 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 }); + assert.equal(event!.requests.length, 1); + assert.deepEqual(event!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] }); }); it('4: should fire event on Ansi color change', async () => { return new Promise(async r => { - inputHandler.onAnsiColorChange(e => { + inputHandler.onColor(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 }); + assert.isNotNull(e!.requests); + assert.deepEqual(e!.requests[0], { index: 17, color: [0x1a, 0x2b, 0x3c] }); + assert.deepEqual(e!.requests[1], { index: 12, color: [0x11, 0x22, 0x33] }); r(); }); await inputHandler.parseP('\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 3505b5487..ba51ebdd5 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -262,8 +262,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; } + private _onColor = new EventEmitter(); + public get onColor(): IEvent { return this._onColor.event; } private _parseStack: IParseStack = { paused: false, @@ -2841,22 +2841,24 @@ export class InputHandler extends Disposable implements IInputHandler { return true; } - protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { - const result: IAnsiColorChangeEvent = { colors: [] }; + protected _parseAnsiColorChange(data: string): IColorEvent | null { + const result: IColorEvent = { requests: [] }; // example data: 5;rgb:aa/bb/cc const regex = /(\d+);rgb:([\da-f]{2})\/([\da-f]{2})\/([\da-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) + result.requests.push({ + index: parseInt(match[1]), + color: [ + parseInt(match[2], 16), + parseInt(match[3], 16), + parseInt(match[4], 16) + ] }); } - if (result.colors.length === 0) { + if (result.requests.length === 0) { return null; } @@ -2923,7 +2925,7 @@ export class InputHandler extends Disposable implements IInputHandler { public setAnsiColor(data: string): boolean { const event = this._parseAnsiColorChange(data); if (event) { - this._onAnsiColorChange.fire(event); + this._onColor.fire(event); } else { this._logService.warn(`Expected format ;rgb:// but got data: ${data}`); @@ -2939,11 +2941,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (slots[0] === '?') { // TODO: query FG color console.log('query FG'); + this._onColor.fire({ requests: [{ index: 256, color: '?' }] }); } else { const color = this._parseXColorName(slots[0]); if (color) { // set new FG color console.log('set FG', color); + this._onColor.fire({ requests: [{ index: 256, color }] }); } } if (slots.length === 2) { @@ -2957,11 +2961,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (slots[0] === '?') { // TODO: query BG color console.log('query BG'); + this._onColor.fire({ requests: [{ index: 257, color: '?' }] }); } else { const color = this._parseXColorName(slots[0]); if (color) { // set new BG color console.log('set BG', color); + this._onColor.fire({ requests: [{ index: 257, color }] }); } } return true; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 78e2e62d1..58adf25e7 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -347,18 +347,16 @@ export interface IWindowOptions { setWinLines?: boolean; } -export interface IAnsiColorChangeEventColor { - colorIndex: number; - red: number; - green: number; - blue: number; +export interface IColorEventColor { + index: number; + color: [number, number, number] | '?'; } /** * Event fired for OSC 4 command - to change ANSI color based on its index. */ -export interface IAnsiColorChangeEvent { - colors: IAnsiColorChangeEventColor[]; +export interface IColorEvent { + requests: IColorEventColor[]; } /**