simplify color events

This commit is contained in:
Jörg Breitbart
2021-11-08 17:17:39 +01:00
parent 1c5f24a707
commit 54375713bc
7 changed files with 291 additions and 217 deletions
+3 -3
View File
@@ -4,6 +4,7 @@
*/
import { IColor } from 'browser/Types';
import { IColorRGB } from 'common/Types';
// FIXME: Move Color.ts lib to common?
@@ -86,9 +87,8 @@ export namespace color {
};
}
export function toXColorName(color: IColor): string {
const [r, g, b] = rgba.toChannels(color.rgba);
return `rgb:${toPaddedHex(r)}${toPaddedHex(r)}/${toPaddedHex(g)}${toPaddedHex(g)}/${toPaddedHex(b)}${toPaddedHex(b)}`;
export function toColorRGB(color: IColor): IColorRGB {
return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];
}
}
+20 -51
View File
@@ -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, IColorEvent } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, IColorRGB } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
@@ -55,6 +55,7 @@ import { CoreTerminal } from 'common/CoreTerminal';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { color, rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { toRgbString } from 'common/input/XParseColor';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -178,58 +179,26 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _handleColorEvent(event: IColorEvent): void {
if (!this._colorManager) { return; }
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);
}
}
if (!this._colorManager) return;
for (const req of event) {
switch (req.index) {
case ColorIndex.FOREGROUND: // OSC 10
if (req.color) this._colorManager.colors.foreground = rgba.toColor(...req.color);
else this.coreService.triggerDataEvent(`${C0.ESC}]10;${toRgbString(color.toColorRGB(this._colorManager.colors.foreground))}${C0.BEL}`);
break;
case ColorIndex.BACKGROUND: // OSC 11
if (req.color) this._colorManager.colors.background = rgba.toColor(...req.color);
else this.coreService.triggerDataEvent(`${C0.ESC}]11;${toRgbString(color.toColorRGB(this._colorManager.colors.background))}${C0.BEL}`);
break;
default: // OSC 4
if (0 <= req.index && req.index < 256) {
if (req.color) this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
else this.coreService.triggerDataEvent(`${C0.ESC}]4;${req.index};${toRgbString(color.toColorRGB(this._colorManager.colors.ansi[req.index]))}${C0.BEL}`);
}
}
}
if (query.length) {
this.coreService.triggerDataEvent(query.join(''));
}
if (hasSet) {
this._renderService?.setColors(this._colorManager.colors);
this.viewport?.onThemeChange(this._colorManager.colors);
}
this._renderService?.setColors(this._colorManager.colors);
this.viewport?.onThemeChange(this._colorManager.colors);
}
public dispose(): void {
+62 -82
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { InputHandler } from 'common/InputHandler';
import { IBufferLine, IAttributeData, IColorEvent } from 'common/Types';
import { IBufferLine, IAttributeData, IColorEvent, ColorIndex } 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): IColorEvent | null { return this._parseAnsiColorChange(data); }
public parseAnsiColorChange(data: string): IColorEvent{ return this._parseAnsiColorChange(data); }
/**
* Promise based parse call to await the full resolve of given input data.
@@ -54,10 +54,6 @@ class TestInputHandler extends InputHandler {
prev = await result;
}
}
public parseColorSpec(data: string): undefined | [number, number, number] {
return this._parseColorSpec(data);
}
}
describe('InputHandler', () => {
@@ -1869,114 +1865,98 @@ describe('InputHandler', () => {
});
});
describe('OSC', () => {
describe('parse xcolor names', () => {
it('rgb:<r>/<g>/<b> scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(inputHandler.parseColorSpec('rgb:0/0/0'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:f/f/f'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:1/2/3'), [17, 34, 51]);
// 8 bit
assert.deepEqual(inputHandler.parseColorSpec('rgb:00/00/00'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:ff/ff/ff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:11/22/33'), [17, 34, 51]);
// 12 bit
assert.deepEqual(inputHandler.parseColorSpec('rgb:000/000/000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:fff/fff/fff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:111/222/333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(inputHandler.parseColorSpec('rgb:0000/0000/0000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:ffff/ffff/ffff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('rgb:1111/2222/3333'), [17, 34, 51]);
});
it('#RGB scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(inputHandler.parseColorSpec('#000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('#fff'), [240, 240, 240]);
assert.deepEqual(inputHandler.parseColorSpec('#123'), [16, 32, 48]);
// 8 bit
assert.deepEqual(inputHandler.parseColorSpec('#000000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('#ffffff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('#112233'), [17, 34, 51]);
// 12 bit
assert.deepEqual(inputHandler.parseColorSpec('#000000000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('#fffffffff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('#111222333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(inputHandler.parseColorSpec('#000000000000'), [0, 0, 0]);
assert.deepEqual(inputHandler.parseColorSpec('#ffffffffffff'), [255, 255, 255]);
assert.deepEqual(inputHandler.parseColorSpec('#111122223333'), [17, 34, 51]);
});
it('supports upper case', () => {
assert.deepEqual(inputHandler.parseColorSpec('RGB:0/A/F'), [0, 170, 255]);
assert.deepEqual(inputHandler.parseColorSpec('#FFF'), [240, 240, 240]);
});
it('does not parse illegal combinations', () => {
// shifting bit width
assert.equal(inputHandler.parseColorSpec('rgb:0/11/222'), undefined);
// unsupported scheme
assert.equal(inputHandler.parseColorSpec('rgbi:00/11/22'), undefined);
// broken # specifier
assert.equal(inputHandler.parseColorSpec('#aabbbcc'), undefined);
// out of range
assert.equal(inputHandler.parseColorSpec('#aabbgg'), undefined);
assert.equal(inputHandler.parseColorSpec('rgb:aa/bb/gg'), undefined);
});
});
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!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] });
assert.deepEqual(event![0], { index: 19, color: [0xa1, 0xb2, 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'));
assert.equal(inputHandler.parseAnsiColorChange('17;rgb:a/b/c').length, 0);
assert.equal(inputHandler.parseAnsiColorChange('17;rgb:#aabbcc').length, 0);
assert.equal(inputHandler.parseAnsiColorChange('17;rgba:aa/bb/cc').length, 0);
assert.equal(inputHandler.parseAnsiColorChange('rgb:aa/bb/cc').length, 0);
});
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!.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] });
assert.equal(event.length, 3);
assert.deepEqual(event[0], { index: 19, color: [0xa1, 0xb2, 0xc3] });
assert.deepEqual(event[1], { index: 17, color: [0x00, 0x11, 0x22] });
assert.deepEqual(event[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!.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] });
assert.equal(event.length, 2);
assert.deepEqual(event[0], { index: 19, color: [0xa1, 0xb2, 0xc3] });
assert.deepEqual(event[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!.requests.length, 1);
assert.deepEqual(event!.requests[0], { index: 19, color: [0xa1, 0xb2, 0xc3] });
assert.equal(event.length, 1);
assert.deepEqual(event[0], { index: 19, color: [0xa1, 0xb2, 0xc3] });
});
it('4: should fire event on Ansi color change', async () => {
return new Promise(async r => {
inputHandler.onColor(e => {
assert.isNotNull(e);
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] });
assert.deepEqual(e[0], { index: 17, color: [0x1a, 0x2b, 0x3c] });
assert.deepEqual(e[1], { index: 12, color: [0x11, 0x22, 0x33] });
r();
});
await inputHandler.parseP('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\');
});
});
it('10: should create appropriate events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single foreground query --> color undefined
await inputHandler.parseP('\x1b]10;?\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND }]]);
stack.length = 0;
// OSC with multiple values maps to OSC 10 & OSC 11
await inputHandler.parseP('\x1b]10;?;?;?;?\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND }], [{ index: ColorIndex.BACKGROUND }]]);
stack.length = 0;
// set foreground color events
await inputHandler.parseP('\x1b]10;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]10;#aabbcc\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set FG and BG at once
await inputHandler.parseP('\x1b]10;rgb:aa/bb/cc;#001122\x07');
assert.deepEqual(stack, [
[{ index: ColorIndex.FOREGROUND, color: [170, 187, 204] }],
[{ index: ColorIndex.BACKGROUND, color: [0, 17, 34] }],
]);
});
it('11: should create appropriate events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single foreground query --> color undefined
await inputHandler.parseP('\x1b]11;?\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND }]]);
stack.length = 0;
// OSC 11 with multiple values creates only one BG event
await inputHandler.parseP('\x1b]11;?;?;?;?\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND }]]);
stack.length = 0;
// set background color events
await inputHandler.parseP('\x1b]11;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]11;#aabbcc\x07');
assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND, color: [170, 187, 204] }]]);
});
});
// issue #3362 and #2979
+24 -72
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -21,6 +21,7 @@ import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowSe
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
import { parseColor } from 'common/input/XParseColor';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -403,9 +404,9 @@ export class InputHandler extends Disposable implements IInputHandler {
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 10 - Change VT100 text foreground color to Pt.
this._parser.registerOscHandler(10, new OscHandler(data => this.queryOrSetFgColor(data)));
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
// 11 - Change VT100 text background color to Pt.
this._parser.registerOscHandler(11, new OscHandler(data => this.queryOrSetBgColor(data)));
this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));
// 12 - Change text cursor color to Pt.
// 13 - Change mouse foreground color to Pt.
// 14 - Change mouse background color to Pt.
@@ -2841,14 +2842,13 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
protected _parseAnsiColorChange(data: string): IColorEvent | null {
const result: IColorEvent = { requests: [] };
protected _parseAnsiColorChange(data: string): IColorEvent {
const result: IColorEvent = [];
// 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.requests.push({
result.push({
index: parseInt(match[1]),
color: [
parseInt(match[2], 16),
@@ -2857,58 +2857,9 @@ export class InputHandler extends Disposable implements IInputHandler {
]
});
}
if (result.requests.length === 0) {
return null;
}
return result;
}
/**
* Parse color spec to RGB values (8 bit per channel).
* See `man xparsecolor` for details about certain format specifications.
*
* Supported formats:
* - rgb:<red>/<green>/<blue> with <red>, <green>, <blue> in h | hh | hhh | hhhh
* - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB
*
* All other formats like rgbi: or device-independent string specifications
* with float numbering are not supported.
*/
protected _parseColorSpec(data: string): [number, number, number] | undefined {
if (!data) return;
// also handle uppercases
let low = data.toLowerCase();
if (low.indexOf('rgb:') === 0) {
// 'rgb:' specifier
low = low.slice(4);
const rex = /^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/;
const m = rex.exec(low);
if (m) {
const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;
return [
Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),
Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),
Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)
];
}
} else if (low.indexOf('#') === 0) {
// '#' specifier
low = low.slice(1);
const rex = /^[\da-f]+$/;
if (rex.exec(low) && [3, 6, 9, 12].includes(low.length)) {
const adv = low.length / 3;
const result: [number, number, number] = [0, 0, 0];
for (let i = 0; i < 3; ++i) {
const c = parseInt(low.slice(adv * i, adv * i + adv), 16);
result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;
}
return result;
}
}
}
/**
* OSC 4; <num> ; <text> ST (set ANSI color <num> to <text>)
*
@@ -2918,7 +2869,7 @@ export class InputHandler extends Disposable implements IInputHandler {
*/
public setAnsiColor(data: string): boolean {
const event = this._parseAnsiColorChange(data);
if (event) {
if (event.length) {
this._onColor.fire(event);
}
else {
@@ -2940,6 +2891,8 @@ export class InputHandler extends Disposable implements IInputHandler {
* - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`
* - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`
*
* **Note:** X11 named colors are currently unsupported.
*
* If `Pt` contains `?` instead of a color specification, the terminal
* returns a sequence with the current default foreground color
* (use that sequence to restore the color after changes).
@@ -2947,21 +2900,20 @@ export class InputHandler extends Disposable implements IInputHandler {
* **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.
* Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.
*/
public queryOrSetFgColor(data: string): boolean {
// note: data may contain multiple ? or color names separated with ;
// Multiple values will map through to OSC 10 - 19, but we only support 10 and 11 currently,
// thus truncate to max. 2 entries.
const slots = data.split(';').slice(0, 2);
public setOrReportFgColor(data: string): boolean {
// Note: data may contain multiple values separated with ; mapping to OSC 10 - 19
const slots = data.split(';');
if (slots[0] === '?') {
this._onColor.fire({ requests: [{ index: 256, color: '?' }] });
this._onColor.fire([{ index: ColorIndex.FOREGROUND }]);
} else {
const color = this._parseColorSpec(slots[0]);
const color = parseColor(slots[0]);
if (color) {
this._onColor.fire({ requests: [{ index: 256, color }] });
this._onColor.fire([{ index: ColorIndex.FOREGROUND, color }]);
}
}
if (slots.length === 2) {
this.queryOrSetBgColor(slots[1]);
// forward second slot to OSC 11 (higher slots are not supported)
if (slots.length > 1) {
this.setOrReportBgColor(slots[1]);
}
return true;
}
@@ -2971,14 +2923,14 @@ export class InputHandler extends Disposable implements IInputHandler {
*
* @vt: #Y OSC 11 "Set or query default background color" "OSC 11 ; Pt BEL" "Same as OSC 10, but for default background."
*/
public queryOrSetBgColor(data: string): boolean {
const slots = data.split(';').slice(0, 1);
public setOrReportBgColor(data: string): boolean {
const slots = data.split(';');
if (slots[0] === '?') {
this._onColor.fire({ requests: [{ index: 257, color: '?' }] });
this._onColor.fire([{ index: ColorIndex.BACKGROUND }]);
} else {
const color = this._parseColorSpec(slots[0]);
const color = parseColor(slots[0]);
if (color) {
this._onColor.fire({ requests: [{ index: 257, color }] });
this._onColor.fire([{ index: ColorIndex.BACKGROUND, color }]);
}
}
return true;
+12 -9
View File
@@ -348,17 +348,20 @@ export interface IWindowOptions {
setWinLines?: boolean;
}
export interface IColorEventColor {
index: number;
color: [number, number, number] | '?';
// color events from common, used for OSC 4/10/11
export const enum ColorIndex {
FOREGROUND = 256,
BACKGROUND = 257
}
export interface IColorReportRequest {
index: ColorIndex;
color?: IColorRGB;
}
export interface IColorSetRequest extends IColorReportRequest {
color: IColorRGB;
}
export type IColorEvent = (IColorReportRequest | IColorSetRequest)[];
/**
* Event fired for OSC 4 command - to change ANSI color based on its index.
*/
export interface IColorEvent {
requests: IColorEventColor[];
}
/**
* Calls the parser and handles actions generated by the parser.
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { parseColor, toRgbString } from 'common/input/XParseColor';
describe('XParseColor', () => {
describe('parseColor', () => {
it('rgb:<r>/<g>/<b> scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(parseColor('rgb:0/0/0'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:f/f/f'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:1/2/3'), [17, 34, 51]);
// 8 bit
assert.deepEqual(parseColor('rgb:00/00/00'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:ff/ff/ff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:11/22/33'), [17, 34, 51]);
// 12 bit
assert.deepEqual(parseColor('rgb:000/000/000'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:fff/fff/fff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:111/222/333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(parseColor('rgb:0000/0000/0000'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:ffff/ffff/ffff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:1111/2222/3333'), [17, 34, 51]);
});
it('#RGB scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(parseColor('#000'), [0, 0, 0]);
assert.deepEqual(parseColor('#fff'), [240, 240, 240]);
assert.deepEqual(parseColor('#123'), [16, 32, 48]);
// 8 bit
assert.deepEqual(parseColor('#000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#ffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#112233'), [17, 34, 51]);
// 12 bit
assert.deepEqual(parseColor('#000000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#fffffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#111222333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(parseColor('#000000000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#ffffffffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#111122223333'), [17, 34, 51]);
});
it('supports upper case', () => {
assert.deepEqual(parseColor('RGB:0/A/F'), [0, 170, 255]);
assert.deepEqual(parseColor('#FFF'), [240, 240, 240]);
});
it('does not parse illegal combinations', () => {
// shifting bit width
assert.equal(parseColor('rgb:0/11/222'), undefined);
// unsupported scheme
assert.equal(parseColor('rgbi:00/11/22'), undefined);
// broken # specifier
assert.equal(parseColor('#aabbbcc'), undefined);
// out of range
assert.equal(parseColor('#aabbgg'), undefined);
assert.equal(parseColor('rgb:aa/bb/gg'), undefined);
});
});
describe('toXColorRgb', () => {
it('rgb:<r>/<g>/<b> scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.equal(toRgbString(parseColor('rgb:0/0/0')!, 4), 'rgb:0/0/0');
assert.equal(toRgbString(parseColor('rgb:f/f/f')!, 4), 'rgb:f/f/f');
assert.equal(toRgbString(parseColor('rgb:1/2/3')!, 4), 'rgb:1/2/3');
// 8 bit
assert.equal(toRgbString(parseColor('rgb:00/00/00')!, 8), 'rgb:00/00/00');
assert.equal(toRgbString(parseColor('rgb:ff/ff/ff')!, 8), 'rgb:ff/ff/ff');
assert.equal(toRgbString(parseColor('rgb:11/22/33')!, 8), 'rgb:11/22/33');
// 12 bit
assert.equal(toRgbString(parseColor('rgb:000/000/000')!, 12), 'rgb:000/000/000');
assert.equal(toRgbString(parseColor('rgb:fff/fff/fff')!, 12), 'rgb:fff/fff/fff');
assert.equal(toRgbString(parseColor('rgb:111/222/333')!, 12), 'rgb:111/222/333');
// 16 bit
assert.equal(toRgbString(parseColor('rgb:0000/0000/0000')!, 16), 'rgb:0000/0000/0000');
assert.equal(toRgbString(parseColor('rgb:ffff/ffff/ffff')!, 16), 'rgb:ffff/ffff/ffff');
assert.equal(toRgbString(parseColor('rgb:1111/2222/3333')!, 16), 'rgb:1111/2222/3333');
});
it('defaults to 16 bit output', () => {
assert.equal(toRgbString(parseColor('rgb:1/2/3')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:11/22/33')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:111/222/333')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:123/123/123')!), 'rgb:1212/1212/1212');
});
it('reduces colors to 8 bit resolution', () => {
assert.equal(toRgbString(parseColor('rgb:123/123/123')!, 12), 'rgb:121/121/121');
assert.equal(toRgbString(parseColor('rgb:1234/1234/1234')!, 16), 'rgb:1212/1212/1212');
});
});
});
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)
const RGB_REX = /^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/;
// '#...' rule - matching any hex digits
const HASH_REX = /^[\da-f]+$/;
/**
* Parse color spec to RGB values (8 bit per channel).
* See `man xparsecolor` for details about certain format specifications.
*
* Supported formats:
* - rgb:<red>/<green>/<blue> with <red>, <green>, <blue> in h | hh | hhh | hhhh
* - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB
*
* All other formats like rgbi: or device-independent string specifications
* with float numbering are not supported.
*/
export function parseColor(data: string): [number, number, number] | undefined {
if (!data) return;
// also handle uppercases
let low = data.toLowerCase();
if (low.indexOf('rgb:') === 0) {
// 'rgb:' specifier
low = low.slice(4);
const m = RGB_REX.exec(low);
if (m) {
const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;
return [
Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),
Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),
Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)
];
}
} else if (low.indexOf('#') === 0) {
// '#' specifier
low = low.slice(1);
if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {
const adv = low.length / 3;
const result: [number, number, number] = [0, 0, 0];
for (let i = 0; i < 3; ++i) {
const c = parseInt(low.slice(adv * i, adv * i + adv), 16);
result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;
}
return result;
}
}
// FIXME: Once #3530 is resolved, implement named colors.
}
// pad hex output to requested bit width
function pad(n: number, bits: number): string {
const s = n.toString(16);
const s2 = s.length < 2 ? '0' + s : s;
switch (bits) {
case 4:
return s[0];
case 8:
return s2;
case 12:
return (s2 + s2).slice(0, 3);
default:
return s2 + s2;
}
}
/**
* Convert a given color to rgb:../../.. string of `bits` depth.
*/
export function toRgbString(color: [number, number, number], bits: number = 16): string {
const [r, g, b] = color;
return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;
}