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