Merge pull request #3524 from jerch/osc_colors

OSC 4/10/11/12 color handling
This commit is contained in:
Daniel Imms
2021-12-22 09:10:37 -08:00
committed by GitHub
9 changed files with 837 additions and 172 deletions
+8
View File
@@ -4,6 +4,7 @@
*/
import { IColor } from 'browser/Types';
import { IColorRGB } from 'common/Types';
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
@@ -17,6 +18,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 +84,10 @@ export namespace color {
rgba: channels.toRgba(r, g, b, a)
};
}
export function toColorRGB(color: IColor): IColorRGB {
return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];
}
}
/**
@@ -197,6 +204,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];
}
+45
View File
@@ -7,6 +7,16 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/T
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'browser/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorIndex } from 'common/Types';
interface IRestoreColorSet {
foreground: IColor;
background: IColor;
cursor: IColor;
ansi: IColor[];
}
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
const DEFAULT_BACKGROUND = css.toColor('#000000');
@@ -73,6 +83,7 @@ export class ColorManager implements IColorManager {
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
private _contrastCache: IColorContrastCache;
private _restoreColors!: IRestoreColorSet;
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
@@ -96,6 +107,7 @@ export class ColorManager implements IColorManager {
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
this._updateRestoreColors();
}
public onOptionsChange(key: string): void {
@@ -142,6 +154,39 @@ export class ColorManager implements IColorManager {
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
// Clear our the cache
this._contrastCache.clear();
this._updateRestoreColors();
}
public restoreColor(slot?: ColorIndex): void {
// unset slot restores all ansi colors
if (slot === undefined) {
for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
this.colors.ansi[i] = this._restoreColors.ansi[i];
}
return;
}
switch (slot) {
case ColorIndex.FOREGROUND:
this.colors.foreground = this._restoreColors.foreground;
break;
case ColorIndex.BACKGROUND:
this.colors.background = this._restoreColors.background;
break;
case ColorIndex.CURSOR:
this.colors.cursor = this._restoreColors.cursor;
break;
default:
this.colors.ansi[slot] = this._restoreColors.ansi[slot];
}
}
private _updateRestoreColors(): void {
this._restoreColors = {
foreground: this.colors.foreground,
background: this.colors.background,
cursor: this.colors.cursor,
ansi: [...this.colors.ansi]
};
}
private _parseColor(
+52 -14
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, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
@@ -52,9 +52,9 @@ import { MouseService } from 'browser/services/MouseService';
import { Linkifier2 } from 'browser/Linkifier2';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { CoreTerminal } from 'common/CoreTerminal';
import { rgba } from 'browser/Color';
import { color, rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { ITerminalOptions } from 'common/services/Services';
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;
@@ -164,7 +164,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));
@@ -174,17 +174,55 @@ 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; }
for (const ansiColor of event.colors) {
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
/**
* Handle color event from inputhandler for OSC 4|104 | 10|110 | 11|111 | 12|112.
* An event from OSC 4|104 may contain multiple set or report requests, and multiple
* or none restore requests (resetting all),
* while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request.
*/
private _handleColorEvent(event: IColorEvent): void {
if (!this._colorManager) return;
for (const req of event) {
let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
let ident = '';
switch (req.index) {
case ColorIndex.FOREGROUND: // OSC 10 | 110
acc = 'foreground';
ident = '10';
break;
case ColorIndex.BACKGROUND: // OSC 11 | 111
acc = 'background';
ident = '11';
break;
case ColorIndex.CURSOR: // OSC 12 | 112
acc = 'cursor';
ident = '12';
break;
default: // OSC 4 | 104
// we can skip the [0..255] range check here (already done in inputhandler)
acc = 'ansi';
ident = '4;' + req.index;
}
if (acc) {
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
? this._colorManager.colors.ansi[req.index]
: this._colorManager.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
break;
case ColorRequestType.RESTORE:
this._colorManager.restoreColor(req.index);
break;
}
}
}
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 {
+134 -49
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { InputHandler } from 'common/InputHandler';
import { IBufferLine, IAttributeData, IAnsiColorChangeEvent } from 'common/Types';
import { IBufferLine, IAttributeData, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
@@ -17,7 +17,7 @@ import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
import { BufferService } from 'common/services/BufferService';
import { CoreService } from 'common/services/CoreService';
import { OscHandler } from 'common/parser/OscParser';
function getCursor(bufferService: IBufferService): number[] {
return [
@@ -41,7 +41,6 @@ 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); }
/**
* Promise based parse call to await the full resolve of given input data.
@@ -1915,58 +1914,144 @@ describe('InputHandler', () => {
});
});
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: query color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;?;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }, { type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
});
it('4: set color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 123, color: [170, 187, 204] }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('4: should ignore invalid values', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;45;rgb:1/22/333;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('104: restore events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]104;0\x07\x1b]104;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }], [{ type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// multiple in one command
await inputHandler.parseP('\x1b]104;0;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }, { type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// full ANSI table restore
await inputHandler.parseP('\x1b]104\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE}]]);
});
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('10: FG set & query 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, [[{ type: ColorRequestType.REPORT, index: ColorIndex.FOREGROUND }]]);
stack.length = 0;
// OSC with multiple values maps to OSC 10 & OSC 11 & OSC 12
await inputHandler.parseP('\x1b]10;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.FOREGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set foreground color events
await inputHandler.parseP('\x1b]10;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]10;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set FG, BG and cursor color at once
await inputHandler.parseP('\x1b]10;rgb:aa/bb/cc;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [170, 187, 204] }],
[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
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('110: restore FG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]110\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]]);
});
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('11: BG set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single background query --> color undefined
await inputHandler.parseP('\x1b]11;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }]]);
stack.length = 0;
// OSC 11 with multiple values creates only BG and cursor event
await inputHandler.parseP('\x1b]11;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set background color events
await inputHandler.parseP('\x1b]11;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]11;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set BG and cursor color at once
await inputHandler.parseP('\x1b]11;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
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('111: restore BG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]111\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]]);
});
it('4: should fire event on Ansi color change', async () => {
return new Promise(async r => {
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 });
r();
});
await inputHandler.parseP('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\');
});
it('12: cursor color set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single cursor query --> color undefined
await inputHandler.parseP('\x1b]12;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// OSC 12 with multiple values creates only cursor event
await inputHandler.parseP('\x1b]12;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set cursor color events
await inputHandler.parseP('\x1b]12;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]12;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [170, 187, 204] }]]);
});
it('112: restore cursor color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]112\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]]);
});
});
+165 -36
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } 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`.
@@ -262,8 +263,8 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onAnsiColorChange = new EventEmitter<IAnsiColorChangeEvent>();
public get onAnsiColorChange(): IEvent<IAnsiColorChangeEvent> { return this._onAnsiColorChange.event; }
private _onColor = new EventEmitter<IColorEvent>();
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
private _parseStack: IParseStack = {
paused: false,
@@ -398,13 +399,16 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
this._parser.registerOscHandler(4, new OscHandler(data => this.setAnsiColor(data)));
this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(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)
// 10 - Change VT100 text foreground color to Pt.
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.setOrReportBgColor(data)));
// 12 - Change text cursor color to Pt.
this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));
// 13 - Change mouse foreground color to Pt.
// 14 - Change mouse background color to Pt.
// 15 - Change Tektronix foreground color to Pt.
@@ -417,11 +421,15 @@ export class InputHandler extends Disposable implements IInputHandler {
// 51 - reserved for Emacs shell.
// 52 - Manipulate Selection Data.
// 104 ; c - Reset Color Number c.
this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));
// 105 ; c - Reset Special Color Number c.
// 106 ; c; f - Enable/disable Special Color Number c.
// 110 - Reset VT100 text foreground color.
this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));
// 111 - Reset VT100 text background color.
this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));
// 112 - Reset text cursor color.
this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));
// 113 - Reset mouse foreground color.
// 114 - Reset mouse background color.
// 115 - Reset Tektronix foreground color.
@@ -1977,7 +1985,7 @@ export class InputHandler extends Disposable implements IInputHandler {
break;
case 1049: // alt screen buffer cursor
this.saveCursor();
// FALL-THROUGH
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());
@@ -2197,7 +2205,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this.restoreCursor();
break;
case 1049: // alt screen buffer cursor
// FALL-THROUGH
// FALL-THROUGH
case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first
// Ensure the selection manager has the correct buffer
@@ -2840,46 +2848,167 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null {
const result: IAnsiColorChangeEvent = { colors: [] };
// 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)
});
}
if (result.colors.length === 0) {
return null;
}
return result;
}
/**
* OSC 4; <num> ; <text> ST (set ANSI color <num> to <text>)
*
* @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.
* `c` is the color index between 0 and 255. The color format of `spec` is derived from `XParseColor` (see OSC 10 for supported formats).
* There may be multipe `c ; spec` pairs present in the same instruction.
* If `spec` contains `?` the terminal returns a sequence with the currently set color.
*/
public setAnsiColor(data: string): boolean {
const event = this._parseAnsiColorChange(data);
if (event) {
this._onAnsiColorChange.fire(event);
public setOrReportIndexedColor(data: string): boolean {
const event: IColorEvent = [];
const slots = data.split(';');
while (slots.length > 1) {
const idx = slots.shift() as string;
const spec = slots.shift() as string;
if (/^\d+$/.exec(idx)) {
const index = parseInt(idx);
if (0 <= index && index < 256) {
if (spec === '?') {
event.push({ type: ColorRequestType.REPORT, index });
} else {
const color = parseColor(spec);
if (color) {
event.push({ type: ColorRequestType.SET, index, color });
}
}
}
}
}
else {
this._logService.warn(`Expected format <num>;rgb:<rr>/<gg>/<bb> but got data: ${data}`);
if (event.length) {
this._onColor.fire(event);
}
return true;
}
// special colors - OSC 10 | 11 | 12
private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR];
/**
* Apply colors requests for special colors in OSC 10 | 11 | 12.
* Since these commands are stacking from multiple parameters,
* we handle them in a loop with an entry offset to `_specialColors`.
*/
private _setOrReportSpecialColor(data: string, offset: number): boolean {
const slots = data.split(';');
for (let i = 0; i < slots.length; ++i, ++offset) {
if (offset >= this._specialColors.length) break;
if (slots[i] === '?') {
this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);
} else {
const color = parseColor(slots[i]);
if (color) {
this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);
}
}
}
return true;
}
/**
* OSC 10 ; <xcolor name>|<?> ST - set or query default foreground color
*
* @vt: #Y OSC 10 "Set or query default foreground color" "OSC 10 ; Pt BEL" "Set or query default foreground color."
* To set the color, the following color specification formats are supported:
* - `rgb:<red>/<green>/<blue>` for `<red>, <green>, <blue>` in `h | hh | hhh | hhhh`, where
* `h` is a single hexadecimal digit (case insignificant). The different widths scale
* from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).
* - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`
* - `#RRGGBB` - 8 bits per channel
* - `#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).
*
* **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 setOrReportFgColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 0);
}
/**
* OSC 11 ; <xcolor name>|<?> ST - set or query default background color
*
* @vt: #Y OSC 11 "Set or query default background color" "OSC 11 ; Pt BEL" "Same as OSC 10, but for default background."
*/
public setOrReportBgColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 1);
}
/**
* OSC 12 ; <xcolor name>|<?> ST - set or query default cursor color
*
* @vt: #Y OSC 12 "Set or query default cursor color" "OSC 12 ; Pt BEL" "Same as OSC 10, but for default cursor color."
*/
public setOrReportCursorColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 2);
}
/**
* OSC 104 ; <num> ST - restore ANSI color <num>
*
* @vt: #Y OSC 104 "Reset ANSI color" "OSC 104 ; c BEL" "Reset color number `c` to themed color."
* `c` is the color index between 0 and 255. This function restores the default color for `c` as
* specified by the loaded theme. Any number of `c` parameters may be given.
* If no parameters are given, the entire indexed color table will be reset.
*/
public restoreIndexedColor(data: string): boolean {
if (!data) {
this._onColor.fire([{ type: ColorRequestType.RESTORE }]);
return true;
}
const event: IColorEvent = [];
const slots = data.split(';');
for (let i = 0; i < slots.length; ++i) {
if (/^\d+$/.exec(slots[i])) {
const index = parseInt(slots[i]);
if (0 <= index && index < 256) {
event.push({ type: ColorRequestType.RESTORE, index });
}
}
}
if (event.length) {
this._onColor.fire(event);
}
return true;
}
/**
* OSC 110 ST - restore default foreground color
*
* @vt: #Y OSC 110 "Restore default foreground color" "OSC 110 BEL" "Restore default foreground to themed color."
*/
public restoreFgColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]);
return true;
}
/**
* OSC 111 ST - restore default background color
*
* @vt: #Y OSC 111 "Restore default background color" "OSC 111 BEL" "Restore default background to themed color."
*/
public restoreBgColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]);
return true;
}
/**
* OSC 112 ST - restore default cursor color
*
* @vt: #Y OSC 112 "Restore default cursor color" "OSC 112 BEL" "Restore default cursor to themed color."
*/
public restoreCursorColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]);
return true;
}
/**
* ESC E
* C1.NEL
+94 -72
View File
@@ -349,19 +349,32 @@ export interface IWindowOptions {
setWinLines?: boolean;
}
export interface IAnsiColorChangeEventColor {
colorIndex: number;
red: number;
green: number;
blue: number;
// color events from common, used for OSC 4/10/11/12 and 104/110/111/112
export const enum ColorRequestType {
REPORT = 0,
SET = 1,
RESTORE = 2
}
export const enum ColorIndex {
FOREGROUND = 256,
BACKGROUND = 257,
CURSOR = 258
}
export interface IColorReportRequest {
type: ColorRequestType.REPORT;
index: ColorIndex;
}
export interface IColorSetRequest {
type: ColorRequestType.SET;
index: ColorIndex;
color: IColorRGB;
}
export interface IColorRestoreRequest {
type: ColorRequestType.RESTORE;
index?: ColorIndex;
}
export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestoreRequest)[];
/**
* 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.
@@ -376,80 +389,89 @@ export interface IInputHandler {
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** C0 BEL */ bell(): boolean;
/** C0 LF */ lineFeed(): boolean;
/** C0 CR */ carriageReturn(): boolean;
/** C0 BS */ backspace(): boolean;
/** C0 HT */ tab(): boolean;
/** C0 SO */ shiftOut(): boolean;
/** C0 SI */ shiftIn(): boolean;
/** CSI @ */ insertChars(params: IParams): boolean;
/** CSI SP @ */ scrollLeft(params: IParams): boolean;
/** CSI A */ cursorUp(params: IParams): boolean;
/** CSI SP A */ scrollRight(params: IParams): boolean;
/** CSI B */ cursorDown(params: IParams): boolean;
/** CSI C */ cursorForward(params: IParams): boolean;
/** CSI D */ cursorBackward(params: IParams): boolean;
/** CSI E */ cursorNextLine(params: IParams): boolean;
/** CSI F */ cursorPrecedingLine(params: IParams): boolean;
/** CSI G */ cursorCharAbsolute(params: IParams): boolean;
/** CSI H */ cursorPosition(params: IParams): boolean;
/** CSI I */ cursorForwardTab(params: IParams): boolean;
/** CSI J */ eraseInDisplay(params: IParams): boolean;
/** CSI K */ eraseInLine(params: IParams): boolean;
/** CSI L */ insertLines(params: IParams): boolean;
/** CSI M */ deleteLines(params: IParams): boolean;
/** CSI P */ deleteChars(params: IParams): boolean;
/** CSI S */ scrollUp(params: IParams): boolean;
/** CSI T */ scrollDown(params: IParams, collect?: string): boolean;
/** CSI X */ eraseChars(params: IParams): boolean;
/** CSI Z */ cursorBackwardTab(params: IParams): boolean;
/** CSI ` */ charPosAbsolute(params: IParams): boolean;
/** CSI a */ hPositionRelative(params: IParams): boolean;
/** CSI b */ repeatPrecedingCharacter(params: IParams): boolean;
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): boolean;
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): boolean;
/** CSI d */ linePosAbsolute(params: IParams): boolean;
/** CSI e */ vPositionRelative(params: IParams): boolean;
/** CSI f */ hVPosition(params: IParams): boolean;
/** CSI g */ tabClear(params: IParams): boolean;
/** CSI h */ setMode(params: IParams, collect?: string): boolean;
/** CSI l */ resetMode(params: IParams, collect?: string): boolean;
/** CSI m */ charAttributes(params: IParams): boolean;
/** CSI n */ deviceStatus(params: IParams, collect?: string): boolean;
/** CSI p */ softReset(params: IParams, collect?: string): boolean;
/** CSI q */ setCursorStyle(params: IParams, collect?: string): boolean;
/** CSI r */ setScrollRegion(params: IParams, collect?: string): boolean;
/** CSI s */ saveCursor(params: IParams): boolean;
/** CSI u */ restoreCursor(params: IParams): boolean;
/** CSI ' } */ insertColumns(params: IParams): boolean;
/** CSI ' ~ */ deleteColumns(params: IParams): boolean;
/** CSI @ */ insertChars(params: IParams): void;
/** CSI SP @ */ scrollLeft(params: IParams): void;
/** CSI A */ cursorUp(params: IParams): void;
/** CSI SP A */ scrollRight(params: IParams): void;
/** CSI B */ cursorDown(params: IParams): void;
/** CSI C */ cursorForward(params: IParams): void;
/** CSI D */ cursorBackward(params: IParams): void;
/** CSI E */ cursorNextLine(params: IParams): void;
/** CSI F */ cursorPrecedingLine(params: IParams): void;
/** CSI G */ cursorCharAbsolute(params: IParams): void;
/** CSI H */ cursorPosition(params: IParams): void;
/** CSI I */ cursorForwardTab(params: IParams): void;
/** CSI J */ eraseInDisplay(params: IParams): void;
/** CSI K */ eraseInLine(params: IParams): void;
/** CSI L */ insertLines(params: IParams): void;
/** CSI M */ deleteLines(params: IParams): void;
/** CSI P */ deleteChars(params: IParams): void;
/** CSI S */ scrollUp(params: IParams): void;
/** CSI T */ scrollDown(params: IParams, collect?: string): void;
/** CSI X */ eraseChars(params: IParams): void;
/** CSI Z */ cursorBackwardTab(params: IParams): void;
/** CSI ` */ charPosAbsolute(params: IParams): void;
/** CSI a */ hPositionRelative(params: IParams): void;
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): void;
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void;
/** CSI d */ linePosAbsolute(params: IParams): void;
/** CSI e */ vPositionRelative(params: IParams): void;
/** CSI f */ hVPosition(params: IParams): void;
/** CSI g */ tabClear(params: IParams): void;
/** CSI h */ setMode(params: IParams, collect?: string): void;
/** CSI l */ resetMode(params: IParams, collect?: string): void;
/** CSI m */ charAttributes(params: IParams): void;
/** CSI n */ deviceStatus(params: IParams, collect?: string): void;
/** CSI p */ softReset(params: IParams, collect?: string): void;
/** CSI q */ setCursorStyle(params: IParams, collect?: string): void;
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
/** CSI s */ saveCursor(params: IParams): void;
/** CSI u */ restoreCursor(params: IParams): void;
/** CSI ' } */ insertColumns(params: IParams): void;
/** 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;
OSC 2 */ setTitle(data: string): boolean;
/** OSC 4 */ setOrReportIndexedColor(data: string): boolean;
/** OSC 10 */ setOrReportFgColor(data: string): boolean;
/** OSC 11 */ setOrReportBgColor(data: string): boolean;
/** OSC 12 */ setOrReportCursorColor(data: string): boolean;
/** OSC 104 */ restoreIndexedColor(data: string): boolean;
/** OSC 110 */ restoreFgColor(data: string): boolean;
/** OSC 111 */ restoreBgColor(data: string): boolean;
/** OSC 112 */ restoreCursorColor(data: string): boolean;
/** ESC E */ nextLine(): boolean;
/** ESC = */ keypadApplicationMode(): boolean;
/** ESC > */ keypadNumericMode(): boolean;
/** ESC % G
ESC % @ */ selectDefaultCharset(): void;
ESC % @ */ selectDefaultCharset(): boolean;
/** ESC ( C
ESC ) C
ESC * C
ESC + C
ESC - C
ESC . C
ESC / C */ selectCharset(collectAndFlag: string): void;
/** ESC D */ index(): void;
/** ESC H */ tabSet(): void;
/** ESC M */ reverseIndex(): void;
/** ESC c */ fullReset(): void;
ESC / C */ selectCharset(collectAndFlag: string): boolean;
/** ESC D */ index(): boolean;
/** ESC H */ tabSet(): boolean;
/** ESC M */ reverseIndex(): boolean;
/** ESC c */ fullReset(): boolean;
/** ESC n
ESC o
ESC |
ESC }
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
ESC ~ */ setgLevel(level: number): boolean;
/** ESC # 8 */ screenAlignmentPattern(): boolean;
}
interface IParseStack {
+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');
});
});
});
+80
View File
@@ -0,0 +1,80 @@
/**
* 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;
}
}
// Named colors are currently not supported due to the large addition to the xterm.js bundle size
// they would add. In order to support named colors, we would need some way of optionally loading
// additional payloads so startup/download time is not bloated (see #3530).
}
// 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)}`;
}
+166 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { pollFor, openTerminal, getBrowserType, launchBrowser } from './TestUtils';
import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils';
import { Browser, Page } from 'playwright';
import { IRenderDimensions } from 'browser/renderer/Types';
@@ -386,6 +386,171 @@ describe('InputHandler Integration Tests', function(): void {
});
});
describe('OSC', () => {
describe('OSC 4', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('query single color', async () => {
await writeSync(page, '\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07']);
await writeSync(page, '\x1b]4;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']);
});
it('query multiple colors', async () => {
await writeSync(page, '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']);
});
it('set & query single color', async () => {
await writeSync(page, '\x1b]4;0;?\x07');
const restore: string[] = await page.evaluate('window._recordedData');
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
// set new color & query
await writeSync(page, '\x1b]4;0;rgb:01/02/03\x07\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07']);
// restore should set old color
await writeSync(page, restore[0] + '\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07', restore[0]]);
});
it('query & set colors mixed', async () => {
await writeSync(page, '\x1b]4;0;?;77;?\x07');
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
// mixed call - change 0, query 43, change 77
await writeSync(page, '\x1b]4;0;rgb:01/02/03;43;?;77;#aabbcc\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x07']);
await page.evaluate('window._recordedData.length = 0;');
// query new values for 0 + 77
await writeSync(page, '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x07', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore old values for 0 + 77
await writeSync(page, restore[0] + restore[1] + '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
});
describe('OSC 4 & 104', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('change & restore single color', async () => {
// test for some random color slots
for (const i of [0, 43, 77, 255]) {
await writeSync(page, `\x1b]4;${i};?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await writeSync(page, `\x1b]4;${i};rgb:01/02/03\x07\x1b]4;${i};?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`]);
// restore slot color
await writeSync(page, `\x1b]104;${i}\x07\x1b]4;${i};?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`, restore[0]]);
await page.evaluate('window._recordedData.length = 0;');
}
});
it('restore multiple at once', async () => {
// change 3 random slots
await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`);
// restore specific slots
await writeSync(page, `\x1b]104;0;43;77\x07` + `\x1b]4;0;?;43;?;77;?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
it('restore full table', async () => {
// change 3 random slots
await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`);
// restore all
await writeSync(page, `\x1b]104\x07` + `\x1b]4;0;?;43;?;77;?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
});
describe('OSC 10 & 11 + 110 | 111 | 112', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('query FG color', async () => {
await writeSync(page, '\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('query BG color', async () => {
await writeSync(page, '\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']);
});
it('query FG & BG color in one call', async () => {
await writeSync(page, '\x1b]10;?;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07', '\x1b]11;rgb:0000/0000/0000\x07']);
});
it('set & query FG', async () => {
await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]10;#ffffff\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07', '\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('set & query BG', async () => {
await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]11;#000000\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07', '\x1b]11;rgb:0000/0000/0000\x07']);
});
it('set & query cursor color', async () => {
await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]12;#ffffff\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07', '\x1b]12;rgb:ffff/ffff/ffff\x07']);
});
it('set & query FG & BG color in one call', async () => {
await writeSync(page, '\x1b]10;#123456;rgb:aa/bb/cc\x07\x1b]10;?;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x07', '\x1b]11;rgb:aaaa/bbbb/cccc\x07']);
await writeSync(page, '\x1b]10;#ffffff;#000000\x07');
});
it('OSC 110: restore FG color', async () => {
await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]110\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('OSC 111: restore BG color', async () => {
await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]111\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']);
});
it('OSC 112: restore cursor color', async () => {
await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]112\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x07']);
});
});
});
describe('ESC', () => {
describe('DECRC: Save cursor, ESC 7', () => {
it('should save the absolute cursor position so resizing restores to the correct position', async () => {