Merge branch 'master' into buffer_optimizations

This commit is contained in:
Jörg Breitbart
2022-10-09 21:41:06 +02:00
103 changed files with 2785 additions and 3664 deletions
+19 -21
View File
@@ -9,7 +9,7 @@ import { IBuffer } from 'common/buffer/Types';
import { isMac } from 'common/Platform';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { removeElementFromParent } from 'browser/Dom';
@@ -65,8 +65,8 @@ export class AccessibilityManager extends Disposable {
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
@@ -87,14 +87,14 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer);
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this.register(this._terminal.onA11yChar(char => this._onChar(char)));
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.onKey(e => this._onKey(e.key)));
this.register(this._terminal.onA11yChar(char => this._handleChar(char)));
this.register(this._terminal.onLineFeed(() => this._handleChar('\n')));
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
@@ -104,15 +104,13 @@ export class AccessibilityManager extends Disposable {
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(toDisposable(() => {
removeElementFromParent(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}));
}
public dispose(): void {
super.dispose();
removeElementFromParent(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = e.target as HTMLElement;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
@@ -172,7 +170,7 @@ export class AccessibilityManager extends Disposable {
e.stopImmediatePropagation();
}
private _onResize(rows: number): void {
private _handleResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -200,13 +198,13 @@ export class AccessibilityManager extends Disposable {
return element;
}
private _onTab(spaceCount: number): void {
private _handleTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
this._handleChar(' ');
}
}
private _onChar(char: string): void {
private _handleChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
@@ -246,7 +244,7 @@ export class AccessibilityManager extends Disposable {
}
}
private _onKey(keyChar: string): void {
private _handleKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
@@ -280,7 +278,7 @@ export class AccessibilityManager extends Disposable {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
this._onResize(this._terminal.rows);
this._handleResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
-366
View File
@@ -1,366 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ColorManager, DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
describe('ColorManager', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: jsdom.DOMWindow;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
fillRect(): void { },
getImageData(): any {
return {data: [0, 0, 0, 0xFF]};
}
});
cm = new ColorManager();
});
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
// A #rrggbb or rgba(...)
assert.ok((cm.colors as any)[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
});
it('should fill 240 colors with expected values', () => {
assert.equal(cm.colors.ansi[16].css, '#000000');
assert.equal(cm.colors.ansi[17].css, '#00005f');
assert.equal(cm.colors.ansi[18].css, '#000087');
assert.equal(cm.colors.ansi[19].css, '#0000af');
assert.equal(cm.colors.ansi[20].css, '#0000d7');
assert.equal(cm.colors.ansi[21].css, '#0000ff');
assert.equal(cm.colors.ansi[22].css, '#005f00');
assert.equal(cm.colors.ansi[23].css, '#005f5f');
assert.equal(cm.colors.ansi[24].css, '#005f87');
assert.equal(cm.colors.ansi[25].css, '#005faf');
assert.equal(cm.colors.ansi[26].css, '#005fd7');
assert.equal(cm.colors.ansi[27].css, '#005fff');
assert.equal(cm.colors.ansi[28].css, '#008700');
assert.equal(cm.colors.ansi[29].css, '#00875f');
assert.equal(cm.colors.ansi[30].css, '#008787');
assert.equal(cm.colors.ansi[31].css, '#0087af');
assert.equal(cm.colors.ansi[32].css, '#0087d7');
assert.equal(cm.colors.ansi[33].css, '#0087ff');
assert.equal(cm.colors.ansi[34].css, '#00af00');
assert.equal(cm.colors.ansi[35].css, '#00af5f');
assert.equal(cm.colors.ansi[36].css, '#00af87');
assert.equal(cm.colors.ansi[37].css, '#00afaf');
assert.equal(cm.colors.ansi[38].css, '#00afd7');
assert.equal(cm.colors.ansi[39].css, '#00afff');
assert.equal(cm.colors.ansi[40].css, '#00d700');
assert.equal(cm.colors.ansi[41].css, '#00d75f');
assert.equal(cm.colors.ansi[42].css, '#00d787');
assert.equal(cm.colors.ansi[43].css, '#00d7af');
assert.equal(cm.colors.ansi[44].css, '#00d7d7');
assert.equal(cm.colors.ansi[45].css, '#00d7ff');
assert.equal(cm.colors.ansi[46].css, '#00ff00');
assert.equal(cm.colors.ansi[47].css, '#00ff5f');
assert.equal(cm.colors.ansi[48].css, '#00ff87');
assert.equal(cm.colors.ansi[49].css, '#00ffaf');
assert.equal(cm.colors.ansi[50].css, '#00ffd7');
assert.equal(cm.colors.ansi[51].css, '#00ffff');
assert.equal(cm.colors.ansi[52].css, '#5f0000');
assert.equal(cm.colors.ansi[53].css, '#5f005f');
assert.equal(cm.colors.ansi[54].css, '#5f0087');
assert.equal(cm.colors.ansi[55].css, '#5f00af');
assert.equal(cm.colors.ansi[56].css, '#5f00d7');
assert.equal(cm.colors.ansi[57].css, '#5f00ff');
assert.equal(cm.colors.ansi[58].css, '#5f5f00');
assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
assert.equal(cm.colors.ansi[60].css, '#5f5f87');
assert.equal(cm.colors.ansi[61].css, '#5f5faf');
assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
assert.equal(cm.colors.ansi[63].css, '#5f5fff');
assert.equal(cm.colors.ansi[64].css, '#5f8700');
assert.equal(cm.colors.ansi[65].css, '#5f875f');
assert.equal(cm.colors.ansi[66].css, '#5f8787');
assert.equal(cm.colors.ansi[67].css, '#5f87af');
assert.equal(cm.colors.ansi[68].css, '#5f87d7');
assert.equal(cm.colors.ansi[69].css, '#5f87ff');
assert.equal(cm.colors.ansi[70].css, '#5faf00');
assert.equal(cm.colors.ansi[71].css, '#5faf5f');
assert.equal(cm.colors.ansi[72].css, '#5faf87');
assert.equal(cm.colors.ansi[73].css, '#5fafaf');
assert.equal(cm.colors.ansi[74].css, '#5fafd7');
assert.equal(cm.colors.ansi[75].css, '#5fafff');
assert.equal(cm.colors.ansi[76].css, '#5fd700');
assert.equal(cm.colors.ansi[77].css, '#5fd75f');
assert.equal(cm.colors.ansi[78].css, '#5fd787');
assert.equal(cm.colors.ansi[79].css, '#5fd7af');
assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
assert.equal(cm.colors.ansi[82].css, '#5fff00');
assert.equal(cm.colors.ansi[83].css, '#5fff5f');
assert.equal(cm.colors.ansi[84].css, '#5fff87');
assert.equal(cm.colors.ansi[85].css, '#5fffaf');
assert.equal(cm.colors.ansi[86].css, '#5fffd7');
assert.equal(cm.colors.ansi[87].css, '#5fffff');
assert.equal(cm.colors.ansi[88].css, '#870000');
assert.equal(cm.colors.ansi[89].css, '#87005f');
assert.equal(cm.colors.ansi[90].css, '#870087');
assert.equal(cm.colors.ansi[91].css, '#8700af');
assert.equal(cm.colors.ansi[92].css, '#8700d7');
assert.equal(cm.colors.ansi[93].css, '#8700ff');
assert.equal(cm.colors.ansi[94].css, '#875f00');
assert.equal(cm.colors.ansi[95].css, '#875f5f');
assert.equal(cm.colors.ansi[96].css, '#875f87');
assert.equal(cm.colors.ansi[97].css, '#875faf');
assert.equal(cm.colors.ansi[98].css, '#875fd7');
assert.equal(cm.colors.ansi[99].css, '#875fff');
assert.equal(cm.colors.ansi[100].css, '#878700');
assert.equal(cm.colors.ansi[101].css, '#87875f');
assert.equal(cm.colors.ansi[102].css, '#878787');
assert.equal(cm.colors.ansi[103].css, '#8787af');
assert.equal(cm.colors.ansi[104].css, '#8787d7');
assert.equal(cm.colors.ansi[105].css, '#8787ff');
assert.equal(cm.colors.ansi[106].css, '#87af00');
assert.equal(cm.colors.ansi[107].css, '#87af5f');
assert.equal(cm.colors.ansi[108].css, '#87af87');
assert.equal(cm.colors.ansi[109].css, '#87afaf');
assert.equal(cm.colors.ansi[110].css, '#87afd7');
assert.equal(cm.colors.ansi[111].css, '#87afff');
assert.equal(cm.colors.ansi[112].css, '#87d700');
assert.equal(cm.colors.ansi[113].css, '#87d75f');
assert.equal(cm.colors.ansi[114].css, '#87d787');
assert.equal(cm.colors.ansi[115].css, '#87d7af');
assert.equal(cm.colors.ansi[116].css, '#87d7d7');
assert.equal(cm.colors.ansi[117].css, '#87d7ff');
assert.equal(cm.colors.ansi[118].css, '#87ff00');
assert.equal(cm.colors.ansi[119].css, '#87ff5f');
assert.equal(cm.colors.ansi[120].css, '#87ff87');
assert.equal(cm.colors.ansi[121].css, '#87ffaf');
assert.equal(cm.colors.ansi[122].css, '#87ffd7');
assert.equal(cm.colors.ansi[123].css, '#87ffff');
assert.equal(cm.colors.ansi[124].css, '#af0000');
assert.equal(cm.colors.ansi[125].css, '#af005f');
assert.equal(cm.colors.ansi[126].css, '#af0087');
assert.equal(cm.colors.ansi[127].css, '#af00af');
assert.equal(cm.colors.ansi[128].css, '#af00d7');
assert.equal(cm.colors.ansi[129].css, '#af00ff');
assert.equal(cm.colors.ansi[130].css, '#af5f00');
assert.equal(cm.colors.ansi[131].css, '#af5f5f');
assert.equal(cm.colors.ansi[132].css, '#af5f87');
assert.equal(cm.colors.ansi[133].css, '#af5faf');
assert.equal(cm.colors.ansi[134].css, '#af5fd7');
assert.equal(cm.colors.ansi[135].css, '#af5fff');
assert.equal(cm.colors.ansi[136].css, '#af8700');
assert.equal(cm.colors.ansi[137].css, '#af875f');
assert.equal(cm.colors.ansi[138].css, '#af8787');
assert.equal(cm.colors.ansi[139].css, '#af87af');
assert.equal(cm.colors.ansi[140].css, '#af87d7');
assert.equal(cm.colors.ansi[141].css, '#af87ff');
assert.equal(cm.colors.ansi[142].css, '#afaf00');
assert.equal(cm.colors.ansi[143].css, '#afaf5f');
assert.equal(cm.colors.ansi[144].css, '#afaf87');
assert.equal(cm.colors.ansi[145].css, '#afafaf');
assert.equal(cm.colors.ansi[146].css, '#afafd7');
assert.equal(cm.colors.ansi[147].css, '#afafff');
assert.equal(cm.colors.ansi[148].css, '#afd700');
assert.equal(cm.colors.ansi[149].css, '#afd75f');
assert.equal(cm.colors.ansi[150].css, '#afd787');
assert.equal(cm.colors.ansi[151].css, '#afd7af');
assert.equal(cm.colors.ansi[152].css, '#afd7d7');
assert.equal(cm.colors.ansi[153].css, '#afd7ff');
assert.equal(cm.colors.ansi[154].css, '#afff00');
assert.equal(cm.colors.ansi[155].css, '#afff5f');
assert.equal(cm.colors.ansi[156].css, '#afff87');
assert.equal(cm.colors.ansi[157].css, '#afffaf');
assert.equal(cm.colors.ansi[158].css, '#afffd7');
assert.equal(cm.colors.ansi[159].css, '#afffff');
assert.equal(cm.colors.ansi[160].css, '#d70000');
assert.equal(cm.colors.ansi[161].css, '#d7005f');
assert.equal(cm.colors.ansi[162].css, '#d70087');
assert.equal(cm.colors.ansi[163].css, '#d700af');
assert.equal(cm.colors.ansi[164].css, '#d700d7');
assert.equal(cm.colors.ansi[165].css, '#d700ff');
assert.equal(cm.colors.ansi[166].css, '#d75f00');
assert.equal(cm.colors.ansi[167].css, '#d75f5f');
assert.equal(cm.colors.ansi[168].css, '#d75f87');
assert.equal(cm.colors.ansi[169].css, '#d75faf');
assert.equal(cm.colors.ansi[170].css, '#d75fd7');
assert.equal(cm.colors.ansi[171].css, '#d75fff');
assert.equal(cm.colors.ansi[172].css, '#d78700');
assert.equal(cm.colors.ansi[173].css, '#d7875f');
assert.equal(cm.colors.ansi[174].css, '#d78787');
assert.equal(cm.colors.ansi[175].css, '#d787af');
assert.equal(cm.colors.ansi[176].css, '#d787d7');
assert.equal(cm.colors.ansi[177].css, '#d787ff');
assert.equal(cm.colors.ansi[178].css, '#d7af00');
assert.equal(cm.colors.ansi[179].css, '#d7af5f');
assert.equal(cm.colors.ansi[180].css, '#d7af87');
assert.equal(cm.colors.ansi[181].css, '#d7afaf');
assert.equal(cm.colors.ansi[182].css, '#d7afd7');
assert.equal(cm.colors.ansi[183].css, '#d7afff');
assert.equal(cm.colors.ansi[184].css, '#d7d700');
assert.equal(cm.colors.ansi[185].css, '#d7d75f');
assert.equal(cm.colors.ansi[186].css, '#d7d787');
assert.equal(cm.colors.ansi[187].css, '#d7d7af');
assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
assert.equal(cm.colors.ansi[190].css, '#d7ff00');
assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
assert.equal(cm.colors.ansi[192].css, '#d7ff87');
assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
assert.equal(cm.colors.ansi[195].css, '#d7ffff');
assert.equal(cm.colors.ansi[196].css, '#ff0000');
assert.equal(cm.colors.ansi[197].css, '#ff005f');
assert.equal(cm.colors.ansi[198].css, '#ff0087');
assert.equal(cm.colors.ansi[199].css, '#ff00af');
assert.equal(cm.colors.ansi[200].css, '#ff00d7');
assert.equal(cm.colors.ansi[201].css, '#ff00ff');
assert.equal(cm.colors.ansi[202].css, '#ff5f00');
assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
assert.equal(cm.colors.ansi[204].css, '#ff5f87');
assert.equal(cm.colors.ansi[205].css, '#ff5faf');
assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
assert.equal(cm.colors.ansi[207].css, '#ff5fff');
assert.equal(cm.colors.ansi[208].css, '#ff8700');
assert.equal(cm.colors.ansi[209].css, '#ff875f');
assert.equal(cm.colors.ansi[210].css, '#ff8787');
assert.equal(cm.colors.ansi[211].css, '#ff87af');
assert.equal(cm.colors.ansi[212].css, '#ff87d7');
assert.equal(cm.colors.ansi[213].css, '#ff87ff');
assert.equal(cm.colors.ansi[214].css, '#ffaf00');
assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
assert.equal(cm.colors.ansi[216].css, '#ffaf87');
assert.equal(cm.colors.ansi[217].css, '#ffafaf');
assert.equal(cm.colors.ansi[218].css, '#ffafd7');
assert.equal(cm.colors.ansi[219].css, '#ffafff');
assert.equal(cm.colors.ansi[220].css, '#ffd700');
assert.equal(cm.colors.ansi[221].css, '#ffd75f');
assert.equal(cm.colors.ansi[222].css, '#ffd787');
assert.equal(cm.colors.ansi[223].css, '#ffd7af');
assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
assert.equal(cm.colors.ansi[226].css, '#ffff00');
assert.equal(cm.colors.ansi[227].css, '#ffff5f');
assert.equal(cm.colors.ansi[228].css, '#ffff87');
assert.equal(cm.colors.ansi[229].css, '#ffffaf');
assert.equal(cm.colors.ansi[230].css, '#ffffd7');
assert.equal(cm.colors.ansi[231].css, '#ffffff');
assert.equal(cm.colors.ansi[232].css, '#080808');
assert.equal(cm.colors.ansi[233].css, '#121212');
assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
assert.equal(cm.colors.ansi[235].css, '#262626');
assert.equal(cm.colors.ansi[236].css, '#303030');
assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
assert.equal(cm.colors.ansi[238].css, '#444444');
assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
assert.equal(cm.colors.ansi[240].css, '#585858');
assert.equal(cm.colors.ansi[241].css, '#626262');
assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
assert.equal(cm.colors.ansi[243].css, '#767676');
assert.equal(cm.colors.ansi[244].css, '#808080');
assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
assert.equal(cm.colors.ansi[246].css, '#949494');
assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
assert.equal(cm.colors.ansi[253].css, '#dadada');
assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
assert.equal(cm.colors.ansi[255].css, '#eeeeee');
});
});
describe('setTheme', () => {
it('should not throw when not setting all colors', () => {
assert.doesNotThrow(() => {
cm.setTheme({});
});
});
it('should set a partial set of colors, using the default if not present', () => {
assert.equal(cm.colors.background.css, '#000000');
assert.equal(cm.colors.foreground.css, '#ffffff');
cm.setTheme({
background: '#FF0000',
foreground: '#00FF00'
});
assert.equal(cm.colors.background.css, '#FF0000');
assert.equal(cm.colors.foreground.css, '#00FF00');
cm.setTheme({
background: '#0000FF'
});
assert.equal(cm.colors.background.css, '#0000FF');
// FG reverts back to default
assert.equal(cm.colors.foreground.css, '#ffffff');
});
it('should set all extended ansi colors in reverse order', () => {
cm.setTheme({
extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
});
for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
assert.equal(cm.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
}
});
it('should set one extended ansi color and keep the other default', () => {
cm.setTheme({
extendedAnsi: ['#ffffff']
});
assert.equal(cm.colors.ansi[16].css, '#ffffff');
assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
});
it('should set extended ansi colors to the default when they are unset', () => {
cm.setTheme({
extendedAnsi: ['#ffffff']
});
assert.equal(cm.colors.ansi[16].css, '#ffffff');
cm.setTheme({
extendedAnsi: []
});
assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
cm.setTheme({
extendedAnsi: ['#ffffff']
});
assert.equal(cm.colors.ansi[16].css, '#ffffff');
cm.setTheme({});
assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
});
it('should set extended ansi colors to the default when they are partially unset', () => {
cm.setTheme({
extendedAnsi: ['#ffffff', '#000000']
});
assert.equal(cm.colors.ansi[16].css, '#ffffff');
assert.equal(cm.colors.ansi[17].css, '#000000');
cm.setTheme({
extendedAnsi: ['#ffffff']
});
assert.equal(cm.colors.ansi[16].css, '#ffffff');
assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
});
});
});
-221
View File
@@ -1,221 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'common/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorIndex, IColor } 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');
const DEFAULT_CURSOR = css.toColor('#ffffff');
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS.
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
const colors = [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
// Generate colors (16-231)
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
colors.push({
css: channels.toCss(c, c, c),
rgba: channels.toRgba(c, c, c)
});
}
return colors;
})());
/**
* Manages the source of truth for a terminal's colors.
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _contrastCache: IColorContrastCache;
private _restoreColors!: IRestoreColorSet;
constructor() {
this._contrastCache = new ColorContrastCache();
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionForeground: undefined,
selectionBackgroundTransparent: DEFAULT_SELECTION,
selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
this._updateRestoreColors();
}
public onOptionsChange(key: string, value: any): void {
switch (key) {
case 'minimumContrastRatio':
this._contrastCache.clear();
break;
}
}
/**
* Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme = {}): void {
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION);
this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
const nullColor: IColor = {
css: '',
rgba: 0
};
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
if (this.colors.selectionForeground === nullColor) {
this.colors.selectionForeground = undefined;
}
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
*/
if (color.isOpaque(this.colors.selectionBackgroundTransparent)) {
const opacity = 0.3;
this.colors.selectionBackgroundTransparent = color.opacity(this.colors.selectionBackgroundTransparent, opacity);
}
if (color.isOpaque(this.colors.selectionInactiveBackgroundTransparent)) {
const opacity = 0.3;
this.colors.selectionInactiveBackgroundTransparent = color.opacity(this.colors.selectionInactiveBackgroundTransparent, opacity);
}
this.colors.ansi = DEFAULT_ANSI_COLORS.slice();
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
if (theme.extendedAnsi) {
const colorCount = Math.min(this.colors.ansi.length - 16, theme.extendedAnsi.length);
for (let i = 0; i < colorCount; i++) {
this.colors.ansi[i + 16] = this._parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
}
}
// 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.slice()
};
}
private _parseColor(
cssString: string | undefined,
fallback: IColor
): IColor {
if (cssString !== undefined) {
try {
return css.toColor(cssString);
} catch {
// no-op
}
}
return fallback;
}
}
+12 -14
View File
@@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { Disposable, getDisposeArrayDisposable, disposeArray, toDisposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
export class Linkifier2 extends Disposable implements ILinkifier2 {
@@ -26,21 +26,19 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
private _activeLine: number = -1;
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
private _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
private readonly _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public readonly onShowLinkUnderline = this._onShowLinkUnderline.event;
private readonly _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
}
public dispose(): void {
super.dispose();
this._lastMouseEvent = undefined;
this.register(toDisposable(() => {
this._lastMouseEvent = undefined;
}));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
@@ -66,12 +64,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this._isMouseOut = true;
this._clearCurrentLink();
}));
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'mousemove', this._handleMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
}
private _onMouseMove(event: MouseEvent): void {
private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
if (!this._element || !this._mouseService) {
@@ -99,12 +97,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) {
this._onHover(position);
this._handleHover(position);
this._lastBufferCell = position;
}
}
private _onHover(position: IBufferCellPosition): void {
private _handleHover(position: IBufferCellPosition): void {
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// Check if we need to clear the link
if (this._activeLine !== position.y) {
+4 -6
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
@@ -26,6 +26,9 @@ export class ScreenDprMonitor extends Disposable {
constructor(private _parentWindow: Window) {
super();
this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio;
this.register(toDisposable(() => {
this.clearListener();
}));
}
public setListener(listener: ScreenDprListener): void {
@@ -43,11 +46,6 @@ export class ScreenDprMonitor extends Disposable {
this._updateDpr();
}
public dispose(): void {
super.dispose();
this.clearListener();
}
private _updateDpr(): void {
if (!this._outerListener) {
return;
-1
View File
@@ -1378,7 +1378,6 @@ describe('Terminal', () => {
assert.deepEqual(disposeStack, [markers[0], markers[1]]);
// trimmed marker objs should be disposed
assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]);
assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]);
// trimmed markers should contain line -1
assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]);
});
+76 -124
View File
@@ -22,7 +22,7 @@
*/
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
import { IRenderer } from 'browser/renderer/Types';
import { IRenderer } from 'browser/renderer/shared/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
@@ -39,9 +39,8 @@ import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseActio
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService, IThemeService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
import { MouseService } from 'browser/services/MouseService';
@@ -56,6 +55,8 @@ import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
import { OscLinkProvider } from 'browser/OscLinkProvider';
import { toDisposable } from 'common/Lifecycle';
import { ThemeService } from 'browser/services/ThemeService';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -85,6 +86,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _themeService: IThemeService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
@@ -119,30 +121,30 @@ export class Terminal extends CoreTerminal implements ITerminal {
public viewport: IViewport | undefined;
private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: AccessibilityManager | undefined;
private _colorManager: ColorManager | undefined;
private _theme: ITheme | undefined;
private _onCursorMove = new EventEmitter<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
private _onRender = new EventEmitter<{ start: number, end: number }>();
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
private _onSelectionChange = new EventEmitter<void>();
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onBell = new EventEmitter<void>();
public get onBell(): IEvent<void> { return this._onBell.event; }
private readonly _onCursorMove = this.register(new EventEmitter<void>());
public readonly onCursorMove = this._onCursorMove.event;
private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>());
public readonly onKey = this._onKey.event;
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRender = this._onRender.event;
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
public readonly onSelectionChange = this._onSelectionChange.event;
private readonly _onTitleChange = this.register(new EventEmitter<string>());
public readonly onTitleChange = this._onTitleChange.event;
private readonly _onBell = this.register(new EventEmitter<void>());
public readonly onBell = this._onBell.event;
private _onFocus = new EventEmitter<void>();
private _onFocus = this.register(new EventEmitter<void>());
public get onFocus(): IEvent<void> { return this._onFocus.event; }
private _onBlur = new EventEmitter<void>();
private _onBlur = this.register(new EventEmitter<void>());
public get onBlur(): IEvent<void> { return this._onBlur.event; }
private _onA11yCharEmitter = new EventEmitter<string>();
private _onA11yCharEmitter = this.register(new EventEmitter<string>());
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
private _onA11yTabEmitter = new EventEmitter<number>();
private _onA11yTabEmitter = this.register(new EventEmitter<number>());
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
private _onWillOpen = this.register(new EventEmitter<HTMLElement>());
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
/**
* Creates a new `Terminal` object.
@@ -182,6 +184,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Setup listeners
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
this.register(toDisposable(() => {
this._customKeyEventHandler = undefined;
this.element?.parentNode?.removeChild(this.element);
}));
}
/**
@@ -191,9 +198,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
* 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;
if (!this._themeService) return;
for (const req of event) {
let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
let acc: 'foreground' | 'background' | 'cursor' | 'ansi';
let ident = '';
switch (req.index) {
case ColorIndex.FOREGROUND: // OSC 10 | 110
@@ -216,32 +223,23 @@ export class Terminal extends CoreTerminal implements ITerminal {
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
? this._colorManager.colors.ansi[req.index]
: this._colorManager.colors[acc]);
? this._themeService.colors.ansi[req.index]
: this._themeService.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
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);
if (acc === 'ansi') {
this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
} else {
const narrowedAcc = acc;
this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
}
break;
case ColorRequestType.RESTORE:
this._colorManager.restoreColor(req.index);
this._themeService.restoreColor(req.index);
break;
}
}
this._renderService?.setColors(this._colorManager.colors);
this.viewport?.onThemeChange(this._colorManager.colors);
}
public dispose(): void {
if (this._isDisposed) {
return;
}
super.dispose();
this._renderService?.dispose();
this._customKeyEventHandler = undefined;
this.write = () => { };
this.element?.parentNode?.removeChild(this.element);
}
protected _setup(): void {
@@ -266,60 +264,21 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
}
protected _updateOptions(key: string): void {
super._updateOptions(key);
// TODO: These listeners should be owned by individual components
switch (key) {
case 'fontFamily':
case 'fontSize':
// When the font changes the size of the cells may change which requires a renderer clear
this._renderService?.clear();
this._charSizeService?.measure();
break;
case 'cursorBlink':
case 'cursorStyle':
// The DOM renderer needs a row refresh to update the cursor styles
this.refresh(this.buffer.y, this.buffer.y);
break;
case 'customGlyphs':
case 'drawBoldTextInBrightColors':
case 'letterSpacing':
case 'lineHeight':
case 'fontWeight':
case 'fontWeightBold':
case 'minimumContrastRatio':
// When the font changes the size of the cells may change which requires a renderer clear
if (this._renderService) {
this._renderService.clear();
this._renderService.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1);
}
break;
case 'scrollback':
this.viewport?.syncScrollArea();
break;
case 'screenReaderMode':
if (this.optionsService.rawOptions.screenReaderMode) {
if (!this._accessibilityManager && this._renderService) {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
} else {
this._accessibilityManager?.dispose();
this._accessibilityManager = undefined;
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'theme':
this._setTheme(this.optionsService.rawOptions.theme);
break;
private _handleScreenReaderModeOptionChange(value: boolean): void {
if (value) {
if (!this._accessibilityManager && this._renderService) {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
} else {
this._accessibilityManager?.dispose();
this._accessibilityManager = undefined;
}
}
/**
* Binds the desired focus behavior on a given terminal object.
*/
private _onTextAreaFocus(ev: KeyboardEvent): void {
private _handleTextAreaFocus(ev: KeyboardEvent): void {
if (this.coreService.decPrivateModes.sendFocus) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
}
@@ -340,7 +299,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Binds the desired blur behavior on a given terminal object.
*/
private _onTextAreaBlur(): void {
private _handleTextAreaBlur(): void {
// Text can safely be removed on blur. Doing it earlier could interfere with
// screen readers reading it out.
this.textarea!.value = '';
@@ -491,8 +450,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window);
@@ -501,16 +460,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager();
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e])));
this._colorManager.setTheme(this._theme);
this._themeService = this._instantiationService.createInstance(ThemeService);
this._instantiationService.setService(IThemeService, this._themeService);
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._renderService = this.register(this._instantiationService.createInstance(RenderService, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
@@ -523,26 +479,32 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
try {
this._onWillOpen.fire(this.element);
}
catch { /* fails to load addon for some reason */ }
if (!this._renderService.hasRenderer()) {
this._renderService.setRenderer(this._createRenderer());
}
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport,
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
this._viewportElement,
this._viewportScrollArea,
this.element
this._viewportScrollArea
);
this.viewport.onThemeChange(this._colorManager.colors);
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
this.register(this.viewport);
this.register(this.onCursorMove(() => {
this._renderService!.onCursorMove();
this._renderService!.handleCursorMove();
this._syncTextArea();
}));
this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows)));
this.register(this.onBlur(() => this._renderService!.onBlur()));
this.register(this.onFocus(() => this._renderService!.onFocus()));
this.register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
this.register(this.onBlur(() => this._renderService!.handleBlur()));
this.register(this.onFocus(() => this._renderService!.handleFocus()));
this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea()));
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
@@ -553,7 +515,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
this.register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
this.register(this._selectionService.onLinuxMouseSelection(text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
@@ -570,7 +532,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
// apply mouse event classes set by escape codes before terminal was attached
if (this.coreMouseService.areMouseEventsActive) {
@@ -585,12 +547,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
if (this.options.overviewRulerWidth) {
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
this.optionsService.onOptionChange(() => {
if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
this.optionsService.onSpecificOptionChange('overviewRulerWidth', value => {
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
});
@@ -609,18 +572,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _createRenderer(): IRenderer {
return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
}
/**
* Sets the theme on the renderer. The renderer must have been initialized.
* @param theme The theme to set.
*/
private _setTheme(theme: ITheme): void {
this._theme = theme;
this._colorManager?.setTheme(theme);
this._renderService?.setColors(this._colorManager!.colors);
this.viewport?.onThemeChange(this._colorManager!.colors);
return this._instantiationService.createInstance(DomRenderer, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
}
/**
@@ -859,20 +811,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
// normal viewport scrolling
// conditionally stop event, if the viewport still had rows to scroll within
if (this.viewport!.onWheel(ev)) {
if (this.viewport!.handleWheel(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
if (this.coreMouseService.areMouseEventsActive) return;
this.viewport!.onTouchStart(ev);
this.viewport!.handleTouchStart(ev);
return this.cancel(ev);
}, { passive: true }));
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.coreMouseService.areMouseEventsActive) return;
if (!this.viewport!.onTouchMove(ev)) {
if (!this.viewport!.handleTouchMove(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
+64 -31
View File
@@ -5,11 +5,11 @@
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { Terminal } from 'browser/Terminal';
@@ -17,6 +17,7 @@ import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } fro
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { css } from 'common/Color';
export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
@@ -41,6 +42,7 @@ export class MockTerminal implements ITerminal {
public onTitleChange!: IEvent<string>;
public onBell!: IEvent<void>;
public onScroll!: IEvent<number>;
public onWillOpen!: IEvent<HTMLElement>;
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
public onRender!: IEvent<{ start: number, end: number }>;
public onResize!: IEvent<{ cols: number, rows: number }>;
@@ -264,7 +266,6 @@ export class MockRenderer implements IRenderer {
public dispose(): void {
throw new Error('Method not implemented.');
}
public colorManager!: IColorManager;
public on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
@@ -278,20 +279,17 @@ export class MockRenderer implements IRenderer {
throw new Error('Method not implemented.');
}
public dimensions!: IRenderDimensions;
public setColors(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
public onResize(cols: number, rows: number): void { }
public onCharSizeChanged(): void { }
public onBlur(): void { }
public onFocus(): void { }
public onSelectionChanged(start: [number, number], end: [number, number]): void { }
public onCursorMove(): void { }
public onOptionsChanged(): void { }
public onDevicePixelRatioChange(): void { }
public handleResize(cols: number, rows: number): void { }
public handleCharSizeChanged(): void { }
public handleBlur(): void { }
public handleFocus(): void { }
public handleSelectionChanged(start: [number, number], end: [number, number]): void { }
public handleCursorMove(): void { }
public handleOptionsChanged(): void { }
public handleDevicePixelRatioChange(): void { }
public clear(): void { }
public renderRows(start: number, end: number): void { }
}
@@ -301,16 +299,16 @@ export class MockViewport implements IViewport {
throw new Error('Method not implemented.');
}
public scrollBarWidth: number = 0;
public onThemeChange(colors: IColorSet): void {
public handleThemeChange(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
public onWheel(ev: WheelEvent): boolean {
public handleWheel(ev: WheelEvent): boolean {
throw new Error('Method not implemented.');
}
public onTouchStart(ev: TouchEvent): void {
public handleTouchStart(ev: TouchEvent): void {
throw new Error('Method not implemented.');
}
public onTouchMove(ev: TouchEvent): boolean {
public handleTouchMove(ev: TouchEvent): boolean {
throw new Error('Method not implemented.');
}
public syncScrollArea(): void { }
@@ -400,31 +398,31 @@ export class MockRenderService implements IRenderService {
public resize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
public hasRenderer(): boolean {
throw new Error('Method not implemented.');
}
public setRenderer(renderer: IRenderer): void {
throw new Error('Method not implemented.');
}
public setColors(colors: IColorSet): void {
public handleDevicePixelRatioChange(): void {
throw new Error('Method not implemented.');
}
public onDevicePixelRatioChange(): void {
public handleResize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
public onResize(cols: number, rows: number): void {
public handleCharSizeChanged(): void {
throw new Error('Method not implemented.');
}
public onCharSizeChanged(): void {
public handleBlur(): void {
throw new Error('Method not implemented.');
}
public onBlur(): void {
public handleFocus(): void {
throw new Error('Method not implemented.');
}
public onFocus(): void {
public handleSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
throw new Error('Method not implemented.');
}
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
throw new Error('Method not implemented.');
}
public onCursorMove(): void {
public handleCursorMove(): void {
throw new Error('Method not implemented.');
}
public clear(): void {
@@ -494,10 +492,45 @@ export class MockSelectionService implements ISelectionService {
public refresh(isLinuxMouseSelection?: boolean): void {
throw new Error('Method not implemented.');
}
public onMouseDown(event: MouseEvent): void {
public handleMouseDown(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public isCellInSelection(x: number, y: number): boolean {
return false;
}
}
export class MockThemeService implements IThemeService{
public serviceBrand: undefined;
public onChangeColors = new EventEmitter<ReadonlyColorSet>().event;
public restoreColor(slot?: ColorIndex | undefined): void {
throw new Error('Method not implemented.');
}
public modifyColors(callback: (colors: IColorSet) => void): void {
throw new Error('Method not implemented.');
}
public colors: ReadonlyColorSet = {
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any;
}
+6 -9
View File
@@ -23,6 +23,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal {
onFocus: IEvent<void>;
onA11yChar: IEvent<string>;
onA11yTab: IEvent<number>;
onWillOpen: IEvent<HTMLElement>;
cancel(ev: Event, force?: boolean): boolean | void;
}
@@ -105,11 +106,6 @@ export interface IBrowser {
isWindows: boolean;
}
export interface IColorManager {
colors: IColorSet;
onOptionsChange(key: string, value: any): void;
}
export interface IColorSet {
foreground: IColor;
background: IColor;
@@ -125,6 +121,8 @@ export interface IColorSet {
contrastCache: IColorContrastCache;
}
export type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };
export interface IColorContrastCache {
clear(): void;
setCss(bg: number, fg: number, value: string | null): void;
@@ -146,10 +144,9 @@ export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): boolean;
onThemeChange(colors: IColorSet): void;
handleWheel(ev: WheelEvent): boolean;
handleTouchStart(ev: TouchEvent): void;
handleTouchMove(ev: TouchEvent): boolean;
}
export interface ILinkifierEvent {
+15 -11
View File
@@ -5,11 +5,11 @@
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -52,12 +52,12 @@ export class Viewport extends Disposable implements IViewport {
private readonly _scrollLines: (amount: number) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
private readonly _element: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService themeService: IThemeService
) {
super();
@@ -65,7 +65,7 @@ export class Viewport extends Disposable implements IViewport {
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
@@ -73,11 +73,15 @@ export class Viewport extends Disposable implements IViewport {
this._renderDimensions = this._renderService.dimensions;
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
this._handleThemeChange(themeService.colors);
this.register(themeService.onChangeColors(e => this._handleThemeChange(e)));
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChange(colors: IColorSet): void {
private _handleThemeChange(colors: ReadonlyColorSet): void {
this._viewportElement.style.backgroundColor = colors.background.css;
}
@@ -157,7 +161,7 @@ export class Viewport extends Disposable implements IViewport {
* terminal to scroll to it.
* @param ev The scroll event.
*/
private _onScroll(ev: Event): void {
private _handleScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
@@ -234,7 +238,7 @@ export class Viewport extends Disposable implements IViewport {
* `Viewport`.
* @param ev The mouse wheel event.
*/
public onWheel(ev: WheelEvent): boolean {
public handleWheel(ev: WheelEvent): boolean {
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return false;
@@ -315,7 +319,7 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
public onTouchStart(ev: TouchEvent): void {
public handleTouchStart(ev: TouchEvent): void {
this._lastTouchY = ev.touches[0].pageY;
}
@@ -323,7 +327,7 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): boolean {
public handleTouchMove(ev: TouchEvent): boolean {
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
@@ -5,7 +5,7 @@
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IRenderService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
export class BufferDecorationRenderer extends Disposable {
@@ -39,12 +39,10 @@ export class BufferDecorationRenderer extends Disposable {
}));
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
}
public override dispose(): void {
this._container.remove();
this._decorationElements.clear();
super.dispose();
this.register(toDisposable(() => {
this._container.remove();
this._decorationElements.clear();
}));
}
private _queueRefresh(): void {
@@ -6,7 +6,7 @@
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
@@ -68,6 +68,9 @@ export class OverviewRulerRenderer extends Disposable {
this._registerDecorationListeners();
this._registerBufferChangeListeners();
this._registerDimensionChangeListeners();
this.register(toDisposable(() => {
this._canvas?.remove();
}));
}
/**
@@ -107,24 +110,13 @@ export class OverviewRulerRenderer extends Disposable {
}
}));
// overview ruler width changed
this.register(this._optionsService.onOptionChange(o => {
if (o === 'overviewRulerWidth') {
this._queueRefresh(true);
}
}));
this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
// device pixel ratio changed
this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => {
this._queueRefresh(true);
}));
this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true)));
// set the canvas dimensions
this._queueRefresh(true);
}
public override dispose(): void {
this._canvas?.remove();
super.dispose();
}
private _refreshDrawConstants(): void {
// width
const outerWidth = Math.floor(this._canvas.width / 3);
-56
View File
@@ -1,56 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRequestRedrawEvent {
start: number;
end: number;
}
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
/**
* Fires when the renderer is requesting to be redrawn on the next animation
* frame but is _not_ a result of content changing (eg. selection changes).
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
+49 -54
View File
@@ -3,12 +3,12 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'common/Color';
@@ -40,10 +40,9 @@ export class DomRenderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions;
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return new EventEmitter<IRequestRedrawEvent>().event; }
public readonly onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>()).event;
constructor(
private _colors: IColorSet,
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _viewportElement: HTMLElement,
@@ -52,7 +51,8 @@ export class DomRenderer extends Disposable implements IRenderer {
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService themeService: IThemeService
) {
super();
this._rowContainer = document.createElement('div');
@@ -79,26 +79,27 @@ export class DomRenderer extends Disposable implements IRenderer {
actualCellHeight: 0
};
this._updateDimensions();
this._injectCss();
this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
this.register(themeService.onChangeColors(e => this._injectCss(e)));
this._injectCss(themeService.colors);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
this._screenElement.appendChild(this._selectionContainer);
this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)));
this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)));
}
this.register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
this.register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
public dispose(): void {
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
this.register(toDisposable(() => {
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
// https://github.com/xtermjs/xterm.js/issues/2960
removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
super.dispose();
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
// https://github.com/xtermjs/xterm.js/issues/2960
removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
}));
}
private _updateDimensions(): void {
@@ -144,12 +145,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public setColors(colors: IColorSet): void {
this._colors = colors;
this._injectCss();
}
private _injectCss(): void {
private _injectCss(colors: ReadonlyColorSet): void {
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
this._screenElement.appendChild(this._themeStyleElement);
@@ -158,7 +154,7 @@ export class DomRenderer extends Disposable implements IRenderer {
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this._colors.foreground.css};` +
` color: ${colors.foreground.css};` +
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
`}`;
@@ -183,18 +179,18 @@ export class DomRenderer extends Disposable implements IRenderer {
styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
` 0% {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
` }` +
` 50% {` +
` background-color: ${this._colors.cursorAccent.css};` +
` color: ${this._colors.cursor.css};` +
` background-color: ${colors.cursorAccent.css};` +
` color: ${colors.cursor.css};` +
` }` +
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` outline: 1px solid ${this._colors.cursor.css};` +
` outline: 1px solid ${colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
@@ -204,14 +200,14 @@ export class DomRenderer extends Disposable implements IRenderer {
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
` box-shadow: 0 -1px 0 ${colors.cursor.css} inset;` +
`}`;
// Selection
styles +=
@@ -224,26 +220,26 @@ export class DomRenderer extends Disposable implements IRenderer {
`}` +
`${this._terminalSelector}.focus .${SELECTION_CLASS} div {` +
` position: absolute;` +
` background-color: ${this._colors.selectionBackgroundOpaque.css};` +
` background-color: ${colors.selectionBackgroundOpaque.css};` +
`}` +
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
` position: absolute;` +
` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` +
` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` +
`}`;
// Colors
this._colors.ansi.forEach((c, i) => {
for (const [i, c] of colors.ansi.entries()) {
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
});
}
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
this._themeStyleElement.textContent = styles;
}
public onDevicePixelRatioChange(): void {
public handleDevicePixelRatioChange(): void {
this._updateDimensions();
}
@@ -260,30 +256,30 @@ export class DomRenderer extends Disposable implements IRenderer {
}
}
public onResize(cols: number, rows: number): void {
public handleResize(cols: number, rows: number): void {
this._refreshRowElements(cols, rows);
this._updateDimensions();
}
public onCharSizeChanged(): void {
public handleCharSizeChanged(): void {
this._updateDimensions();
}
public onBlur(): void {
public handleBlur(): void {
this._rowContainer.classList.remove(FOCUS_CLASS);
}
public onFocus(): void {
public handleFocus(): void {
this._rowContainer.classList.add(FOCUS_CLASS);
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Remove all selections
while (this._selectionContainer.children.length) {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
}
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
this.renderRows(0, this._bufferService.rows - 1);
// Selection does not exist
@@ -343,14 +339,13 @@ export class DomRenderer extends Disposable implements IRenderer {
return element;
}
public onCursorMove(): void {
public handleCursorMove(): void {
// No-op, the cursor is drawn when rows are drawn
}
public onOptionsChanged(): void {
private _handleOptionsChanged(): void {
// Force a refresh
this._updateDimensions();
this._injectCss();
}
public clear(): void {
@@ -378,11 +373,11 @@ export class DomRenderer extends Disposable implements IRenderer {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
private _onLinkHover(e: ILinkifierEvent): void {
private _handleLinkHover(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
private _onLinkLeave(e: ILinkifierEvent): void {
private _handleLinkLeave(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
}
@@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
import { css } from 'common/Color';
import { MockCharacterJoinerService, MockCoreBrowserService } from 'browser/TestUtils.test';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -23,35 +23,12 @@ describe('DomRendererRowFactory', () => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(
dom.window.document,
{
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any,
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
new MockCoreBrowserService(),
new MockCoreService(),
new MockDecorationService()
new MockDecorationService(),
new MockThemeService()
);
lineData = createEmptyLineData(2);
});
@@ -299,7 +276,7 @@ describe('DomRendererRowFactory', () => {
it('should force selected cells with content to be rendered above the background', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
rowFactory.onSelectionChanged([1, 0], [2, 0], false);
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span>a</span><span class="xterm-decoration-top">b</span>'
@@ -307,7 +284,7 @@ describe('DomRendererRowFactory', () => {
});
it('should force whitespace cells to be rendered above the background', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
rowFactory.onSelectionChanged([0, 0], [2, 0], false);
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-decoration-top"> </span><span class="xterm-decoration-top">a</span>'
@@ -4,15 +4,15 @@
*/
import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { IColorSet } from 'browser/Types';
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
export const BOLD_CLASS = 'xterm-bold';
@@ -35,20 +35,16 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
private _colors: IColorSet,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@ICoreService private readonly _coreService: ICoreService,
@IDecorationService private readonly _decorationService: IDecorationService
@IDecorationService private readonly _decorationService: IDecorationService,
@IThemeService private readonly _themeService: IThemeService
) {
}
public setColors(colors: IColorSet): void {
this._colors = colors;
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
@@ -71,6 +67,8 @@ export class DomRendererRowFactory {
}
}
const colors = this._themeService.colors;
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
let width = this._workCell.getWidth();
@@ -176,7 +174,7 @@ export class DomRendererRowFactory {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
charElement.style.textDecorationColor = this._colors.ansi[fg].css;
charElement.style.textDecorationColor = colors.ansi[fg].css;
}
}
}
@@ -224,17 +222,17 @@ export class DomRendererRowFactory {
// Apply selection foreground if applicable
const isInSelection = this._isCellInSelection(x, row);
if (!isTop) {
if (this._colors.selectionForeground && isInSelection) {
if (colors.selectionForeground && isInSelection) {
fgColorMode = Attributes.CM_RGB;
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = this._colors.selectionForeground;
fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = colors.selectionForeground;
}
}
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections
if (isInSelection) {
bgOverride = this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque;
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
isTop = true;
}
@@ -248,7 +246,7 @@ export class DomRendererRowFactory {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
resolvedBg = this._colors.ansi[bg];
resolvedBg = colors.ansi[bg];
charElement.classList.add(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
@@ -258,10 +256,10 @@ export class DomRendererRowFactory {
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
resolvedBg = this._colors.foreground;
resolvedBg = colors.foreground;
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
} else {
resolvedBg = this._colors.background;
resolvedBg = colors.background;
}
}
@@ -279,7 +277,7 @@ export class DomRendererRowFactory {
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) {
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
charElement.classList.add(`xterm-fg-${fg}`);
}
break;
@@ -295,7 +293,7 @@ export class DomRendererRowFactory {
break;
case Attributes.CM_DEFAULT:
default:
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) {
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) {
if (isInverse) {
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}
@@ -317,13 +315,13 @@ export class DomRendererRowFactory {
// Try get from cache first, only use the cache when there are no decoration overrides
let adjustedColor: IColor | undefined | null = undefined;
if (!bgOverride && !fgOverride) {
adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba);
adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba);
}
// Calculate and store in cache
if (adjustedColor === undefined) {
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio);
this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
}
if (adjustedColor) {
@@ -0,0 +1,137 @@
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { IDecorationService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from 'xterm';
// Work variables to avoid garbage collection
let $fg = 0;
let $bg = 0;
let $hasFg = false;
let $hasBg = false;
let $isSelected = false;
let $colors: ReadonlyColorSet | undefined;
export class CellColorResolver {
/**
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
* any other calls will share object.
*/
public readonly result: { fg: number, bg: number, ext: number } = {
fg: 0,
bg: 0,
ext: 0
};
constructor(
private readonly _terminal: Terminal,
private readonly _selectionRenderModel: ISelectionRenderModel,
private readonly _decorationService: IDecorationService,
private readonly _coreBrowserService: ICoreBrowserService,
private readonly _themeService: IThemeService
) {
}
/**
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
*/
public resolve(cell: ICellData, x: number, y: number): void {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
// Reset overrides work variables
$bg = 0;
$fg = 0;
$hasBg = false;
$hasFg = false;
$isSelected = false;
$colors = this._themeService.colors;
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
$hasBg = true;
if ($colors.selectionForeground) {
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
}
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if ($hasBg) {
if ($isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
$bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
} else {
// Non-RGB attributes from model + override + force RGB color mode
$bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
}
}
if ($hasFg) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
$fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
}
// Handle case where inverse was specified by only one of bg override or fg override was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this.result.fg & FgFlags.INVERSE) {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasFg = true;
}
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasBg = true;
}
}
// Release object
$colors = undefined;
// Use the override if it exists
this.result.bg = $hasBg ? $bg : this.result.bg;
this.result.fg = $hasFg ? $fg : this.result.fg;
}
}
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { Terminal } from 'xterm';
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types';
import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: Terminal[];
}
const charAtlasCache: ITextureAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireTextureAtlas(
terminal: Terminal,
colors: ReadonlyColorSet,
scaledCellWidth: number,
scaledCellHeight: number,
scaledCharWidth: number,
scaledCharHeight: number,
devicePixelRatio: number
): ITextureAtlas {
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
entry.atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.atlas;
}
}
const core: ITerminal = (terminal as any)._core;
const newEntry: ITextureAtlasCacheEntry = {
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
* @param terminal The terminal to remove.
*/
export function removeTerminalFromCache(terminal: Terminal): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only terminal
charAtlasCache[i].atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from './Types';
import { Attributes } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { NULL_COLOR } from 'common/Color';
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
background: colors.background,
cursor: NULL_COLOR,
cursorAccent: NULL_COLOR,
selectionForeground: NULL_COLOR,
selectionBackgroundTransparent: NULL_COLOR,
selectionBackgroundOpaque: NULL_COLOR,
selectionInactiveBackgroundTransparent: NULL_COLOR,
selectionInactiveBackgroundOpaque: NULL_COLOR,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(),
contrastCache: colors.contrastCache
};
return {
customGlyphs: terminal.options.customGlyphs,
devicePixelRatio,
letterSpacing: terminal.options.letterSpacing,
lineHeight: terminal.options.lineHeight,
scaledCellWidth,
scaledCellHeight,
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors,
minimumContrastRatio: terminal.options.minimumContrastRatio,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.customGlyphs === b.customGlyphs &&
a.lineHeight === b.lineHeight &&
a.letterSpacing === b.letterSpacing &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
a.minimumContrastRatio === b.minimumContrastRatio &&
a.colors.foreground.rgba === b.colors.foreground.rgba &&
a.colors.background.rgba === b.colors.background.rgba;
}
export function is256Color(colorCode: number): boolean {
return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256;
}
@@ -3,7 +3,7 @@
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IBlockVector {
x: number;
+1
View File
@@ -0,0 +1 @@
This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module.
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { Terminal } from 'xterm';
class SelectionRenderModel implements ISelectionRenderModel {
public hasSelection!: boolean;
public columnSelectMode!: boolean;
public viewportStartRow!: number;
public viewportEndRow!: number;
public viewportCappedStartRow!: number;
public viewportCappedEndRow!: number;
public startCol!: number;
public endCol!: number;
public selectionStart: [number, number] | undefined;
public selectionEnd: [number, number] | undefined;
constructor() {
this.clear();
}
public clear(): void {
this.hasSelection = false;
this.columnSelectMode = false;
this.viewportStartRow = 0;
this.viewportEndRow = 0;
this.viewportCappedStartRow = 0;
this.viewportCappedEndRow = 0;
this.startCol = 0;
this.endCol = 0;
this.selectionStart = undefined;
this.selectionEnd = undefined;
}
public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this.selectionStart = start;
this.selectionEnd = end;
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this.clear();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this.clear();
return;
}
this.hasSelection = true;
this.columnSelectMode = columnSelectMode;
this.viewportStartRow = viewportStartRow;
this.viewportEndRow = viewportEndRow;
this.viewportCappedStartRow = viewportCappedStartRow;
this.viewportCappedEndRow = viewportCappedEndRow;
this.startCol = start[0];
this.endCol = end[0];
}
public isCellSelected(terminal: Terminal, x: number, y: number): boolean {
if (!this.hasSelection) {
return false;
}
y -= terminal.buffer.active.viewportY;
if (this.columnSelectMode) {
if (this.startCol <= this.endCol) {
return x >= this.startCol && y >= this.viewportCappedStartRow &&
x < this.endCol && y <= this.viewportCappedEndRow;
}
return x < this.startCol && y >= this.viewportCappedStartRow &&
x >= this.endCol && y <= this.viewportCappedEndRow;
}
return (y > this.viewportStartRow && y < this.viewportEndRow) ||
(this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) ||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) ||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol);
}
}
export function createSelectionRenderModel(): ISelectionRenderModel {
return new SelectionRenderModel();
}
+843
View File
@@ -0,0 +1,843 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { color, NULL_COLOR, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
/**
* The amount of the texture to be filled before throwing it away and starting
* again. Since the throw away and individual glyph draws don't cost too much,
* this prevent juggling multiple textures in the GL context.
*/
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
/**
* A shared object which is used to draw nothing for a particular cell.
*/
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
offset: { x: 0, y: 0 },
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
size: { x: 0, y: 0 },
sizeClipSpace: { x: 0, y: 0 }
};
const TMP_CANVAS_GLYPH_PADDING = 2;
interface ICharAtlasActiveRow {
x: number;
y: number;
height: number;
}
// Work variables to avoid garbage collection
let $glyph = undefined;
export class TextureAtlas implements ITextureAtlas {
private _didWarmUp: boolean = false;
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();
// The texture that the atlas is drawn to
public cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
private _tmpCanvas: HTMLCanvasElement;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// Texture atlas current positioning data. The texture packing strategy used is to fill from
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
// row's height, the following happens:
//
// - The current row becomes the fixed height row A
// - A new fixed height row B the exact size of the glyph is created below the current row
// - A new dynamic height current row is created below B
//
// This strategy does a good job preventing space being wasted for very short glyphs such as
// underscores, hyphens etc. or those with underlines rendered.
private _currentRow: ICharAtlasActiveRow = {
x: 0,
y: 0,
height: 0
};
private readonly _fixedRows: ICharAtlasActiveRow[] = [];
public hasCanvasChanged = false;
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
private _workAttributeData: AttributeData = new AttributeData();
constructor(
document: Document,
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
this.cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
this._tmpCanvas = document.createElement('canvas');
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {
alpha: this._config.allowTransparency,
willReadFrequently: true
}));
}
public dispose(): void {
if (this.cacheCanvas.parentElement) {
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
}
}
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
private _doWarmUp(): void {
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
const queue = new IdleTaskQueue();
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
}
});
}
}
public beginFrame(): boolean {
if (this._currentRow.y > TEXTURE_CAPACITY) {
this.clearTexture();
this.warmUp();
return true;
}
return false;
}
public clearTexture(): void {
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._cacheMap.clear();
this._cacheMapCombined.clear();
this._currentRow.x = 0;
this._currentRow.y = 0;
this._currentRow.height = 0;
this._fixedRows.length = 0;
this._didWarmUp = false;
this.hasCanvasChanged = true;
}
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext);
}
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext);
}
/**
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
private _getFromCacheMap(
cacheMap: FourKeyMap<string | number, number, number, number, IRasterizedGlyph>,
key: string | number,
bg: number,
fg: number,
ext: number
): IRasterizedGlyph {
$glyph = cacheMap.get(key, bg, fg, ext);
if (!$glyph) {
$glyph = this._drawToCache(key, bg, fg, ext);
cacheMap.set(key, bg, fg, ext, $glyph);
}
return $glyph;
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx >= this._config.colors.ansi.length) {
throw new Error('No color found for idx ' + idx);
}
return this._config.colors.ansi[idx];
}
private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean, dim: boolean): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return NULL_COLOR;
}
let result: IColor;
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
result = this._getColorFromAnsiIndex(bgColor);
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
// TODO: This object creation is slow
result = rgba.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
result = this._config.colors.foreground;
} else {
result = this._config.colors.background;
}
break;
}
if (dim) {
// Blend here instead of using opacity because transparent colors mess with clipping the
// glyph's bounding box
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, DIM_OPACITY));
}
return result;
}
private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, dim: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor {
// TODO: Pass dim along to get min contrast?
const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, excludeFromContrastRatioDemands);
if (minimumContrastColor) {
return minimumContrastColor;
}
let result: IColor;
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
result = this._getColorFromAnsiIndex(fgColor);
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
result = rgba.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
result = this._config.colors.background;
} else {
result = this._config.colors.foreground;
}
}
// Always use an opaque color regardless of allowTransparency
if (this._config.allowTransparency) {
result = color.opaque(result);
}
// Apply dim to the color, opacity is fine to use for the foreground color
if (dim) {
result = color.multiplyOpacity(result, DIM_OPACITY);
}
return result;
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor).rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.foreground.rgba;
}
return this._config.colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.background.rgba;
}
return this._config.colors.foreground.rgba;
}
}
private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined {
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._config.colors.contrastCache.getColor(bg, fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
if (!result) {
this._config.colors.contrastCache.setColor(bg, fg, null);
return undefined;
}
const color = rgba.toColor(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
this._config.colors.contrastCache.setColor(bg, fg, color);
return color;
}
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
// Uncomment for debugging
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
// Include line height when drawing glyphs
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
if (this._tmpCanvas.height < allowedHeight) {
this._tmpCanvas.height = allowedHeight;
}
this._tmpCtx.save();
this._workAttributeData.fg = fg;
this._workAttributeData.bg = bg;
this._workAttributeData.extended.ext = ext;
const invisible = !!this._workAttributeData.isInvisible();
if (invisible) {
return NULL_RASTERIZED_GLYPH;
}
const bold = !!this._workAttributeData.isBold();
const inverse = !!this._workAttributeData.isInverse();
const dim = !!this._workAttributeData.isDim();
const italic = !!this._workAttributeData.isItalic();
const underline = !!this._workAttributeData.isUnderline();
const strikethrough = !!this._workAttributeData.isStrikethrough();
let fgColor = this._workAttributeData.getFgColor();
let fgColorMode = this._workAttributeData.getFgColorMode();
let bgColor = this._workAttributeData.getBgColor();
let bgColorMode = this._workAttributeData.getBgColorMode();
if (inverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
// draw the background
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse, dim);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.globalCompositeOperation = 'source-over';
// draw the foreground/glyph
const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight;
const fontStyle = italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = restrictedPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
// Draw custom characters if applicable
let customGlyph = false;
if (this._config.customGlyphs !== false) {
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize, this._config.devicePixelRatio);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
// background color. This should be disabled when the glyph contains multiple colors such as
// underline colors to prevent important colors could get cleared.
let enableClearThresholdCheck = !powerlineGlyph;
let chWidth: number;
if (typeof codeOrChars === 'number') {
chWidth = this._unicodeService.wcwidth(codeOrChars);
} else {
chWidth = this._unicodeService.getStringCellWidth(codeOrChars);
}
// Draw underline
if (underline) {
this._tmpCtx.save();
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15));
// When the line width is odd, draw at a 0.5 position
const yOffset = lineWidth % 2 === 1 ? 0.5 : 0;
this._tmpCtx.lineWidth = lineWidth;
// Underline color
if (this._workAttributeData.isUnderlineColorDefault()) {
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
} else if (this._workAttributeData.isUnderlineColorRGB()) {
enableClearThresholdCheck = false;
this._tmpCtx.strokeStyle = `rgb(${AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(',')})`;
} else {
enableClearThresholdCheck = false;
let fg = this._workAttributeData.getUnderlineColor();
if (this._config.drawBoldTextInBrightColors && this._workAttributeData.isBold() && fg < 8) {
fg += 8;
}
this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css;
}
// Underline style/stroke
this._tmpCtx.beginPath();
const xLeft = padding;
const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset;
const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset;
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset;
for (let i = 0; i < chWidth; i++) {
this._tmpCtx.save();
const xChLeft = xLeft + i * this._config.scaledCellWidth;
const xChRight = xLeft + (i + 1) * this._config.scaledCellWidth;
const xChMid = xChLeft + this._config.scaledCellWidth / 2;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
this._tmpCtx.moveTo(xChLeft, yBot);
this._tmpCtx.lineTo(xChRight, yBot);
break;
case UnderlineStyle.CURLY:
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
// made taller when the line width is as otherwise it's not very clear otherwise.
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
// Clip the left and right edges of the underline such that it can be drawn just outside
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
// glyphs adjacent to one another.
const clipRegion = new Path2D();
clipRegion.rect(xChLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
this._tmpCtx.moveTo(xChLeft - this._config.scaledCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
xChLeft, yCurlyTop,
xChLeft, yMid
);
this._tmpCtx.bezierCurveTo(
xChLeft, yCurlyBot,
xChMid, yCurlyBot,
xChMid, yMid
);
this._tmpCtx.bezierCurveTo(
xChMid, yCurlyTop,
xChRight, yCurlyTop,
xChRight, yMid
);
this._tmpCtx.bezierCurveTo(
xChRight, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yMid
);
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 2, this._config.devicePixelRatio]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
case UnderlineStyle.SINGLE:
default:
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
}
this._tmpCtx.stroke();
this._tmpCtx.restore();
}
this._tmpCtx.restore();
// Draw stroke in the background color for non custom characters in order to give an outline
// between the text and the underline. Only do this when font size is >= 12 as the underline
// looks odd when the font size is too small
if (!customGlyph && this._config.fontSize >= 12) {
// This only works when transparency is disabled because it's not clear how to clear stroked
// text
if (!this._config.allowTransparency && chars !== ' ') {
// Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
// baseline
this._tmpCtx.save();
this._tmpCtx.textBaseline = 'alphabetic';
const metrics = this._tmpCtx.measureText(chars);
this._tmpCtx.restore();
if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) {
// This translates to 1/2 the line width in either direction
this._tmpCtx.save();
// Clip the region to only draw in valid pixels near the underline to avoid a slight
// outline around the whole glyph, as well as additional pixels in the glyph at the top
// which would increase GPU memory demands
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
this._tmpCtx.clip(clipRegion);
this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.restore();
}
}
}
}
// Draw the character
if (!customGlyph) {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
}
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
// even on the bottom row, try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
if (isBeyondCellBounds) {
for (let offset = 1; offset <= 5; offset++) {
this._tmpCtx.save();
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.restore();
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
if (!isBeyondCellBounds) {
break;
}
}
}
}
// Draw strokethrough
if (strikethrough) {
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 10));
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
this._tmpCtx.lineWidth = lineWidth;
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
this._tmpCtx.beginPath();
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth * chWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.stroke();
}
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
const imageData = this._tmpCtx.getImageData(
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
);
// Clear out the background color and determine if the glyph is empty.
let isEmpty: boolean;
if (!this._config.allowTransparency) {
isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck);
} else {
isEmpty = checkCompletelyTransparent(imageData);
}
// Handle empty glyphs
if (isEmpty) {
return NULL_RASTERIZED_GLYPH;
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
// Find the best atlas row to use
let activeRow: ICharAtlasActiveRow;
while (true) {
// Select the ideal existing row, preferring fixed rows over the current row
activeRow = this._currentRow;
for (const row of this._fixedRows) {
if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) {
activeRow = row;
}
}
// Create a new one if vertical space would be wasted, fixing the previously active row in the
// process as it now has a fixed height
if (activeRow.height > rasterizedGlyph.size.y * 2) {
// Fix the current row as the new row is being added below
if (this._currentRow.height > 0) {
this._fixedRows.push(this._currentRow);
}
// Create the new fixed height row
activeRow = {
x: 0,
y: this._currentRow.y + this._currentRow.height,
height: rasterizedGlyph.size.y
};
this._fixedRows.push(activeRow);
// Create the new current row below the new fixed height row
this._currentRow = {
x: 0,
y: activeRow.y + activeRow.height,
height: 0
};
}
// Exit the loop if there is enough room in the row
if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) {
break;
}
// If there is enough room in the current row, finish it and try again
if (activeRow === this._currentRow) {
activeRow.x = 0;
activeRow.y += activeRow.height;
activeRow.height = 0;
} else {
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
}
}
// Record texture position
rasterizedGlyph.texturePosition.x = activeRow.x;
rasterizedGlyph.texturePosition.y = activeRow.y;
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
// height
activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y);
activeRow.x += rasterizedGlyph.size.x;
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
return rasterizedGlyph;
}
/**
* Given an ImageData object, find the bounding box of the non-transparent
* portion of the texture and return an IRasterizedGlyph with these
* dimensions.
* @param imageData The image data to read.
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCellWidth : allowedWidth;
let found = false;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.top = y;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.left = 0;
found = false;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.left = x;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.right = width;
found = false;
for (let x = width - 1; x >= 0; x--) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.right = x;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.bottom = height;
found = false;
for (let y = height - 1; y >= 0; y--) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.bottom = y;
found = true;
break;
}
}
if (found) {
break;
}
}
return {
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
size: {
x: boundingBox.right - boundingBox.left + 1,
y: boundingBox.bottom - boundingBox.top + 1
},
sizeClipSpace: {
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
},
offset: {
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.scaledCellWidth - this._config.scaledCharWidth) / 2) : 0),
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
}
};
}
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
const width = boundingBox.right - boundingBox.left + 1;
const height = boundingBox.bottom - boundingBox.top + 1;
const clippedData = new Uint8ClampedArray(width * height * 4);
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
clippedData[newOffset] = imageData.data[oldOffset];
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
}
}
return new ImageData(clippedData, width, height);
}
}
/**
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThresholdCheck: boolean): boolean {
// Get color channels
const r = bg.rgba >>> 24;
const g = bg.rgba >>> 16 & 0xFF;
const b = bg.rgba >>> 8 & 0xFF;
const fgR = fg.rgba >>> 24;
const fgG = fg.rgba >>> 16 & 0xFF;
const fgB = fg.rgba >>> 8 & 0xFF;
// Calculate a threshold that when below a color will be treated as transpart when the sum of
// channel value differs. This helps improve rendering when glyphs overlap with others. This
// threshold is calculated relative to the difference between the background and foreground to
// ensure important details of the glyph are always shown, even when the contrast ratio is low.
// The number 12 is largely arbitrary to ensure the pixels that escape the cell in the test case
// were covered (fg=#8ae234, bg=#c4a000).
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
// Set alpha channel of relevent pixels to 0
let isEmpty = true;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
// Check exact match
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
} else {
// Check the threshold based difference
if (enableThresholdCheck &&
(Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b)) < threshold) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
}
}
}
return isEmpty;
}
function checkCompletelyTransparent(imageData: ImageData): boolean {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset + 3] > 0) {
return false;
}
}
return true;
}
+154
View File
@@ -0,0 +1,154 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight, Terminal } from 'xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IEvent } from 'common/EventEmitter';
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRequestRedrawEvent {
start: number;
end: number;
}
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
/**
* Fires when the renderer is requesting to be redrawn on the next animation
* frame but is _not_ a result of content changing (eg. selection changes).
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
handleDevicePixelRatioChange(): void;
handleResize(cols: number, rows: number): void;
handleCharSizeChanged(): void;
handleBlur(): void;
handleFocus(): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleCursorMove(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
export interface ITextureAtlas extends IDisposable {
readonly cacheCanvas: HTMLCanvasElement;
hasCanvasChanged: boolean;
/**
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
*/
warmUp(): void;
/**
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
* for a new set of glyphs.
*/
beginFrame(): boolean;
/**
* Clear all glyphs from the texture atlas.
*/
clearTexture(): void;
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph;
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph;
}
/**
* Represents a rasterized glyph within a texture atlas. Some numbers are
* tracked in CSS pixels as well in order to reduce calculations during the
* render loop.
*/
export interface IRasterizedGlyph {
/**
* The x and y offset between the glyph's top/left and the top/left of a cell
* in pixels.
*/
offset: IVector;
/**
* the x and y position of the glyph in the texture in pixels.
*/
texturePosition: IVector;
/**
* the x and y position of the glyph in the texture in clip space coordinates.
*/
texturePositionClipSpace: IVector;
/**
* The width and height of the glyph in the texture in pixels.
*/
size: IVector;
/**
* The width and height of the glyph in the texture in clip space coordinates.
*/
sizeClipSpace: IVector;
}
export interface IVector {
x: number;
y: number;
}
export interface IBoundingBox {
top: number;
left: number;
right: number;
bottom: number;
}
export interface ISelectionRenderModel {
readonly hasSelection: boolean;
readonly columnSelectMode: boolean;
readonly viewportStartRow: number;
readonly viewportEndRow: number;
readonly viewportCappedStartRow: number;
readonly viewportCappedEndRow: number;
readonly startCol: number;
readonly endCol: number;
readonly selectionStart: [number, number] | undefined;
readonly selectionEnd: [number, number] | undefined;
clear(): void;
update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void;
isCellSelected(terminal: Terminal, x: number, y: number): boolean;
}
+3 -3
View File
@@ -50,17 +50,17 @@ describe('SelectionModel', () => {
it('should trim a portion of the selection when a part of it is trimmed', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
model.onTrim(1);
model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
model.onTrim(1);
model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
});
it('should clear selection when it is trimmed in its entirety', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 0];
model.onTrim(1);
model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, undefined);
assert.deepEqual(model.finalSelectionEnd, undefined);
});
+1 -1
View File
@@ -120,7 +120,7 @@ export class SelectionModel {
* @param amount The amount the buffer is being trimmed.
* @return Whether a refresh is necessary.
*/
public onTrim(amount: number): boolean {
public handleTrim(amount: number): boolean {
// Adjust the selection position based on the trimmed amount.
if (this.selectionStart) {
this.selectionStart[1] -= amount;
+8 -4
View File
@@ -4,10 +4,12 @@
*/
import { IOptionsService } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { ITerminalOptions } from 'common/Types';
export class CharSizeService implements ICharSizeService {
export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
public width: number = 0;
@@ -16,15 +18,17 @@ export class CharSizeService implements ICharSizeService {
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
private _onCharSizeChange = new EventEmitter<void>();
public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; }
private readonly _onCharSizeChange = this.register(new EventEmitter<void>());
public readonly onCharSizeChange = this._onCharSizeChange.event;
constructor(
document: Document,
parentElement: HTMLElement,
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
public measure(): void {
+85 -49
View File
@@ -3,16 +3,17 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
import { IColorSet, IRenderDebouncerWithCallback, ReadonlyColorSet } from 'browser/Types';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { ThemeService } from 'browser/services/ThemeService';
interface ISelectionState {
start: [number, number] | undefined;
@@ -23,6 +24,7 @@ interface ISelectionState {
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
private _renderer: IRenderer | undefined;
private _renderDebouncer: IRenderDebouncerWithCallback;
private _screenDprMonitor: ScreenDprMonitor;
private _pausedResizeTask = new DebouncedIdleTask();
@@ -39,42 +41,42 @@ export class RenderService extends Disposable implements IRenderService {
columnSelectMode: false
};
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
private _onRender = new EventEmitter<{ start: number, end: number }>();
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; }
private readonly _onDimensionsChange = this.register(new EventEmitter<IRenderDimensions>());
public readonly onDimensionsChange = this._onDimensionsChange.event;
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRender = this._onRender.event;
private readonly _onRefreshRequest = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRefreshRequest = this._onRefreshRequest.event;
public get dimensions(): IRenderDimensions { return this._renderer.dimensions; }
public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; }
constructor(
private _renderer: IRenderer,
private _rowCount: number,
screenElement: HTMLElement,
@IOptionsService optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
@IThemeService themeService: IThemeService
) {
super();
this.register({ dispose: () => this._renderer.dispose() });
this.register({ dispose: () => this._renderer?.dispose() });
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
this.register(this._renderDebouncer);
this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window);
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
this._screenDprMonitor.setListener(() => this.handleDevicePixelRatioChange());
this.register(this._screenDprMonitor);
this.register(bufferService.onResize(() => this._fullRefresh()));
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
this.register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged()));
// Do a full refresh whenever any decoration is added or removed. This may not actually result
// in changes but since decorations should be used sparingly or added/removed all in the same
@@ -82,23 +84,45 @@ export class RenderService extends Disposable implements IRenderService {
this.register(decorationService.onDecorationRegistered(() => this._fullRefresh()));
this.register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
// No need to register this as renderer is explicitly disposed in RenderService.dispose
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
// Clear the renderer when the a change that could affect glyphs occurs
this.register(optionsService.onMultipleOptionChange([
'customGlyphs',
'drawBoldTextInBrightColors',
'letterSpacing',
'lineHeight',
'fontFamily',
'fontSize',
'fontWeight',
'fontWeightBold',
'minimumContrastRatio'
], () => {
this.clear();
this.handleResize(bufferService.cols, bufferService.rows);
this._fullRefresh();
}));
// Refresh the cursor line when the cursor changes
this.register(optionsService.onMultipleOptionChange([
'cursorBlink',
'cursorStyle'
], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true)));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange()));
this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange()));
this.register(themeService.onChangeColors(() => this._fullRefresh()));
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in coreBrowserService.window) {
const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 });
const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
observer.observe(screenElement);
this.register({ dispose: () => observer.disconnect() });
}
}
private _onIntersectionChange(entry: IntersectionObserverEntry): void {
private _handleIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting;
// Terminal was hidden on open
@@ -125,11 +149,14 @@ export class RenderService extends Disposable implements IRenderService {
}
private _renderRows(start: number, end: number): void {
if (!this._renderer) {
return;
}
this._renderer.renderRows(start, end);
// Update selection if needed
if (this._needsSelectionRefresh) {
this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
this._renderer.handleSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
this._needsSelectionRefresh = false;
}
@@ -147,12 +174,17 @@ export class RenderService extends Disposable implements IRenderService {
}
private _handleOptionsChanged(): void {
this._renderer.onOptionsChanged();
if (!this._renderer) {
return;
}
this.refreshRows(0, this._rowCount - 1);
this._fireOnCanvasResize();
}
private _fireOnCanvasResize(): void {
if (!this._renderer) {
return;
}
// Don't fire the event if the dimensions haven't changed
if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) {
return;
@@ -160,13 +192,13 @@ export class RenderService extends Disposable implements IRenderService {
this._onDimensionsChange.fire(this._renderer.dimensions);
}
public dispose(): void {
super.dispose();
public hasRenderer(): boolean {
return !!this._renderer;
}
public setRenderer(renderer: IRenderer): void {
// TODO: RenderService should be the only one to dispose the renderer
this._renderer.dispose();
this._renderer?.dispose();
this._renderer = renderer;
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
@@ -188,58 +220,62 @@ export class RenderService extends Disposable implements IRenderService {
}
public clearTextureAtlas(): void {
this._renderer?.clearTextureAtlas?.();
if (!this._renderer) {
return;
}
this._renderer.clearTextureAtlas?.();
this._fullRefresh();
}
public setColors(colors: IColorSet): void {
this._renderer.setColors(colors);
this._fullRefresh();
}
public onDevicePixelRatioChange(): void {
public handleDevicePixelRatioChange(): void {
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
// when devicePixelRatio changes
this._charSizeService.measure();
this._renderer.onDevicePixelRatioChange();
if (!this._renderer) {
return;
}
this._renderer.handleDevicePixelRatioChange();
this.refreshRows(0, this._rowCount - 1);
}
public onResize(cols: number, rows: number): void {
public handleResize(cols: number, rows: number): void {
if (!this._renderer) {
return;
}
if (this._isPaused) {
this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows));
this._pausedResizeTask.set(() => this._renderer!.handleResize(cols, rows));
} else {
this._renderer.onResize(cols, rows);
this._renderer.handleResize(cols, rows);
}
this._fullRefresh();
}
// TODO: Is this useful when we have onResize?
public onCharSizeChanged(): void {
this._renderer.onCharSizeChanged();
public handleCharSizeChanged(): void {
this._renderer?.handleCharSizeChanged();
}
public onBlur(): void {
this._renderer.onBlur();
public handleBlur(): void {
this._renderer?.handleBlur();
}
public onFocus(): void {
this._renderer.onFocus();
public handleFocus(): void {
this._renderer?.handleFocus();
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionState.start = start;
this._selectionState.end = end;
this._selectionState.columnSelectMode = columnSelectMode;
this._renderer.onSelectionChanged(start, end, columnSelectMode);
this._renderer?.handleSelectionChanged(start, end, columnSelectMode);
}
public onCursorMove(): void {
this._renderer.onCursorMove();
public handleCursorMove(): void {
this._renderer?.handleCursorMove();
}
public clear(): void {
this._renderer.clear();
this._renderer?.clear();
}
}
+31 -31
View File
@@ -15,7 +15,7 @@ import { IBufferRange, ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { getRangeLength } from 'common/buffer/BufferRange';
/**
@@ -111,14 +111,14 @@ export class SelectionService extends Disposable implements ISelectionService {
private _oldSelectionStart: [number, number] | undefined = undefined;
private _oldSelectionEnd: [number, number] | undefined = undefined;
private _onLinuxMouseSelection = this.register(new EventEmitter<string>());
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
private _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
public get onRequestRedraw(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
private _onSelectionChange = this.register(new EventEmitter<void>());
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
public get onRequestScrollLines(): IEvent<ISelectionRequestScrollLinesEvent> { return this._onRequestScrollLines.event; }
private readonly _onLinuxMouseSelection = this.register(new EventEmitter<string>());
public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event;
private readonly _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
public readonly onRequestRedraw = this._onRedrawRequest.event;
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
public readonly onSelectionChange = this._onSelectionChange.event;
private readonly _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
constructor(
private readonly _element: HTMLElement,
@@ -134,24 +134,24 @@ export class SelectionService extends Disposable implements ISelectionService {
super();
// Init listeners
this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent);
this._mouseUpListener = event => this._onMouseUp(event as MouseEvent);
this._mouseMoveListener = event => this._handleMouseMove(event as MouseEvent);
this._mouseUpListener = event => this._handleMouseUp(event as MouseEvent);
this._coreService.onUserInput(() => {
if (this.hasSelection) {
this.clearSelection();
}
});
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount));
this.register(this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)));
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
this.register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e)));
this.enable();
this._model = new SelectionModel(this._bufferService);
this._activeSelectionMode = SelectionMode.NORMAL;
}
public dispose(): void {
this._removeMouseDownListeners();
this.register(toDisposable(() => {
this._removeMouseDownListeners();
}));
}
public reset(): void {
@@ -375,8 +375,8 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
*/
private _onTrim(amount: number): void {
const needsRefresh = this._model.onTrim(amount);
private _handleTrim(amount: number): void {
const needsRefresh = this._model.handleTrim(amount);
if (needsRefresh) {
this.refresh();
}
@@ -438,7 +438,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handles te mousedown event, setting up for a new selection.
* @param event The mousedown event.
*/
public onMouseDown(event: MouseEvent): void {
public handleMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
@@ -468,14 +468,14 @@ export class SelectionService extends Disposable implements ISelectionService {
this._dragScrollAmount = 0;
if (this._enabled && event.shiftKey) {
this._onIncrementalClick(event);
this._handleIncrementalClick(event);
} else {
if (event.detail === 1) {
this._onSingleClick(event);
this._handleSingleClick(event);
} else if (event.detail === 2) {
this._onDoubleClick(event);
this._handleDoubleClick(event);
} else if (event.detail === 3) {
this._onTripleClick(event);
this._handleTripleClick(event);
}
}
@@ -512,7 +512,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* position.
* @param event The mouse event.
*/
private _onIncrementalClick(event: MouseEvent): void {
private _handleIncrementalClick(event: MouseEvent): void {
if (this._model.selectionStart) {
this._model.selectionEnd = this._getMouseBufferCoords(event);
}
@@ -523,7 +523,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* start position.
* @param event The mouse event.
*/
private _onSingleClick(event: MouseEvent): void {
private _handleSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
@@ -557,7 +557,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Performs a double click, selecting the current word.
* @param event The mouse event.
*/
private _onDoubleClick(event: MouseEvent): void {
private _handleDoubleClick(event: MouseEvent): void {
if (this._selectWordAtCursor(event, true)) {
this._activeSelectionMode = SelectionMode.WORD;
}
@@ -568,7 +568,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* select mode.
* @param event The mouse event.
*/
private _onTripleClick(event: MouseEvent): void {
private _handleTripleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.LINE;
@@ -589,7 +589,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* end of the selection and refreshing the selection.
* @param event The mousemove event.
*/
private _onMouseMove(event: MouseEvent): void {
private _handleMouseMove(event: MouseEvent): void {
// If the mousemove listener is active it means that a selection is
// currently being made, we should stop propagation to prevent mouse events
// to be sent to the pty.
@@ -690,7 +690,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handles the mouseup event, removing the mousedown listeners.
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent): void {
private _handleMouseUp(event: MouseEvent): void {
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
@@ -746,14 +746,14 @@ export class SelectionService extends Disposable implements ISelectionService {
this._onSelectionChange.fire();
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
private _handleBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
this._trimListener.dispose();
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
}
/**
+29 -13
View File
@@ -4,11 +4,12 @@
*/
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDisposable } from 'common/Types';
import { ColorIndex, IDisposable } from 'common/Types';
import { ITheme } from 'common/services/Services';
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
export interface ICharSizeService {
@@ -71,16 +72,15 @@ export interface IRenderService extends IDisposable {
refreshRows(start: number, end: number): void;
clearTextureAtlas(): void;
resize(cols: number, rows: number): void;
hasRenderer(): boolean;
setRenderer(renderer: IRenderer): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
// TODO: Is this useful when we have onResize?
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
handleDevicePixelRatioChange(): void;
handleResize(cols: number, rows: number): void;
handleCharSizeChanged(): void;
handleBlur(): void;
handleFocus(): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleCursorMove(): void;
clear(): void;
}
@@ -109,7 +109,7 @@ export interface ISelectionService {
shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean;
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
onMouseDown(event: MouseEvent): void;
handleMouseDown(event: MouseEvent): void;
isCellInSelection(x: number, y: number): boolean;
}
@@ -121,3 +121,19 @@ export interface ICharacterJoinerService {
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export const IThemeService = createDecorator<IThemeService>('ThemeService');
export interface IThemeService {
serviceBrand: undefined;
readonly colors: ReadonlyColorSet;
readonly onChangeColors: IEvent<ReadonlyColorSet>;
restoreColor(slot?: ColorIndex): void;
/**
* Allows external modifying of colors in the theme, this is used instead of {@link colors} to
* prevent accidental writes.
*/
modifyColors(callback: (colors: IColorSet) => void): void;
}
+367
View File
@@ -0,0 +1,367 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ThemeService, DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
import { OptionsService } from 'common/services/OptionsService';
describe('ThemeService', () => {
let themeService: ThemeService;
let dom: jsdom.JSDOM;
let window: jsdom.DOMWindow;
let optionsService: OptionsService;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
fillRect(): void { },
getImageData(): any {
return {data: [0, 0, 0, 0xFF]};
}
});
optionsService = new OptionsService({});
themeService = new ThemeService(optionsService);
});
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(themeService.colors)) {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
// A #rrggbb or rgba(...)
assert.ok((themeService.colors as any)[key].css.length >= 7);
}
}
assert.equal(themeService.colors.ansi.length, 256);
});
it('should fill 240 colors with expected values', () => {
assert.equal(themeService.colors.ansi[16].css, '#000000');
assert.equal(themeService.colors.ansi[17].css, '#00005f');
assert.equal(themeService.colors.ansi[18].css, '#000087');
assert.equal(themeService.colors.ansi[19].css, '#0000af');
assert.equal(themeService.colors.ansi[20].css, '#0000d7');
assert.equal(themeService.colors.ansi[21].css, '#0000ff');
assert.equal(themeService.colors.ansi[22].css, '#005f00');
assert.equal(themeService.colors.ansi[23].css, '#005f5f');
assert.equal(themeService.colors.ansi[24].css, '#005f87');
assert.equal(themeService.colors.ansi[25].css, '#005faf');
assert.equal(themeService.colors.ansi[26].css, '#005fd7');
assert.equal(themeService.colors.ansi[27].css, '#005fff');
assert.equal(themeService.colors.ansi[28].css, '#008700');
assert.equal(themeService.colors.ansi[29].css, '#00875f');
assert.equal(themeService.colors.ansi[30].css, '#008787');
assert.equal(themeService.colors.ansi[31].css, '#0087af');
assert.equal(themeService.colors.ansi[32].css, '#0087d7');
assert.equal(themeService.colors.ansi[33].css, '#0087ff');
assert.equal(themeService.colors.ansi[34].css, '#00af00');
assert.equal(themeService.colors.ansi[35].css, '#00af5f');
assert.equal(themeService.colors.ansi[36].css, '#00af87');
assert.equal(themeService.colors.ansi[37].css, '#00afaf');
assert.equal(themeService.colors.ansi[38].css, '#00afd7');
assert.equal(themeService.colors.ansi[39].css, '#00afff');
assert.equal(themeService.colors.ansi[40].css, '#00d700');
assert.equal(themeService.colors.ansi[41].css, '#00d75f');
assert.equal(themeService.colors.ansi[42].css, '#00d787');
assert.equal(themeService.colors.ansi[43].css, '#00d7af');
assert.equal(themeService.colors.ansi[44].css, '#00d7d7');
assert.equal(themeService.colors.ansi[45].css, '#00d7ff');
assert.equal(themeService.colors.ansi[46].css, '#00ff00');
assert.equal(themeService.colors.ansi[47].css, '#00ff5f');
assert.equal(themeService.colors.ansi[48].css, '#00ff87');
assert.equal(themeService.colors.ansi[49].css, '#00ffaf');
assert.equal(themeService.colors.ansi[50].css, '#00ffd7');
assert.equal(themeService.colors.ansi[51].css, '#00ffff');
assert.equal(themeService.colors.ansi[52].css, '#5f0000');
assert.equal(themeService.colors.ansi[53].css, '#5f005f');
assert.equal(themeService.colors.ansi[54].css, '#5f0087');
assert.equal(themeService.colors.ansi[55].css, '#5f00af');
assert.equal(themeService.colors.ansi[56].css, '#5f00d7');
assert.equal(themeService.colors.ansi[57].css, '#5f00ff');
assert.equal(themeService.colors.ansi[58].css, '#5f5f00');
assert.equal(themeService.colors.ansi[59].css, '#5f5f5f');
assert.equal(themeService.colors.ansi[60].css, '#5f5f87');
assert.equal(themeService.colors.ansi[61].css, '#5f5faf');
assert.equal(themeService.colors.ansi[62].css, '#5f5fd7');
assert.equal(themeService.colors.ansi[63].css, '#5f5fff');
assert.equal(themeService.colors.ansi[64].css, '#5f8700');
assert.equal(themeService.colors.ansi[65].css, '#5f875f');
assert.equal(themeService.colors.ansi[66].css, '#5f8787');
assert.equal(themeService.colors.ansi[67].css, '#5f87af');
assert.equal(themeService.colors.ansi[68].css, '#5f87d7');
assert.equal(themeService.colors.ansi[69].css, '#5f87ff');
assert.equal(themeService.colors.ansi[70].css, '#5faf00');
assert.equal(themeService.colors.ansi[71].css, '#5faf5f');
assert.equal(themeService.colors.ansi[72].css, '#5faf87');
assert.equal(themeService.colors.ansi[73].css, '#5fafaf');
assert.equal(themeService.colors.ansi[74].css, '#5fafd7');
assert.equal(themeService.colors.ansi[75].css, '#5fafff');
assert.equal(themeService.colors.ansi[76].css, '#5fd700');
assert.equal(themeService.colors.ansi[77].css, '#5fd75f');
assert.equal(themeService.colors.ansi[78].css, '#5fd787');
assert.equal(themeService.colors.ansi[79].css, '#5fd7af');
assert.equal(themeService.colors.ansi[80].css, '#5fd7d7');
assert.equal(themeService.colors.ansi[81].css, '#5fd7ff');
assert.equal(themeService.colors.ansi[82].css, '#5fff00');
assert.equal(themeService.colors.ansi[83].css, '#5fff5f');
assert.equal(themeService.colors.ansi[84].css, '#5fff87');
assert.equal(themeService.colors.ansi[85].css, '#5fffaf');
assert.equal(themeService.colors.ansi[86].css, '#5fffd7');
assert.equal(themeService.colors.ansi[87].css, '#5fffff');
assert.equal(themeService.colors.ansi[88].css, '#870000');
assert.equal(themeService.colors.ansi[89].css, '#87005f');
assert.equal(themeService.colors.ansi[90].css, '#870087');
assert.equal(themeService.colors.ansi[91].css, '#8700af');
assert.equal(themeService.colors.ansi[92].css, '#8700d7');
assert.equal(themeService.colors.ansi[93].css, '#8700ff');
assert.equal(themeService.colors.ansi[94].css, '#875f00');
assert.equal(themeService.colors.ansi[95].css, '#875f5f');
assert.equal(themeService.colors.ansi[96].css, '#875f87');
assert.equal(themeService.colors.ansi[97].css, '#875faf');
assert.equal(themeService.colors.ansi[98].css, '#875fd7');
assert.equal(themeService.colors.ansi[99].css, '#875fff');
assert.equal(themeService.colors.ansi[100].css, '#878700');
assert.equal(themeService.colors.ansi[101].css, '#87875f');
assert.equal(themeService.colors.ansi[102].css, '#878787');
assert.equal(themeService.colors.ansi[103].css, '#8787af');
assert.equal(themeService.colors.ansi[104].css, '#8787d7');
assert.equal(themeService.colors.ansi[105].css, '#8787ff');
assert.equal(themeService.colors.ansi[106].css, '#87af00');
assert.equal(themeService.colors.ansi[107].css, '#87af5f');
assert.equal(themeService.colors.ansi[108].css, '#87af87');
assert.equal(themeService.colors.ansi[109].css, '#87afaf');
assert.equal(themeService.colors.ansi[110].css, '#87afd7');
assert.equal(themeService.colors.ansi[111].css, '#87afff');
assert.equal(themeService.colors.ansi[112].css, '#87d700');
assert.equal(themeService.colors.ansi[113].css, '#87d75f');
assert.equal(themeService.colors.ansi[114].css, '#87d787');
assert.equal(themeService.colors.ansi[115].css, '#87d7af');
assert.equal(themeService.colors.ansi[116].css, '#87d7d7');
assert.equal(themeService.colors.ansi[117].css, '#87d7ff');
assert.equal(themeService.colors.ansi[118].css, '#87ff00');
assert.equal(themeService.colors.ansi[119].css, '#87ff5f');
assert.equal(themeService.colors.ansi[120].css, '#87ff87');
assert.equal(themeService.colors.ansi[121].css, '#87ffaf');
assert.equal(themeService.colors.ansi[122].css, '#87ffd7');
assert.equal(themeService.colors.ansi[123].css, '#87ffff');
assert.equal(themeService.colors.ansi[124].css, '#af0000');
assert.equal(themeService.colors.ansi[125].css, '#af005f');
assert.equal(themeService.colors.ansi[126].css, '#af0087');
assert.equal(themeService.colors.ansi[127].css, '#af00af');
assert.equal(themeService.colors.ansi[128].css, '#af00d7');
assert.equal(themeService.colors.ansi[129].css, '#af00ff');
assert.equal(themeService.colors.ansi[130].css, '#af5f00');
assert.equal(themeService.colors.ansi[131].css, '#af5f5f');
assert.equal(themeService.colors.ansi[132].css, '#af5f87');
assert.equal(themeService.colors.ansi[133].css, '#af5faf');
assert.equal(themeService.colors.ansi[134].css, '#af5fd7');
assert.equal(themeService.colors.ansi[135].css, '#af5fff');
assert.equal(themeService.colors.ansi[136].css, '#af8700');
assert.equal(themeService.colors.ansi[137].css, '#af875f');
assert.equal(themeService.colors.ansi[138].css, '#af8787');
assert.equal(themeService.colors.ansi[139].css, '#af87af');
assert.equal(themeService.colors.ansi[140].css, '#af87d7');
assert.equal(themeService.colors.ansi[141].css, '#af87ff');
assert.equal(themeService.colors.ansi[142].css, '#afaf00');
assert.equal(themeService.colors.ansi[143].css, '#afaf5f');
assert.equal(themeService.colors.ansi[144].css, '#afaf87');
assert.equal(themeService.colors.ansi[145].css, '#afafaf');
assert.equal(themeService.colors.ansi[146].css, '#afafd7');
assert.equal(themeService.colors.ansi[147].css, '#afafff');
assert.equal(themeService.colors.ansi[148].css, '#afd700');
assert.equal(themeService.colors.ansi[149].css, '#afd75f');
assert.equal(themeService.colors.ansi[150].css, '#afd787');
assert.equal(themeService.colors.ansi[151].css, '#afd7af');
assert.equal(themeService.colors.ansi[152].css, '#afd7d7');
assert.equal(themeService.colors.ansi[153].css, '#afd7ff');
assert.equal(themeService.colors.ansi[154].css, '#afff00');
assert.equal(themeService.colors.ansi[155].css, '#afff5f');
assert.equal(themeService.colors.ansi[156].css, '#afff87');
assert.equal(themeService.colors.ansi[157].css, '#afffaf');
assert.equal(themeService.colors.ansi[158].css, '#afffd7');
assert.equal(themeService.colors.ansi[159].css, '#afffff');
assert.equal(themeService.colors.ansi[160].css, '#d70000');
assert.equal(themeService.colors.ansi[161].css, '#d7005f');
assert.equal(themeService.colors.ansi[162].css, '#d70087');
assert.equal(themeService.colors.ansi[163].css, '#d700af');
assert.equal(themeService.colors.ansi[164].css, '#d700d7');
assert.equal(themeService.colors.ansi[165].css, '#d700ff');
assert.equal(themeService.colors.ansi[166].css, '#d75f00');
assert.equal(themeService.colors.ansi[167].css, '#d75f5f');
assert.equal(themeService.colors.ansi[168].css, '#d75f87');
assert.equal(themeService.colors.ansi[169].css, '#d75faf');
assert.equal(themeService.colors.ansi[170].css, '#d75fd7');
assert.equal(themeService.colors.ansi[171].css, '#d75fff');
assert.equal(themeService.colors.ansi[172].css, '#d78700');
assert.equal(themeService.colors.ansi[173].css, '#d7875f');
assert.equal(themeService.colors.ansi[174].css, '#d78787');
assert.equal(themeService.colors.ansi[175].css, '#d787af');
assert.equal(themeService.colors.ansi[176].css, '#d787d7');
assert.equal(themeService.colors.ansi[177].css, '#d787ff');
assert.equal(themeService.colors.ansi[178].css, '#d7af00');
assert.equal(themeService.colors.ansi[179].css, '#d7af5f');
assert.equal(themeService.colors.ansi[180].css, '#d7af87');
assert.equal(themeService.colors.ansi[181].css, '#d7afaf');
assert.equal(themeService.colors.ansi[182].css, '#d7afd7');
assert.equal(themeService.colors.ansi[183].css, '#d7afff');
assert.equal(themeService.colors.ansi[184].css, '#d7d700');
assert.equal(themeService.colors.ansi[185].css, '#d7d75f');
assert.equal(themeService.colors.ansi[186].css, '#d7d787');
assert.equal(themeService.colors.ansi[187].css, '#d7d7af');
assert.equal(themeService.colors.ansi[188].css, '#d7d7d7');
assert.equal(themeService.colors.ansi[189].css, '#d7d7ff');
assert.equal(themeService.colors.ansi[190].css, '#d7ff00');
assert.equal(themeService.colors.ansi[191].css, '#d7ff5f');
assert.equal(themeService.colors.ansi[192].css, '#d7ff87');
assert.equal(themeService.colors.ansi[193].css, '#d7ffaf');
assert.equal(themeService.colors.ansi[194].css, '#d7ffd7');
assert.equal(themeService.colors.ansi[195].css, '#d7ffff');
assert.equal(themeService.colors.ansi[196].css, '#ff0000');
assert.equal(themeService.colors.ansi[197].css, '#ff005f');
assert.equal(themeService.colors.ansi[198].css, '#ff0087');
assert.equal(themeService.colors.ansi[199].css, '#ff00af');
assert.equal(themeService.colors.ansi[200].css, '#ff00d7');
assert.equal(themeService.colors.ansi[201].css, '#ff00ff');
assert.equal(themeService.colors.ansi[202].css, '#ff5f00');
assert.equal(themeService.colors.ansi[203].css, '#ff5f5f');
assert.equal(themeService.colors.ansi[204].css, '#ff5f87');
assert.equal(themeService.colors.ansi[205].css, '#ff5faf');
assert.equal(themeService.colors.ansi[206].css, '#ff5fd7');
assert.equal(themeService.colors.ansi[207].css, '#ff5fff');
assert.equal(themeService.colors.ansi[208].css, '#ff8700');
assert.equal(themeService.colors.ansi[209].css, '#ff875f');
assert.equal(themeService.colors.ansi[210].css, '#ff8787');
assert.equal(themeService.colors.ansi[211].css, '#ff87af');
assert.equal(themeService.colors.ansi[212].css, '#ff87d7');
assert.equal(themeService.colors.ansi[213].css, '#ff87ff');
assert.equal(themeService.colors.ansi[214].css, '#ffaf00');
assert.equal(themeService.colors.ansi[215].css, '#ffaf5f');
assert.equal(themeService.colors.ansi[216].css, '#ffaf87');
assert.equal(themeService.colors.ansi[217].css, '#ffafaf');
assert.equal(themeService.colors.ansi[218].css, '#ffafd7');
assert.equal(themeService.colors.ansi[219].css, '#ffafff');
assert.equal(themeService.colors.ansi[220].css, '#ffd700');
assert.equal(themeService.colors.ansi[221].css, '#ffd75f');
assert.equal(themeService.colors.ansi[222].css, '#ffd787');
assert.equal(themeService.colors.ansi[223].css, '#ffd7af');
assert.equal(themeService.colors.ansi[224].css, '#ffd7d7');
assert.equal(themeService.colors.ansi[225].css, '#ffd7ff');
assert.equal(themeService.colors.ansi[226].css, '#ffff00');
assert.equal(themeService.colors.ansi[227].css, '#ffff5f');
assert.equal(themeService.colors.ansi[228].css, '#ffff87');
assert.equal(themeService.colors.ansi[229].css, '#ffffaf');
assert.equal(themeService.colors.ansi[230].css, '#ffffd7');
assert.equal(themeService.colors.ansi[231].css, '#ffffff');
assert.equal(themeService.colors.ansi[232].css, '#080808');
assert.equal(themeService.colors.ansi[233].css, '#121212');
assert.equal(themeService.colors.ansi[234].css, '#1c1c1c');
assert.equal(themeService.colors.ansi[235].css, '#262626');
assert.equal(themeService.colors.ansi[236].css, '#303030');
assert.equal(themeService.colors.ansi[237].css, '#3a3a3a');
assert.equal(themeService.colors.ansi[238].css, '#444444');
assert.equal(themeService.colors.ansi[239].css, '#4e4e4e');
assert.equal(themeService.colors.ansi[240].css, '#585858');
assert.equal(themeService.colors.ansi[241].css, '#626262');
assert.equal(themeService.colors.ansi[242].css, '#6c6c6c');
assert.equal(themeService.colors.ansi[243].css, '#767676');
assert.equal(themeService.colors.ansi[244].css, '#808080');
assert.equal(themeService.colors.ansi[245].css, '#8a8a8a');
assert.equal(themeService.colors.ansi[246].css, '#949494');
assert.equal(themeService.colors.ansi[247].css, '#9e9e9e');
assert.equal(themeService.colors.ansi[248].css, '#a8a8a8');
assert.equal(themeService.colors.ansi[249].css, '#b2b2b2');
assert.equal(themeService.colors.ansi[250].css, '#bcbcbc');
assert.equal(themeService.colors.ansi[251].css, '#c6c6c6');
assert.equal(themeService.colors.ansi[252].css, '#d0d0d0');
assert.equal(themeService.colors.ansi[253].css, '#dadada');
assert.equal(themeService.colors.ansi[254].css, '#e4e4e4');
assert.equal(themeService.colors.ansi[255].css, '#eeeeee');
});
});
describe('setTheme', () => {
it('should not throw when not setting all colors', () => {
assert.doesNotThrow(() => {
optionsService.options.theme = {};
});
});
it('should set a partial set of colors, using the default if not present', () => {
assert.equal(themeService.colors.background.css, '#000000');
assert.equal(themeService.colors.foreground.css, '#ffffff');
optionsService.options.theme = {
background: '#FF0000',
foreground: '#00FF00'
};
assert.equal(themeService.colors.background.css, '#FF0000');
assert.equal(themeService.colors.foreground.css, '#00FF00');
optionsService.options.theme = {
background: '#0000FF'
};
assert.equal(themeService.colors.background.css, '#0000FF');
// FG reverts back to default
assert.equal(themeService.colors.foreground.css, '#ffffff');
});
it('should set all extended ansi colors in reverse order', () => {
optionsService.options.theme = {
extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
};
for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
assert.equal(themeService.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
}
});
it('should set one extended ansi color and keep the other default', () => {
optionsService.options.theme = {
extendedAnsi: ['#ffffff']
};
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
});
it('should set extended ansi colors to the default when they are unset', () => {
optionsService.options.theme = {
extendedAnsi: ['#ffffff']
};
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
optionsService.options.theme = {
extendedAnsi: []
};
assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
optionsService.options.theme = {
extendedAnsi: ['#ffffff']
};
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
optionsService.options.theme = {};
assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
});
it('should set extended ansi colors to the default when they are partially unset', () => {
optionsService.options.theme = {
extendedAnsi: ['#ffffff', '#000000']
};
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
assert.equal(themeService.colors.ansi[17].css, '#000000');
optionsService.options.theme = {
extendedAnsi: ['#ffffff']
};
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
});
});
});
+235
View File
@@ -0,0 +1,235 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { IThemeService } from 'browser/services/Services';
import { IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types';
import { channels, color, css, NULL_COLOR } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IOptionsService, ITheme } from 'common/services/Services';
import { ColorIndex, IColor } 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');
const DEFAULT_CURSOR = css.toColor('#ffffff');
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS.
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
const colors = [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
// Generate colors (16-231)
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
colors.push({
css: channels.toCss(c, c, c),
rgba: channels.toRgba(c, c, c)
});
}
return colors;
})());
export class ThemeService extends Disposable implements IThemeService {
public serviceBrand: undefined;
private _colors: IColorSet;
private _contrastCache: IColorContrastCache;
private _restoreColors!: IRestoreColorSet;
public get colors(): ReadonlyColorSet { return this._colors; }
private readonly _onChangeColors = this.register(new EventEmitter<ReadonlyColorSet>());
public readonly onChangeColors = this._onChangeColors.event;
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._contrastCache = new ColorContrastCache();
this._colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionForeground: undefined,
selectionBackgroundTransparent: DEFAULT_SELECTION,
selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
this._updateRestoreColors();
this._setTheme(this._optionsService.rawOptions.theme);
this.register(this._optionsService.onSpecificOptionChange('minimumContrastRatio', () => this._contrastCache.clear()));
this.register(this._optionsService.onSpecificOptionChange('theme', () => this._setTheme(this._optionsService.rawOptions.theme)));
}
/**
* Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default
* colors will be used where colors are not defined.
*/
private _setTheme(theme: ITheme = {}): void {
const colors = this._colors;
colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND);
colors.background = parseColor(theme.background, DEFAULT_BACKGROUND);
colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR);
colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION);
colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent);
colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent);
colors.selectionInactiveBackgroundOpaque = color.blend(colors.background, colors.selectionInactiveBackgroundTransparent);
colors.selectionForeground = theme.selectionForeground ? parseColor(theme.selectionForeground, NULL_COLOR) : undefined;
if (colors.selectionForeground === NULL_COLOR) {
colors.selectionForeground = undefined;
}
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
*/
if (color.isOpaque(colors.selectionBackgroundTransparent)) {
const opacity = 0.3;
colors.selectionBackgroundTransparent = color.opacity(colors.selectionBackgroundTransparent, opacity);
}
if (color.isOpaque(colors.selectionInactiveBackgroundTransparent)) {
const opacity = 0.3;
colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity);
}
colors.ansi = DEFAULT_ANSI_COLORS.slice();
colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
colors.ansi[2] = parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
colors.ansi[3] = parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
colors.ansi[4] = parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
colors.ansi[5] = parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
colors.ansi[6] = parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
colors.ansi[7] = parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
colors.ansi[8] = parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
colors.ansi[9] = parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
colors.ansi[10] = parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
colors.ansi[11] = parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
colors.ansi[12] = parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
colors.ansi[13] = parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
colors.ansi[14] = parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
colors.ansi[15] = parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
if (theme.extendedAnsi) {
const colorCount = Math.min(colors.ansi.length - 16, theme.extendedAnsi.length);
for (let i = 0; i < colorCount; i++) {
colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
}
}
// Clear our the cache
this._contrastCache.clear();
this._updateRestoreColors();
this._onChangeColors.fire(this.colors);
}
public restoreColor(slot?: ColorIndex): void {
this._restoreColor(slot);
this._onChangeColors.fire(this.colors);
}
private _restoreColor(slot: ColorIndex | undefined): 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];
}
}
public modifyColors(callback: (colors: IColorSet) => void): void {
callback(this._colors);
// Assume the change happened
this._onChangeColors.fire(this.colors);
}
private _updateRestoreColors(): void {
this._restoreColors = {
foreground: this._colors.foreground,
background: this._colors.background,
cursor: this._colors.cursor,
ansi: this._colors.ansi.slice()
};
}
}
function parseColor(
cssString: string | undefined,
fallback: IColor
): IColor {
if (cssString !== undefined) {
try {
return css.toColor(cssString);
} catch {
// no-op
}
}
return fallback;
}
+9 -7
View File
@@ -5,6 +5,7 @@
import { ICircularList } from 'common/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
export interface IInsertEvent {
index: number;
@@ -20,21 +21,22 @@ export interface IDeleteEvent {
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> implements ICircularList<T> {
export class CircularList<T> extends Disposable implements ICircularList<T> {
protected _array: (T | undefined)[];
private _startIndex: number;
private _length: number;
public onDeleteEmitter = new EventEmitter<IDeleteEvent>();
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
public onInsertEmitter = new EventEmitter<IInsertEvent>();
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
public onTrimEmitter = new EventEmitter<number>();
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
public readonly onDeleteEmitter = this.register(new EventEmitter<IDeleteEvent>());
public readonly onDelete = this.onDeleteEmitter.event;
public readonly onInsertEmitter = this.register(new EventEmitter<IInsertEvent>());
public readonly onInsert = this.onInsertEmitter.event;
public readonly onTrimEmitter = this.register(new EventEmitter<number>());
public readonly onTrim = this.onTrimEmitter.event;
constructor(
private _maxLength: number
) {
super();
this._array = new Array<T>(this._maxLength);
this._startIndex = 0;
this._length = 0;
+6 -1
View File
@@ -11,6 +11,11 @@ let $g = 0;
let $b = 0;
let $a = 0;
export const NULL_COLOR: IColor = {
css: '#00000000',
rgba: 0
};
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
@@ -129,7 +134,7 @@ export namespace css {
*/
export function toColor(css: string): IColor {
// Formats: #rgb[a] and #rrggbb[aa]
if (css.match(/#[0-9a-f]{3,8}/i)) {
if (css.match(/#[\da-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
$r = parseInt(css.slice(1, 2).repeat(2), 16);
+37 -51
View File
@@ -21,8 +21,8 @@
* http://linux.die.net/man/7/urxvt
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
@@ -31,7 +31,6 @@ import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
import { DirtyRowService } from 'common/services/DirtyRowService';
import { UnicodeService } from 'common/services/UnicodeService';
import { CharsetService } from 'common/services/CharsetService';
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
@@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _bufferService: IBufferService;
protected readonly _logService: ILogService;
protected readonly _charsetService: ICharsetService;
protected readonly _dirtyRowService: IDirtyRowService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
@@ -61,28 +59,29 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
private _writeBuffer: WriteBuffer;
private _windowsMode: IDisposable | undefined;
private _onBinary = new EventEmitter<string>();
public get onBinary(): IEvent<string> { return this._onBinary.event; }
private _onData = new EventEmitter<string>();
public get onData(): IEvent<string> { return this._onData.event; }
protected _onLineFeed = new EventEmitter<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
protected _onScroll = new EventEmitter<IScrollEvent, void>();
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
protected _onWriteParsed = new EventEmitter<void>();
private readonly _onBinary = this.register(new EventEmitter<string>());
public readonly onBinary = this._onBinary.event;
private readonly _onData = this.register(new EventEmitter<string>());
public readonly onData = this._onData.event;
protected _onLineFeed = this.register(new EventEmitter<void>());
public readonly onLineFeed = this._onLineFeed.event;
private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
public readonly onResize = this._onResize.event;
protected readonly _onWriteParsed = this.register(new EventEmitter<void>());
public readonly onWriteParsed = this._onWriteParsed.event;
/**
* Internally we track the source of the scroll but this is meaningless outside the library so
* it's filtered out.
*/
protected _onScrollApi?: EventEmitter<number, void>;
protected _onScroll = this.register(new EventEmitter<IScrollEvent, void>());
public get onScroll(): IEvent<number, void> {
if (!this._onScrollApi) {
this._onScrollApi = new EventEmitter<number, void>();
this.register(this._onScroll.event(ev => {
this._onScrollApi = this.register(new EventEmitter<number, void>());
this._onScroll.event(ev => {
this._onScrollApi?.fire(ev.position);
}));
});
}
return this._onScrollApi.event;
}
@@ -104,19 +103,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
// Setup and initialize services
this._instantiationService = new InstantiationService();
this.optionsService = new OptionsService(options);
this.optionsService = this.register(new OptionsService(options));
this._instantiationService.setService(IOptionsService, this.optionsService);
this._bufferService = this.register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this._logService = this._instantiationService.createInstance(LogService);
this._logService = this.register(this._instantiationService.createInstance(LogService));
this._instantiationService.setService(ILogService, this._logService);
this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()));
this._instantiationService.setService(ICoreService, this.coreService);
this.coreMouseService = this._instantiationService.createInstance(CoreMouseService);
this.coreMouseService = this.register(this._instantiationService.createInstance(CoreMouseService));
this._instantiationService.setService(ICoreMouseService, this.coreMouseService);
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
this.unicodeService = this._instantiationService.createInstance(UnicodeService);
this.unicodeService = this.register(this._instantiationService.createInstance(UnicodeService));
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
@@ -124,7 +121,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
// Register input handler and handle/forward events
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
this.register(this._inputHandler);
@@ -132,28 +129,25 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(forwardEvent(this._bufferService.onResize, this._onResize));
this.register(forwardEvent(this.coreService.onData, this._onData));
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e)));
this.register(this._bufferService.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
this.register(this._inputHandler.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
// Setup WriteBuffer
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
}
public dispose(): void {
if (this._isDisposed) {
return;
}
super.dispose();
this._windowsMode?.dispose();
this._windowsMode = undefined;
this.register(toDisposable(() => {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}));
}
public write(data: string | Uint8Array, callback?: () => void): void {
@@ -267,20 +261,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.coreMouseService.reset();
}
protected _updateOptions(key: string): void {
// TODO: These listeners should be owned by individual components
switch (key) {
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
break;
case 'windowsMode':
if (this.optionsService.rawOptions.windowsMode) {
this._enableWindowsMode();
} else {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}
break;
private _handleWindowsModeOptionChange(value: boolean): void {
if (value) {
this._enableWindowsMode();
} else {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}
}
+6 -12
View File
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { IBufferService, ICoreService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -67,7 +67,7 @@ describe('InputHandler', () => {
bufferService.resize(80, 30);
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -236,7 +236,7 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle bracketedPasteMode', () => {
const coreService = new MockCoreService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
@@ -258,7 +258,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -305,7 +304,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -356,7 +354,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -394,7 +391,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -445,7 +441,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -572,7 +567,6 @@ describe('InputHandler', () => {
new MockBufferService(80, 30),
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -599,7 +593,7 @@ describe('InputHandler', () => {
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -796,7 +790,7 @@ describe('InputHandler', () => {
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
@@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => {
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
+117 -62
View File
@@ -12,11 +12,11 @@ import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
@@ -104,6 +104,8 @@ export enum WindowsOptionsReportType {
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
// Work variables to avoid garbage collection
let $temp = 0;
/**
* The terminal's standard implementation of IInputHandler, this handles all
@@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _windowTitle = '';
private _iconName = '';
private _currentLinkId?: number;
private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -129,33 +132,33 @@ export class InputHandler extends Disposable implements IInputHandler {
private _activeBuffer: IBuffer;
private _onRequestBell = new EventEmitter<void>();
public get onRequestBell(): IEvent<void> { return this._onRequestBell.event; }
private _onRequestRefreshRows = new EventEmitter<number, number>();
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
private _onRequestReset = new EventEmitter<void>();
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
private _onRequestSendFocus = new EventEmitter<void>();
public get onRequestSendFocus(): IEvent<void> { return this._onRequestSendFocus.event; }
private _onRequestSyncScrollBar = new EventEmitter<void>();
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
public get onRequestWindowsOptionsReport(): IEvent<WindowsOptionsReportType> { return this._onRequestWindowsOptionsReport.event; }
private readonly _onRequestBell = this.register(new EventEmitter<void>());
public readonly onRequestBell = this._onRequestBell.event;
private readonly _onRequestRefreshRows = this.register(new EventEmitter<number, number>());
public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;
private readonly _onRequestReset = this.register(new EventEmitter<void>());
public readonly onRequestReset = this._onRequestReset.event;
private readonly _onRequestSendFocus = this.register(new EventEmitter<void>());
public readonly onRequestSendFocus = this._onRequestSendFocus.event;
private readonly _onRequestSyncScrollBar = this.register(new EventEmitter<void>());
public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;
private readonly _onRequestWindowsOptionsReport = this.register(new EventEmitter<WindowsOptionsReportType>());
public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;
private _onA11yChar = new EventEmitter<string>();
public get onA11yChar(): IEvent<string> { return this._onA11yChar.event; }
private _onA11yTab = new EventEmitter<number>();
public get onA11yTab(): IEvent<number> { return this._onA11yTab.event; }
private _onCursorMove = new EventEmitter<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onLineFeed = new EventEmitter<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onColor = new EventEmitter<IColorEvent>();
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
private readonly _onA11yChar = this.register(new EventEmitter<string>());
public readonly onA11yChar = this._onA11yChar.event;
private readonly _onA11yTab = this.register(new EventEmitter<number>());
public readonly onA11yTab = this._onA11yTab.event;
private readonly _onCursorMove = this.register(new EventEmitter<void>());
public readonly onCursorMove = this._onCursorMove.event;
private readonly _onLineFeed = this.register(new EventEmitter<void>());
public readonly onLineFeed = this._onLineFeed.event;
private readonly _onScroll = this.register(new EventEmitter<number>());
public readonly onScroll = this._onScroll.event;
private readonly _onTitleChange = this.register(new EventEmitter<string>());
public readonly onTitleChange = this._onTitleChange.event;
private readonly _onColor = this.register(new EventEmitter<IColorEvent>());
public readonly onColor = this._onColor.event;
private _parseStack: IParseStack = {
paused: false,
@@ -169,7 +172,6 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
private readonly _coreService: ICoreService,
private readonly _dirtyRowService: IDirtyRowService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
@@ -179,6 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler {
) {
super();
this.register(this._parser);
this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
@@ -379,10 +382,6 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));
}
public dispose(): void {
super.dispose();
}
/**
* Async parse support.
*/
@@ -459,7 +458,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// Clear the dirty row service so we know which lines changed as a result of parsing
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
this._dirtyRowService.clearRange();
this._dirtyRowTracker.clearRange();
}
// process big data in smaller chunks
@@ -493,7 +492,7 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// Refresh any dirty rows accumulated as part of parsing
this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end);
this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end);
}
public print(data: Uint32Array, start: number, end: number): void {
@@ -507,7 +506,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const curAttr = this._curAttrData;
let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
// handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
@@ -635,7 +634,7 @@ export class InputHandler extends Disposable implements IInputHandler {
bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -699,7 +698,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF."
*/
public lineFeed(): boolean {
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._optionsService.rawOptions.convertEol) {
this._activeBuffer.x = 0;
}
@@ -714,7 +713,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._activeBuffer.x >= this._bufferService.cols) {
this._activeBuffer.x--;
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
this._onLineFeed.fire();
return true;
@@ -842,14 +841,14 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = this._coreService.decPrivateModes.origin
? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Set absolute cursor position.
*/
private _setCursor(x: number, y: number): void {
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._coreService.decPrivateModes.origin) {
this._activeBuffer.x = x;
this._activeBuffer.y = this._activeBuffer.scrollTop + y;
@@ -858,7 +857,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = y;
}
this._restrictCursor();
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -1178,16 +1177,16 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params.params[0]) {
case 0:
j = this._activeBuffer.y;
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);
for (; j < this._bufferService.rows; j++) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
break;
case 1:
j = this._activeBuffer.y;
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
@@ -1197,15 +1196,15 @@ export class InputHandler extends Disposable implements IInputHandler {
while (j--) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(0);
this._dirtyRowTracker.markDirty(0);
break;
case 2:
j = this._bufferService.rows;
this._dirtyRowService.markDirty(j - 1);
this._dirtyRowTracker.markDirty(j - 1);
while (j--) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(0);
this._dirtyRowTracker.markDirty(0);
break;
case 3:
// Clear scrollback (everything not in viewport)
@@ -1257,7 +1256,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);
break;
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
return true;
}
@@ -1289,7 +1288,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1323,7 +1322,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1349,7 +1348,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1375,7 +1374,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1395,7 +1394,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1411,7 +1410,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1443,7 +1442,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1476,7 +1475,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1499,7 +1498,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1522,7 +1521,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1544,7 +1543,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -3220,7 +3219,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;
this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);
this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
} else {
this._activeBuffer.y--;
this._restrictCursor(); // quickfix to not run out of bounds
@@ -3293,7 +3292,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.isWrapped = false;
}
}
this._dirtyRowService.markAllDirty();
this._dirtyRowTracker.markAllDirty();
this._setCursor(0, 0);
return true;
}
@@ -3344,4 +3343,60 @@ export class InputHandler extends Disposable implements IInputHandler {
if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);
return f(`P0$r`);
}
public markRangeDirty(y1: number, y2: number): void {
this._dirtyRowTracker.markRangeDirty(y1, y2);
}
}
export interface IDirtyRowTracker {
readonly start: number;
readonly end: number;
clearRange(): void;
markDirty(y: number): void;
markRangeDirty(y1: number, y2: number): void;
markAllDirty(): void;
}
class DirtyRowTracker implements IDirtyRowTracker {
public start!: number;
public end!: number;
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
this.clearRange();
}
public clearRange(): void {
this.start = this._bufferService.buffer.y;
this.end = this._bufferService.buffer.y;
}
public markDirty(y: number): void {
if (y < this.start) {
this.start = y;
} else if (y > this.end) {
this.end = y;
}
}
public markRangeDirty(y1: number, y2: number): void {
if (y1 > y2) {
$temp = y1;
y1 = y2;
y2 = $temp;
}
if (y1 < this.start) {
this.start = y1;
}
if (y2 > this.end) {
this.end = y2;
}
}
public markAllDirty(): void {
this.markRangeDirty(0, this._bufferService.rows - 1);
}
}
+6 -3
View File
@@ -17,15 +17,18 @@ export abstract class Disposable implements IDisposable {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
* Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a
* readonly property instead of a method to prevent subclasses overriding it which is an easy
* mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls
* should be done via {@link register}.
*/
public dispose(): void {
public readonly dispose = (): void => {
this._isDisposed = true;
for (const d of this._disposables) {
d.dispose();
}
this._disposables.length = 0;
}
};
/**
* Registers a disposable object.
+19 -13
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
import { IDecorationOptions, IDecoration } from 'xterm';
@@ -100,16 +100,6 @@ export class MockCoreService implements ICoreService {
public triggerBinaryEvent(data: string): void { }
}
export class MockDirtyRowService implements IDirtyRowService {
public serviceBrand: any;
public start: number = 0;
public end: number = 0;
public clearRange(): void { }
public markDirty(y: number): void { }
public markRangeDirty(y1: number, y2: number): void { }
public markAllDirty(): void { }
}
export class MockLogService implements ILogService {
public serviceBrand: any;
public logLevel = LogLevelEnum.DEBUG;
@@ -123,7 +113,7 @@ export class MockOptionsService implements IOptionsService {
public serviceBrand: any;
public readonly rawOptions: Required<ITerminalOptions> = clone(DEFAULT_OPTIONS);
public options: Required<ITerminalOptions> = this.rawOptions;
public onOptionChange: IEvent<string> = new EventEmitter<string>().event;
public onOptionChange: IEvent<keyof ITerminalOptions> = new EventEmitter<keyof ITerminalOptions>().event;
constructor(testOptions?: Partial<ITerminalOptions>) {
if (testOptions) {
for (const key of Object.keys(testOptions)) {
@@ -131,6 +121,22 @@ export class MockOptionsService implements IOptionsService {
}
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (arg1: ITerminalOptions[T]) => any): IDisposable {
return this.onOptionChange(eventKey => {
if (eventKey === key) {
listener(this.rawOptions[key]);
}
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
return this.onOptionChange(eventKey => {
if (keys.indexOf(eventKey) !== -1) {
listener();
}
});
}
public setOptions(options: ITerminalOptions): void {
for (const key of Object.keys(options)) {
this.options[key] = options[key];
+2
View File
@@ -168,6 +168,7 @@ export class Buffer implements IBuffer {
// Deal with columns increasing (reducing needs to happen after reflow)
if (this._cols < newCols) {
for (let i = 0; i < this.lines.length; i++) {
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
@@ -247,6 +248,7 @@ export class Buffer implements IBuffer {
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
+6 -8
View File
@@ -37,10 +37,8 @@ const enum Cell {
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
/** Work variables to avoid garbage collection. */
const w: { startIndex: number } = {
startIndex: 0
};
// Work variables to avoid garbage collection
let $startIndex = 0;
/** Factor when to cleanup underlying array buffer after shrinking. */
const CLEANUP_THRESHOLD = 2;
@@ -181,10 +179,10 @@ export class BufferLine implements IBufferLine {
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
w.startIndex = index * CELL_SIZE;
cell.content = this._data[w.startIndex + Cell.CONTENT];
cell.fg = this._data[w.startIndex + Cell.FG];
cell.bg = this._data[w.startIndex + Cell.BG];
$startIndex = index * CELL_SIZE;
cell.content = this._data[$startIndex + Cell.CONTENT];
cell.fg = this._data[$startIndex + Cell.FG];
cell.bg = this._data[$startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
+5 -2
View File
@@ -19,8 +19,8 @@ export class BufferSet extends Disposable implements IBufferSet {
private _alt!: Buffer;
private _activeBuffer!: Buffer;
private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
private readonly _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
public readonly onBufferActivate = this._onBufferActivate.event;
/**
* Create a new BufferSet for the given terminal.
@@ -32,6 +32,8 @@ export class BufferSet extends Disposable implements IBufferSet {
) {
super();
this.reset();
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));
this.register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));
}
public reset(): void {
@@ -119,6 +121,7 @@ export class BufferSet extends Disposable implements IBufferSet {
public resize(newCols: number, newRows: number): void {
this._normal.resize(newCols, newRows);
this._alt.resize(newCols, newRows);
this.setupTabStops(newCols);
}
/**
+15 -9
View File
@@ -3,25 +3,25 @@
* @license MIT
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IMarker } from 'common/Types';
import { EventEmitter } from 'common/EventEmitter';
import { disposeArray } from 'common/Lifecycle';
import { IDisposable, IMarker } from 'common/Types';
export class Marker extends Disposable implements IMarker {
export class Marker implements IMarker {
private static _nextId = 1;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
private _disposables: IDisposable[] = [];
private _id: number = Marker._nextId++;
public get id(): number { return this._id; }
private _onDispose = new EventEmitter<void>();
public get onDispose(): IEvent<void> { return this._onDispose.event; }
private readonly _onDispose = this.register(new EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
constructor(
public line: number
) {
super();
}
public dispose(): void {
@@ -32,6 +32,12 @@ export class Marker extends Disposable implements IMarker {
this.line = -1;
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
super.dispose();
disposeArray(this._disposables);
this._disposables.length = 0;
}
public register<T extends IDisposable>(disposable: T): T {
this._disposables.push(disposable);
return disposable;
}
}
+35 -28
View File
@@ -121,35 +121,42 @@ describe('text encodings', () => {
describe('Utf8ToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535 (1/2/3 byte sequences)', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 0; i < 65536; ++i) {
// skip surrogate pairs and a BOM
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
continue;
function formatRange(min: number, max: number): string {
return `${min}..${max} (0x${min.toString(16).toUpperCase()}..0x${max.toString(16).toUpperCase()})`;
}
for (let min = 0; min < 65535; min += 10000) {
const max = Math.min(min + 10000, 65536);
it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = min; i < max; ++i) {
// skip surrogate pairs and a BOM
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
continue;
}
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(toString(target, length), String.fromCharCode(i));
decoder.clear();
}
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(toString(target, length), String.fromCharCode(i));
decoder.clear();
}
});
it('65536..0x10FFFF (4 byte sequences)', function (): void {
this.timeout(20000);
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 65536; i < 0x10FFFF; ++i) {
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(target[0], i);
decoder.clear();
}
});
});
}
for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += 10000) {
const min = Math.max(minRaw, 65536);
const max = Math.min(minRaw + 10000, 0x10FFFF);
it(`${formatRange(min, max)} (4 byte sequences)`, function (): void {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = min; i < max; ++i) {
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(target[0], i);
decoder.clear();
}
});
}
it('0xFEFF(BOM)', () => {
const decoder = new Utf8ToUtf32();
+27 -5
View File
@@ -5,6 +5,7 @@
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
declare const setTimeout: (handler: () => void, timeout?: number) => void;
@@ -33,17 +34,25 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
export class WriteBuffer {
export class WriteBuffer extends Disposable {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
private _isSyncWriting = false;
private _syncCalls = 0;
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
private _onWriteParsed = new EventEmitter<void>();
private _didUserInput = false;
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
private readonly _onWriteParsed = this.register(new EventEmitter<void>());
public readonly onWriteParsed = this._onWriteParsed.event;
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) {
super();
}
public handleUserInput(): void {
this._didUserInput = true;
}
/**
* @deprecated Unreliable, to be removed soon.
@@ -99,7 +108,20 @@ export class WriteBuffer {
// schedule chunk processing for next event loop run
if (!this._writeBuffer.length) {
this._bufferOffset = 0;
queueMicrotask(() => this._innerWrite());
// If this is the first write call after the user has done some input,
// parse it immediately to minimize input latency,
// otherwise schedule for the next event
if (this._didUserInput) {
this._didUserInput = false;
this._pendingData += data.length;
this._writeBuffer.push(data);
this._callbacks.push(callback);
this._innerWrite();
return;
}
setTimeout(() => this._innerWrite());
}
this._pendingData += data.length;
@@ -87,7 +87,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
}
}
public mockOscParser(): void {
this._oscParser = oscPutParser;
(this as any)._oscParser = oscPutParser;
}
public identifier(id: IFunctionIdentifier): number {
return this._identifier(id);
+10 -13
View File
@@ -5,7 +5,7 @@
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
@@ -242,8 +242,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected _oscParser: IOscParser;
protected _dcsParser: IDcsParser;
protected readonly _oscParser: IOscParser;
protected readonly _dcsParser: IDcsParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
@@ -284,8 +284,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscParser = new OscParser();
this._dcsParser = new DcsParser();
this.register(toDisposable(() => {
this._csiHandlers = Object.create(null);
this._executeHandlers = Object.create(null);
this._escHandlers = Object.create(null);
}));
this._oscParser = this.register(new OscParser());
this._dcsParser = this.register(new DcsParser());
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
@@ -338,14 +343,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
return res.reverse().join('');
}
public dispose(): void {
this._csiHandlers = Object.create(null);
this._executeHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscParser.dispose();
this._dcsParser.dispose();
}
public setPrintHandler(handler: PrintHandlerType): void {
this._printHandler = handler;
}
+4 -3
View File
@@ -5,14 +5,15 @@
import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm';
import { BufferApiView } from 'common/public/BufferApiView';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { EventEmitter } from 'common/EventEmitter';
import { ICoreTerminal } from 'common/Types';
export class BufferNamespaceApi implements IBufferNamespaceApi {
private _normal: BufferApiView;
private _alternate: BufferApiView;
private _onBufferChange = new EventEmitter<IBufferApi>();
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
private readonly _onBufferChange = new EventEmitter<IBufferApi>();
public readonly onBufferChange = this._onBufferChange.event;
constructor(private _core: ICoreTerminal) {
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
+7 -12
View File
@@ -6,7 +6,7 @@
import { IBufferService, IOptionsService } from 'common/services/Services';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { EventEmitter, IEventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
@@ -22,10 +22,10 @@ export class BufferService extends Disposable implements IBufferService {
/** Whether the user is scrolling (locks the scroll position) */
public isUserScrolling: boolean = false;
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
private _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
public readonly onResize = this._onResize.event;
private readonly _onScroll = this.register(new EventEmitter<number>());
public readonly onScroll = this._onScroll.event;
public get buffer(): IBuffer { return this.buffers.active; }
@@ -36,19 +36,14 @@ export class BufferService extends Disposable implements IBufferService {
super();
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = new BufferSet(optionsService, this);
}
public dispose(): void {
super.dispose();
this.buffers.dispose();
this.buffers = this.register(new BufferSet(optionsService, this));
}
public resize(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
this.buffers.resize(cols, rows);
this.buffers.setupTabStops(this.cols);
// TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event
this._onResize.fire({ cols, rows });
}
+6 -9
View File
@@ -5,6 +5,7 @@
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
/**
* Supported default protocols.
@@ -165,18 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
* a tracking report to the backend based on protocol and encoding limitations.
* To send a mouse event call `triggerMouseEvent`.
*/
export class CoreMouseService implements ICoreMouseService {
export class CoreMouseService extends Disposable implements ICoreMouseService {
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
private _encodings: { [name: string]: CoreMouseEncoding } = {};
private _activeProtocol: string = '';
private _activeEncoding: string = '';
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
private _lastEvent: ICoreMouseEvent | null = null;
private readonly _onProtocolChange = this.register(new EventEmitter<CoreMouseEventType>());
public readonly onProtocolChange = this._onProtocolChange.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService
) {
super();
// register default protocols and encodings
for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);
for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);
@@ -225,13 +229,6 @@ export class CoreMouseService implements ICoreMouseService {
this._lastEvent = null;
}
/**
* Event to announce changes in mouse tracking.
*/
public get onProtocolChange(): IEvent<CoreMouseEventType> {
return this._onProtocolChange.event;
}
/**
* Triggers a mouse event to be sent.
*
+6 -6
View File
@@ -34,12 +34,12 @@ export class CoreService extends Disposable implements ICoreService {
// Circular dependency, this must be unset or memory will leak after Terminal.dispose
private _scrollToBottom: (() => void) | undefined;
private _onData = this.register(new EventEmitter<string>());
public get onData(): IEvent<string> { return this._onData.event; }
private _onUserInput = this.register(new EventEmitter<void>());
public get onUserInput(): IEvent<void> { return this._onUserInput.event; }
private _onBinary = this.register(new EventEmitter<string>());
public get onBinary(): IEvent<string> { return this._onBinary.event; }
private readonly _onData = this.register(new EventEmitter<string>());
public readonly onData = this._onData.event;
private readonly _onUserInput = this.register(new EventEmitter<void>());
public readonly onUserInput = this._onUserInput.event;
private readonly _onBinary = this.register(new EventEmitter<string>());
public readonly onBinary = this._onBinary.event;
constructor(
// TODO: Move this into a service
+30 -30
View File
@@ -5,17 +5,15 @@
import { css } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
import { SortedList } from 'common/SortedList';
import { IColor } from 'common/Types';
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
/** Work variables to avoid garbage collection. */
const w = {
xmin: 0,
xmax: 0
};
// Work variables to avoid garbage collection
let $xmin = 0;
let $xmax = 0;
export class DecorationService extends Disposable implements IDecorationService {
public serviceBrand: any;
@@ -27,13 +25,23 @@ export class DecorationService extends Disposable implements IDecorationService
*/
private readonly _decorations: SortedList<IInternalDecoration> = new SortedList(e => e?.marker.line);
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRegistered(): IEvent<IInternalDecoration> { return this._onDecorationRegistered.event; }
private _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRemoved(): IEvent<IInternalDecoration> { return this._onDecorationRemoved.event; }
private readonly _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
private readonly _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
public readonly onDecorationRemoved = this._onDecorationRemoved.event;
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
constructor() {
super();
this.register(toDisposable(() => {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}));
}
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
if (options.marker.isDisposed) {
return undefined;
@@ -76,32 +84,26 @@ export class DecorationService extends Disposable implements IDecorationService
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
this._decorations.forEachByKey(line, d => {
w.xmin = d.options.x ?? 0;
w.xmax = w.xmin + (d.options.width ?? 1);
if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
$xmin = d.options.x ?? 0;
$xmax = $xmin + (d.options.width ?? 1);
if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
callback(d);
}
});
}
public dispose(): void {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}
}
class Decoration extends Disposable implements IInternalDecoration {
public readonly marker: IMarker;
public element: HTMLElement | undefined;
public isDisposed: boolean = false;
public readonly onRenderEmitter = this.register(new EventEmitter<HTMLElement>());
public readonly onRender = this.onRenderEmitter.event;
private _onDispose = this.register(new EventEmitter<void>());
private readonly _onDispose = this.register(new EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
public get isDisposed(): boolean { return this._isDisposed; }
private _cachedBg: IColor | undefined | null = null;
public get backgroundColorRGB(): IColor | undefined {
if (this._cachedBg === null) {
@@ -134,14 +136,12 @@ class Decoration extends Disposable implements IInternalDecoration {
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
this.options.overviewRulerOptions.position = 'full';
}
}
public override dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._onDispose.fire();
super.dispose();
this.register(toDisposable(() => {
if (this._isDisposed) {
return;
}
this._onDispose.fire();
}));
}
}
-53
View File
@@ -1,53 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferService, IDirtyRowService } from 'common/services/Services';
export class DirtyRowService implements IDirtyRowService {
public serviceBrand: any;
private _start!: number;
private _end!: number;
public get start(): number { return this._start; }
public get end(): number { return this._end; }
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
this.clearRange();
}
public clearRange(): void {
this._start = this._bufferService.buffer.y;
this._end = this._bufferService.buffer.y;
}
public markDirty(y: number): void {
if (y < this._start) {
this._start = y;
} else if (y > this._end) {
this._end = y;
}
}
public markRangeDirty(y1: number, y2: number): void {
if (y1 > y2) {
const temp = y1;
y1 = y2;
y2 = temp;
}
if (y1 < this._start) {
this._start = y1;
}
if (y2 > this._end) {
this._end = y2;
}
}
public markAllDirty(): void {
this.markRangeDirty(0, this._bufferService.rows - 1);
}
}
+3 -1
View File
@@ -29,7 +29,9 @@ export class ServiceCollection {
}
public forEach(callback: (id: IServiceIdentifier<any>, instance: any) => any): void {
this._entries.forEach((value, key) => callback(key, value));
for (const [key, value] of this._entries.entries()) {
callback(key, value);
}
}
public has(id: IServiceIdentifier<any>): boolean {
+4 -6
View File
@@ -3,6 +3,7 @@
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
type LogType = (message?: any, ...optionalParams: any[]) => void;
@@ -29,7 +30,7 @@ const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
const LOG_PREFIX = 'xterm.js: ';
export class LogService implements ILogService {
export class LogService extends Disposable implements ILogService {
public serviceBrand: any;
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
@@ -37,12 +38,9 @@ export class LogService implements ILogService {
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._updateLogLevel();
this._optionsService.onOptionChange(key => {
if (key === 'logLevel') {
this._updateLogLevel();
}
});
this.register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));
}
private _updateLogLevel(): void {
+77 -1
View File
@@ -5,6 +5,7 @@
import { assert } from 'chai';
import { OptionsService, DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IDisposable } from 'common/Types';
describe('OptionsService', () => {
describe('constructor', () => {
@@ -59,7 +60,7 @@ describe('OptionsService', () => {
});
it('normalizes invalid fontWeight option values', () => {
service.options.fontWeight = 350;
assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing';
assert.doesNotThrow(() => service.options.fontWeight = 10000, 'fontWeight should be normalized instead of throwing');
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default');
service.options.fontWeight = 350;
@@ -71,4 +72,79 @@ describe('OptionsService', () => {
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default');
});
});
describe('onOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire on any option change', async () => {
let disposable: IDisposable;
await new Promise<void>(r => {
disposable = service.onOptionChange(e => {
assert.strictEqual(e, 'cursorWidth');
r();
});
service.options.cursorWidth = 10;
});
disposable!.dispose();
await new Promise<void>(r => {
service.onOptionChange(e => {
assert.strictEqual(e, 'scrollback');
r();
});
service.options.scrollback = 20;
});
});
});
describe('onSpecificOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only on a specific option change', async () => {
await new Promise<void>(r => {
service.onSpecificOptionChange('scrollback', e => {
assert.strictEqual(e, 20);
r();
});
service.options.cursorWidth = 10;
service.options.scrollback = 20;
});
});
});
describe('onSpecificOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only on a specific option change', async () => {
await new Promise<void>(r => {
service.onSpecificOptionChange('scrollback', e => {
assert.strictEqual(e, 20);
r();
});
service.options.cursorWidth = 10;
service.options.scrollback = 20;
});
});
});
describe('onMultipleOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only for specific options', async () => {
await new Promise<void>(r => {
let called = false;
service.onMultipleOptionChange(['scrollback'], () => {
called = true;
});
service.options.cursorWidth = 10;
assert.notOk(called);
service.options.scrollback = 20;
assert.ok(called);
r();
});
});
});
});
+24 -4
View File
@@ -6,7 +6,8 @@
import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { isMac } from 'common/Platform';
import { CursorStyle } from 'common/Types';
import { CursorStyle, IDisposable } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
cols: 80,
@@ -51,16 +52,17 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
export class OptionsService implements IOptionsService {
export class OptionsService extends Disposable implements IOptionsService {
public serviceBrand: any;
public readonly rawOptions: Required<ITerminalOptions>;
public options: Required<ITerminalOptions>;
private _onOptionChange = new EventEmitter<string>();
public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; }
private readonly _onOptionChange = this.register(new EventEmitter<keyof ITerminalOptions>());
public readonly onOptionChange = this._onOptionChange.event;
constructor(options: Partial<ITerminalOptions>) {
super();
// set the default value of each option
const defaultOptions = { ...DEFAULT_OPTIONS };
for (const key in options) {
@@ -80,6 +82,24 @@ export class OptionsService implements IOptionsService {
this._setupOptions();
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {
return this.onOptionChange(eventKey => {
if (eventKey === key) {
listener(this.rawOptions[key]);
}
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
return this.onOptionChange(eventKey => {
if (keys.indexOf(eventKey) !== -1) {
listener();
}
});
}
private _setupOptions(): void {
const getter = (propName: string): any => {
if (!(propName in DEFAULT_OPTIONS)) {
+25 -14
View File
@@ -122,19 +122,6 @@ export interface ICharsetService {
setgCharset(g: number, charset: ICharset | undefined): void;
}
export const IDirtyRowService = createDecorator<IDirtyRowService>('DirtyRowService');
export interface IDirtyRowService {
serviceBrand: undefined;
readonly start: number;
readonly end: number;
clearRange(): void;
markDirty(y: number): void;
markRangeDirty(y1: number, y2: number): void;
markAllDirty(): void;
}
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
@@ -195,9 +182,33 @@ export interface IOptionsService {
* internally.
*/
readonly rawOptions: Required<ITerminalOptions>;
/**
* Options as exposed through the public API, this property uses getters and setters with
* validation which makes it safer but slower. {@link rawOptions} should be used for pretty much
* all internal usage for performance reasons.
*/
readonly options: Required<ITerminalOptions>;
readonly onOptionChange: IEvent<string>;
/**
* Adds an event listener for when any option changes.
*/
readonly onOptionChange: IEvent<keyof ITerminalOptions>;
/**
* Adds an event listener for when a specific option changes, this is a convenience method that is
* preferred over {@link onOptionChange} when only a single option is being listened to.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (arg1: Required<ITerminalOptions>[T]) => any): IDisposable;
/**
* Adds an event listener for when a set of specific options change, this is a convenience method
* that is preferred over {@link onOptionChange} when multiple options are being listened to and
* handled the same way.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;
}
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
+7 -3
View File
@@ -6,15 +6,15 @@ import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Servic
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { UnicodeV6 } from 'common/input/UnicodeV6';
export class UnicodeService implements IUnicodeService {
public serviceBrand: any;
private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);
private _active: string = '';
private _activeProvider: IUnicodeVersionProvider;
private _onChange = new EventEmitter<string>();
public get onChange(): IEvent<string> { return this._onChange.event; }
private readonly _onChange = new EventEmitter<string>();
public readonly onChange = this._onChange.event;
constructor() {
const defaultProvider = new UnicodeV6();
@@ -23,6 +23,10 @@ export class UnicodeService implements IUnicodeService {
this._activeProvider = defaultProvider;
}
public dispose(): void {
this._onChange.dispose();
}
public get versions(): string[] {
return Object.keys(this._providers);
}
+10 -28
View File
@@ -32,17 +32,16 @@ export class Terminal extends CoreTerminal {
// TODO: We should remove options once components adopt optionsService
public get options(): Required<IInitializedTerminalOptions> { return this.optionsService.options; }
private _onBell = new EventEmitter<void>();
public get onBell(): IEvent<void> { return this._onBell.event; }
private _onCursorMove = new EventEmitter<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onA11yCharEmitter = new EventEmitter<string>();
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
private _onA11yTabEmitter = new EventEmitter<number>();
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
private readonly _onBell = this.register(new EventEmitter<void>());
public readonly onBell = this._onBell.event;
private readonly _onCursorMove = this.register(new EventEmitter<void>());
public readonly onCursorMove = this._onCursorMove.event;
private readonly _onTitleChange = this.register(new EventEmitter<string>());
public readonly onTitleChange = this._onTitleChange.event;
private readonly _onA11yCharEmitter = this.register(new EventEmitter<string>());
public readonly onA11yChar = this._onA11yCharEmitter.event;
private readonly _onA11yTabEmitter = this.register(new EventEmitter<number>());
public readonly onA11yTab = this._onA11yTabEmitter.event;
/**
* Creates a new `Terminal` object.
@@ -72,14 +71,6 @@ export class Terminal extends CoreTerminal {
this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
}
public dispose(): void {
if (this._isDisposed) {
return;
}
super.dispose();
this.write = () => { };
}
/**
* Convenience property to active buffer.
*/
@@ -87,15 +78,6 @@ export class Terminal extends CoreTerminal {
return this.buffers.active;
}
protected _updateOptions(key: string): void {
super._updateOptions(key);
// TODO: These listeners should be owned by individual components
switch (key) {
case 'tabStopWidth': this.buffers.setupTabStops(); break;
}
}
// TODO: Support paste here?
public get markers(): IMarker[] {