Merge branch 'master' into buffer_optimizations
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 }));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+6
-9
@@ -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
@@ -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);
|
||||
|
||||
Vendored
-56
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user