diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 6a9dff94..a4e415af 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -217,13 +217,6 @@ export namespace rgba { rgba: channels.toRgba(r, g, b) }; } - - /** - * convert 0xRRGGBBAA to 0xAABBGGRR (32-bit representation on LE systems) - */ - export function toABGR32(rgba: number): number { - return ((rgba & 0xFF) << 24 | (rgba >>> 8 & 0xFF) << 16 | (rgba >>> 16 & 0xFF) << 8 | rgba >>> 24 & 0xFF) >>> 0; - } } export function toPaddedHex(c: number): string { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 0602f604..9e0d7f54 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -178,23 +178,44 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); } + /** + * Handle color event from inputhandler for OSC 4 | 10 | 11 | 12. + * An event from OSC 4 may contain multiple set or report requests, + * while OSC 10 | 11 | 12 create single requests. + */ 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 - 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}`); + acc = 'foreground'; + ident = '10'; 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}`); + acc = 'background'; + ident = '11'; + break; + case ColorIndex.CURSOR: // OSC 12 + acc = 'cursor'; + ident = '12'; 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}`); - } + // we can skip the [0..255] range check here (already done in inputhandler) + acc = 'ansi'; + ident = '4;' + req.index; + } + if (acc) { + if (req.color) { + if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); + else this._colorManager.colors[acc] = rgba.toColor(...req.color); + } else { + 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}`); + } } } this._renderService?.setColors(this._colorManager.colors); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index d63a9654..fe48cafc 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -1902,16 +1902,16 @@ describe('InputHandler', () => { stack.length = 0; }); - it('10: should create appropriate events', async () => { + 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, [[{ index: ColorIndex.FOREGROUND }]]); stack.length = 0; - // OSC with multiple values maps to OSC 10 & OSC 11 + // OSC with multiple values maps to OSC 10 & OSC 11 & OSC 12 await inputHandler.parseP('\x1b]10;?;?;?;?\x07'); - assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND }], [{ index: ColorIndex.BACKGROUND }]]); + assert.deepEqual(stack, [[{ index: ColorIndex.FOREGROUND }], [{ index: ColorIndex.BACKGROUND }], [{ index: ColorIndex.CURSOR }]]); stack.length = 0; // set foreground color events await inputHandler.parseP('\x1b]10;rgb:01/02/03\x07'); @@ -1920,23 +1920,24 @@ describe('InputHandler', () => { 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'); + // 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, [ [{ index: ColorIndex.FOREGROUND, color: [170, 187, 204] }], - [{ index: ColorIndex.BACKGROUND, color: [0, 17, 34] }] + [{ index: ColorIndex.BACKGROUND, color: [0, 17, 34] }], + [{ index: ColorIndex.CURSOR, color: [18, 52, 86] }] ]); }); - it('11: should create appropriate events', async () => { + it('11: BG set & query events', async () => { const stack: IColorEvent[] = []; inputHandler.onColor(ev => stack.push(ev)); - // single foreground query --> color undefined + // single background 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 + // OSC 11 with multiple values creates only BG and cursor event await inputHandler.parseP('\x1b]11;?;?;?;?\x07'); - assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND }]]); + assert.deepEqual(stack, [[{ index: ColorIndex.BACKGROUND }], [{ index: ColorIndex.CURSOR }]]); stack.length = 0; // set background color events await inputHandler.parseP('\x1b]11;rgb:01/02/03\x07'); @@ -1944,6 +1945,31 @@ describe('InputHandler', () => { stack.length = 0; await inputHandler.parseP('\x1b]11;#aabbcc\x07'); assert.deepEqual(stack, [[{ 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, [ + [{ index: ColorIndex.BACKGROUND, color: [0, 17, 34] }], + [{ index: ColorIndex.CURSOR, color: [18, 52, 86] }] + ]); + }); + 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, [[{ index: ColorIndex.CURSOR }]]); + stack.length = 0; + // OSC 12 with multiple values creates only cursor event + await inputHandler.parseP('\x1b]12;?;?;?;?\x07'); + assert.deepEqual(stack, [[{ index: ColorIndex.CURSOR }]]); + stack.length = 0; + // set cursor color events + await inputHandler.parseP('\x1b]12;rgb:01/02/03\x07'); + assert.deepEqual(stack, [[{ index: ColorIndex.CURSOR, color: [1, 2, 3] }]]); + stack.length = 0; + await inputHandler.parseP('\x1b]12;#aabbcc\x07'); + assert.deepEqual(stack, [[{ index: ColorIndex.CURSOR, color: [170, 187, 204] }]]); }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 00943601..5377bc03 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -408,6 +408,7 @@ export class InputHandler extends Disposable implements IInputHandler { // 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. @@ -2846,8 +2847,9 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC 4; ; ST (set ANSI color to ) * * @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`." - * `c` is the color index between 0 and 255. `spec` color format is 'rgb:hh/hh/hh' where `h` are hexadecimal digits. - * There may be multipe c ; spec elements present in the same instruction, e.g. 1;rgb:10/20/30;2;rgb:a0/b0/c0. + * `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 setOrReportIndexedColor(data: string): boolean { const event: IColorEvent = []; @@ -2856,12 +2858,15 @@ export class InputHandler extends Disposable implements IInputHandler { const idx = slots.shift() as string; const spec = slots.shift() as string; if (/^\d+$/.exec(idx)) { - if (spec === '?') { - event.push({ index: parseInt(idx) }); - } else { - const color = parseColor(spec); - if (color) { - event.push({ index: parseInt(idx), color }); + const index = parseInt(idx); + if (0 <= index && index < 256) { + if (spec === '?') { + event.push({ index }); + } else { + const color = parseColor(spec); + if (color) { + event.push({ index, color }); + } } } } @@ -2872,6 +2877,30 @@ export class InputHandler extends Disposable implements IInputHandler { 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([{ index: this._specialColors[offset] }]); + } else { + const color = parseColor(slots[i]); + if (color) { + this._onColor.fire([{ index: this._specialColors[offset], color }]); + } + } + } + return true; + } + /** * OSC 10 ; | ST - set or query default foreground color * @@ -2895,21 +2924,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries. */ 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([{ index: ColorIndex.FOREGROUND }]); - } else { - const color = parseColor(slots[0]); - if (color) { - this._onColor.fire([{ index: ColorIndex.FOREGROUND, color }]); - } - } - // forward second slot to OSC 11 (higher slots are not supported) - if (slots.length > 1) { - this.setOrReportBgColor(slots[1]); - } - return true; + return this._setOrReportSpecialColor(data, 0); } /** @@ -2918,16 +2933,16 @@ 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 setOrReportBgColor(data: string): boolean { - const slots = data.split(';'); - if (slots[0] === '?') { - this._onColor.fire([{ index: ColorIndex.BACKGROUND }]); - } else { - const color = parseColor(slots[0]); - if (color) { - this._onColor.fire([{ index: ColorIndex.BACKGROUND, color }]); - } - } - return true; + return this._setOrReportSpecialColor(data, 1); + } + + /** + * OSC 12 ; | 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); } /** diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 741f8b7c..7e1778a9 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -348,10 +348,11 @@ export interface IWindowOptions { setWinLines?: boolean; } -// color events from common, used for OSC 4/10/11 +// color events from common, used for OSC 4/10/11/12 export const enum ColorIndex { FOREGROUND = 256, - BACKGROUND = 257 + BACKGROUND = 257, + CURSOR = 258 } export interface IColorReportRequest { index: ColorIndex; @@ -431,6 +432,7 @@ export interface IInputHandler { /** OSC 4 */ setOrReportIndexedColor(data: string): boolean; /** OSC 10 */ setOrReportFgColor(data: string): boolean; /** OSC 11 */ setOrReportBgColor(data: string): boolean; + /** OSC 12 */ setOrReportCursorColor(data: string): boolean; /** ESC E */ nextLine(): boolean; /** ESC = */ keypadApplicationMode(): boolean; diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 42681a4e..fc8aadab 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -469,6 +469,12 @@ describe('InputHandler Integration Tests', function(): void { 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']);