Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSC 4/10/11/12 color handling #3524

Merged
merged 27 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6a6043d
properly parse xcolor names
jerch Oct 22, 2021
0f4d71b
make linter happy
jerch Oct 22, 2021
8e71cec
remove only from tests
jerch Oct 22, 2021
01a7322
OSC 10/11 working, OSC 4 partially fixed
jerch Oct 22, 2021
258178f
cleanup & docs for OSC 10/11
jerch Oct 22, 2021
9eb72ee
report 16 bit color spec for query
jerch Oct 22, 2021
5478dd2
Merge branch 'master' into osc_colors
jerch Oct 22, 2021
0871ace
create minified color names module
jerch Oct 24, 2021
9d95c8c
Merge branch 'master' into osc_colors
jerch Nov 7, 2021
b7560be
remove named colors
jerch Nov 8, 2021
1c5f24a
Merge branch 'master' into osc_colors
jerch Nov 8, 2021
5437571
simplify color events
jerch Nov 8, 2021
064f683
make linter happy
jerch Nov 8, 2021
56f0e69
api tests for OSC 10 & 11
jerch Nov 8, 2021
b55ee77
fix inputhandler type definition
jerch Nov 8, 2021
54822af
use xcolor parser for OSC 4
jerch Nov 8, 2021
b8097f5
OSC 4 integration tests
jerch Nov 8, 2021
855ed63
remove only from tests
jerch Nov 8, 2021
4be063b
Merge branch 'master' into osc_colors
jerch Nov 8, 2021
896f5a7
extend logic to OSC 12 cursor color
jerch Nov 9, 2021
e62e82b
Merge branch 'master' into osc_colors
jerch Nov 10, 2021
9964210
implement OSC 104|110|111|112
jerch Nov 10, 2021
8432562
cleanup
jerch Nov 10, 2021
0c20589
remove temp atlas fix
jerch Nov 10, 2021
3cc9994
Merge remote-tracking branch 'upstream/master' into pr/jerch/3524
Tyriar Dec 22, 2021
e635b3d
Merge remote-tracking branch 'upstream/master' into pr/jerch/3524
Tyriar Dec 22, 2021
086ca58
Comment tweaks
Tyriar Dec 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/browser/Color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/

import { IColor } from 'browser/Types';
import { IColorRGB } from 'common/Types';

// FIXME: Move Color.ts lib to common?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Color.ts lives in browser historically as it's only required when a renderer is active, things have changed a little with this PR where InputHandler knows more about color. Sounds like a good change if XParseColor ends up using these utils, but if not it would be best to keep in browser as then it's not included unnecessarily in xterm-headless.


/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
Expand All @@ -17,6 +20,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;
}
Expand Down Expand Up @@ -81,6 +86,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];
}
}

/**
Expand Down Expand Up @@ -197,6 +206,7 @@ export namespace rgba {
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}

// FIXME: Move this to channels NS?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace was meant to represent the source, so rgba.toChannels = rgba format to channels format

export function toChannels(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
Expand Down
45 changes: 45 additions & 0 deletions src/browser/ColorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -96,6 +107,7 @@ export class ColorManager implements IColorManager {
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
this._updateRestoreColors();
}

public onOptionsChange(key: string): void {
Expand Down Expand Up @@ -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(
Expand Down
66 changes: 52 additions & 14 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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 {
Expand Down
Loading