Merge branch 'master' into tyriar/dim_bg

This commit is contained in:
Daniel Imms
2022-07-29 10:20:22 -07:00
committed by GitHub
31 changed files with 328 additions and 172 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-canvas", "name": "xterm-addon-canvas",
"version": "0.12.0", "version": "0.1.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -30,9 +30,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0; private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0; private _scaledCharTop: number = 0;
private _selectionStart: [number, number] | undefined; protected _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined; protected _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false; protected _columnSelectMode: boolean = false;
protected _charAtlas: BaseCharAtlas | undefined; protected _charAtlas: BaseCharAtlas | undefined;
@@ -49,6 +49,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
italic: false italic: false
}; };
public get canvas(): HTMLCanvasElement { return this._canvas; }
constructor( constructor(
private _container: HTMLElement, private _container: HTMLElement,
id: string, id: string,
+10 -5
View File
@@ -3,10 +3,10 @@
* @license MIT * @license MIT
*/ */
import { IRenderService } from 'browser/services/Services'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer'; import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, IInstantiationService } from 'common/services/Services'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm'; import { ITerminalAddon, Terminal } from 'xterm';
export class CanvasAddon implements ITerminalAddon { export class CanvasAddon implements ITerminalAddon {
@@ -18,13 +18,18 @@ export class CanvasAddon implements ITerminalAddon {
throw new Error('Cannot activate CanvasAddon before Terminal.open'); throw new Error('Cannot activate CanvasAddon before Terminal.open');
} }
this._terminal = terminal; this._terminal = terminal;
const instantiationService: IInstantiationService = (terminal as any)._core._instantiationService; const bufferService: IBufferService = (terminal as any)._core._bufferService;
const bufferService: IBufferService = (terminal as any)._core._renderService;
const renderService: IRenderService = (terminal as any)._core._renderService; const renderService: IRenderService = (terminal as any)._core._renderService;
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService;
const coreService: ICoreService = (terminal as any)._core.coreService;
const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
const optionsService: IOptionsService = (terminal as any)._core.optionsService;
const colors: IColorSet = (terminal as any)._core._colorManager.colors; const colors: IColorSet = (terminal as any)._core._colorManager.colors;
const screenElement: HTMLElement = (terminal as any)._core.screenElement; const screenElement: HTMLElement = (terminal as any)._core.screenElement;
const linkifier = (terminal as any)._core.linkifier2; const linkifier = (terminal as any)._core.linkifier2;
this._renderer = instantiationService.createInstance(CanvasRenderer, colors, screenElement, linkifier); this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
renderService.setRenderer(this._renderer); renderService.setRenderer(this._renderer);
renderService.onResize(bufferService.cols, bufferService.rows); renderService.onResize(bufferService.cols, bufferService.rows);
} }
+32 -46
View File
@@ -11,10 +11,11 @@ import { IRenderLayer } from './Types';
import { LinkRenderLayer } from './LinkRenderLayer'; import { LinkRenderLayer } from './LinkRenderLayer';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier2 } from 'browser/Types'; import { IColorSet, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; import { IBufferService, IOptionsService, IInstantiationService, IDecorationService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from './atlas/CharAtlasCache'; import { removeTerminalFromCache } from './atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter'; import { EventEmitter, IEvent } from 'common/EventEmitter';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
let nextRendererId = 1; let nextRendererId = 1;
@@ -33,18 +34,21 @@ export class CanvasRenderer extends Disposable implements IRenderer {
private _colors: IColorSet, private _colors: IColorSet,
private readonly _screenElement: HTMLElement, private readonly _screenElement: HTMLElement,
linkifier2: ILinkifier2, linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService, private readonly _bufferService: IBufferService,
@IBufferService private readonly _bufferService: IBufferService, private readonly _charSizeService: ICharSizeService,
@ICharSizeService private readonly _charSizeService: ICharSizeService, private readonly _optionsService: IOptionsService,
@IOptionsService private readonly _optionsService: IOptionsService characterJoinerService: ICharacterJoinerService,
coreService: ICoreService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService
) { ) {
super(); super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency; const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [ this._renderLayers = [
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id), new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id), new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._optionsService, decorationService),
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier2), new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw) new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, coreBrowserService, decorationService)
]; ];
this.dimensions = { this.dimensions = {
scaledCharWidth: 0, scaledCharWidth: 0,
@@ -62,6 +66,9 @@ export class CanvasRenderer extends Disposable implements IRenderer {
}; };
this._devicePixelRatio = window.devicePixelRatio; this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions(); this._updateDimensions();
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this.onOptionsChanged(); this.onOptionsChanged();
} }
@@ -167,53 +174,32 @@ export class CanvasRenderer extends Disposable implements IRenderer {
return; return;
} }
// Calculate the scaled character width. Width is floored as it must be // See the WebGL renderer for an explanation of this section.
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio); this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2); this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. ICharSizeService.width/height by itself
// is insufficient when the page is not at 100% zoom level as it's measured
// in CSS pixels, but the actual char size on the canvas can differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
} }
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
this.dimensions.scaledCanvasHeight = height;
this.dimensions.scaledCanvasWidth = width;
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
this._requestRedrawViewport();
}
private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
} }
@@ -37,11 +37,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
colors: IColorSet, colors: IColorSet,
rendererId: number, rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>, private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
@IBufferService bufferService: IBufferService, bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService, optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService, private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, private readonly _coreBrowserService: ICoreBrowserService,
@IDecorationService decorationService: IDecorationService decorationService: IDecorationService
) { ) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
this._state = { this._state = {
@@ -19,9 +19,9 @@ export class LinkRenderLayer extends BaseRenderLayer {
colors: IColorSet, colors: IColorSet,
rendererId: number, rendererId: number,
linkifier2: ILinkifier2, linkifier2: ILinkifier2,
@IBufferService bufferService: IBufferService, bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService, optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService decorationService: IDecorationService
) { ) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
@@ -23,9 +23,9 @@ export class SelectionRenderLayer extends BaseRenderLayer {
zIndex: number, zIndex: number,
colors: IColorSet, colors: IColorSet,
rendererId: number, rendererId: number,
@IBufferService bufferService: IBufferService, bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService, optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService decorationService: IDecorationService
) { ) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
this._clearState(); this._clearState();
@@ -42,8 +42,11 @@ export class SelectionRenderLayer extends BaseRenderLayer {
public resize(dim: IRenderDimensions): void { public resize(dim: IRenderDimensions): void {
super.resize(dim); super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state // On resize use the base render layer's cached selection values since resize clears _state
this._clearState(); // inside reset.
if (this._selectionStart && this._selectionEnd) {
this.onSelectionChanged(this._selectionStart, this._selectionEnd, this._columnSelectMode);
}
} }
public reset(): void { public reset(): void {
@@ -36,10 +36,10 @@ export class TextRenderLayer extends BaseRenderLayer {
colors: IColorSet, colors: IColorSet,
alpha: boolean, alpha: boolean,
rendererId: number, rendererId: number,
@IBufferService bufferService: IBufferService, bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService, optionsService: IOptionsService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, private readonly _characterJoinerService: ICharacterJoinerService,
@IDecorationService decorationService: IDecorationService decorationService: IDecorationService
) { ) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService);
this._state = new GridCache<CharData>(); this._state = new GridCache<CharData>();
+2
View File
@@ -57,6 +57,8 @@ export interface IRenderer extends IDisposable {
} }
export interface IRenderLayer extends IDisposable { export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
/** /**
* Called when the terminal loses focus. * Called when the terminal loses focus.
*/ */
@@ -21,7 +21,6 @@
}, },
"strict": true, "strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true,
"types": [ "types": [
"../../../node_modules/@types/mocha" "../../../node_modules/@types/mocha"
] ]
@@ -22,10 +22,13 @@ const fontsFolder = path.join(__dirname, '../fonts');
async function download() { async function download() {
await mkdirp(fontsFolder); await mkdirp(fontsFolder);
await downloadFiraCode(); try {
await downloadIosevka(); await downloadFiraCode();
await downloadIosevka();
console.log('Loaded all fonts for testing') console.log('Loaded all fonts for testing')
} catch (e) {
console.warn('Fonts failed to download, ligature tests will not work', e);
}
} }
async function downloadFiraCode() { async function downloadFiraCode() {
+19 -1
View File
@@ -65,6 +65,22 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
console.error(err.name, err.message); console.error(err.name, err.message);
} }
} }
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
else if (typeof process !== 'object' && 'queryLocalFonts' in window) {
const fonts: Record<string, IFontMetadata[]> = {};
try {
const fontsIterator = await (window as any).queryLocalFonts();
for (const metadata of fontsIterator) {
if (!fonts.hasOwnProperty(metadata.family)) {
fonts[metadata.family] = [];
}
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: any) {
console.error(err.name, err.message);
}
}
// Node environment or no font access API // Node environment or no font access API
else { else {
try { try {
@@ -90,7 +106,9 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) { if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
const font = fonts[family][0]; const font = fonts[family][0];
if ('blob' in font) { if ('blob' in font) {
return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize }); const bytes = await font.blob();
const buffer = await bytes.arrayBuffer();
return loadBuffer(buffer, { cacheSize });
} }
return await loadFile(font.path, { cacheSize }); return await loadFile(font.path, { cacheSize });
} }
+1 -1
View File
@@ -54,7 +54,7 @@ export function enableLigatures(term: Terminal): void {
// Only refresh things if we actually found a font // Only refresh things if we actually found a font
if (f) { if (f) {
term.refresh(0, term.options.rows! - 1); term.refresh(0, term.rows - 1);
} }
} }
}) })
+37 -39
View File
@@ -11,11 +11,12 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer'; import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types'; import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle'; import { Disposable, toDisposable } from 'common/Lifecycle';
import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm'; import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types'; import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
import { ITerminal, IColorSet } from 'browser/Types'; import { ITerminal, IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter'; import { EventEmitter } from 'common/EventEmitter';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
@@ -96,6 +97,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
} }
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this._core.screenElement!.appendChild(this._canvas); this._core.screenElement!.appendChild(this._canvas);
@@ -518,67 +520,63 @@ export class WebglRenderer extends Disposable implements IRenderer {
return; return;
} }
// Calculate the scaled character width. Width is floored as it must be // Calculate the scaled character width. Width is floored as it must be drawn to an integer grid
// drawn to an integer grid in order for the CharAtlas "stamps" to not be // in order for the char atlas glyphs to not be blurry.
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
// NOTE: ceil fixes sometime, floor does others :s
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio); this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case // Calculate the scaled character height. Height is ceiled in case devicePixelRatio is a
// devicePixelRatio is a floating point number in order to ensure there is // floating point number in order to ensure there is enough space to draw the character to the
// enough space to draw the character to the cell. // cell.
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio); this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value // Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be
// will be floored because since lineHeight can never be lower then 1, there // floored since lineHeight can never be lower then 1, this guarentees the scaled cell height
// is a guarentee that the scaled line height will always be larger than // will always be larger than scaled char height.
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
// Calculate the y coordinate within a cell that text should draw from in // Calculate the y offset within a cell that glyph should draw at in order for it to be centered
// order to draw in the center of a cell. // correctly within the cell.
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account. // Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
// Calculate the x coordinate with a cell that text should draw from in // Calculate the x offset with a cell that text should draw from in order for it to be centered
// order to draw in the center of a cell. // correctly within the cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2); this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of // Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in
// pixel in the canvas // the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this // The the size of the canvas on the page. It's important that this rounds to nearest integer
// rounds to nearest integer and not ceils as browsers often set // and not ceils as browsers often have floating point precision issues where
// window.devicePixelRatio as something like 1.100000023841858, when it's // `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 // it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
// pixel too large for the canvas element size. // large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio); this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio; // Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio; // device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
// Get the _actual_ dimensions of an individual cell. This needs to be // size on the canvas can differ.
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
// This fixes 110% and 125%, not 150% or 175% though
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
} }
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
if (this.dimensions.scaledCanvasWidth === width && this.dimensions.scaledCanvasHeight === height) {
return;
}
this.dimensions.scaledCanvasWidth = width;
this.dimensions.scaledCanvasHeight = height;
this._canvas.width = width;
this._canvas.height = height;
this._requestRedrawViewport();
}
private _requestRedrawViewport(): void { private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
} }
@@ -416,15 +416,15 @@ export class WebglCharAtlas implements IDisposable {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); 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, // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
// try for a maximum of 5 pixels. // even on the bottom row, try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) { if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor);
if (isBeyondCellBounds) { if (isBeyondCellBounds) {
for (let offset = 1; offset <= 5; offset++) { for (let offset = 1; offset <= 5; offset++) {
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); 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, this._config.allowTransparency); isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor);
if (!isBeyondCellBounds) { if (!isBeyondCellBounds) {
break; break;
} }
@@ -460,7 +460,12 @@ export class WebglCharAtlas implements IDisposable {
); );
// Clear out the background color and determine if the glyph is empty. // Clear out the background color and determine if the glyph is empty.
const isEmpty = clearColor(imageData, backgroundColor, foregroundColor, this._config.allowTransparency); let isEmpty: boolean;
if (!this._config.allowTransparency) {
isEmpty = clearColor(imageData, backgroundColor, foregroundColor);
} else {
isEmpty = checkCompletelyTransparent(imageData);
}
// Handle empty glyphs // Handle empty glyphs
if (isEmpty) { if (isEmpty) {
@@ -604,7 +609,7 @@ export class WebglCharAtlas implements IDisposable {
* transparent. * transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent. * @returns True if the result is "empty", meaning all pixels are fully transparent.
*/ */
function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTransparency: boolean): boolean { function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean {
// Get color channels // Get color channels
const r = bg.rgba >>> 24; const r = bg.rgba >>> 24;
const g = bg.rgba >>> 16 & 0xFF; const g = bg.rgba >>> 16 & 0xFF;
@@ -624,15 +629,14 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTranspare
// Set alpha channel of relevent pixels to 0 // Set alpha channel of relevent pixels to 0
let isEmpty = true; let isEmpty = true;
for (let offset = 0; offset < imageData.data.length; offset += 4) { for (let offset = 0; offset < imageData.data.length; offset += 4) {
// Check exact match
if (imageData.data[offset] === r && if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g && imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) { imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0; imageData.data[offset + 3] = 0;
} else { } else {
// Check the threshold only when transparency is not allowed only as overlapping isn't an // Check the threshold based difference
// issue for transparency glyphs. if ((Math.abs(imageData.data[offset] - r) +
if (!allowTransparency &&
(Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) + Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b)) < threshold) { Math.abs(imageData.data[offset + 2] - b)) < threshold) {
imageData.data[offset + 3] = 0; imageData.data[offset + 3] = 0;
@@ -645,6 +649,15 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTranspare
return isEmpty; 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;
}
function toPaddedHex(c: number): string { function toPaddedHex(c: number): string {
const s = c.toString(16); const s = c.toString(16);
return s.length < 2 ? '0' + s : s; return s.length < 2 ? '0' + s : s;
@@ -54,6 +54,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
this.onOptionsChanged(terminal); this.onOptionsChanged(terminal);
} }
public override dispose(): void {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
super.dispose();
}
public resize(terminal: Terminal, dim: IRenderDimensions): void { public resize(terminal: Terminal, dim: IRenderDimensions): void {
super.resize(terminal, dim); super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state // Resizing the canvas discards the contents of the canvas so clear state
+7 -3
View File
@@ -98,9 +98,13 @@ function getNextBetaVersion(packageJson) {
process.exit(1); process.exit(1);
} }
const tag = 'beta'; const tag = 'beta';
// const stableVersion = packageJson.version.split('.'); let nextStableVersion;
// const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; if (packageJson.name === 'xterm') {
const nextStableVersion = `5.0.0`; nextStableVersion = `5.0.0`;
} else {
const stableVersion = packageJson.version.split('.');
nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
}
const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag);
if (publishedVersions.length === 0) { if (publishedVersions.length === 0) {
return `${nextStableVersion}-${tag}.1`; return `${nextStableVersion}-${tag}.1`;
+2 -2
View File
@@ -79,8 +79,8 @@
"playwright": "^1.22.1", "playwright": "^1.22.1",
"source-map-loader": "^3.0.0", "source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"ts-loader": "^9.1.2", "ts-loader": "^9.3.1",
"typescript": "^4.4.4", "typescript": "4.7",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"webpack": "^5.61.0", "webpack": "^5.61.0",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.1",
+5
View File
@@ -38,6 +38,11 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
} }
public dispose(): void {
super.dispose();
this._lastMouseEvent = undefined;
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._linkProviders.push(linkProvider); this._linkProviders.push(linkProvider);
return { return {
+5 -4
View File
@@ -81,6 +81,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// browser services // browser services
private _decorationService: DecorationService; private _decorationService: DecorationService;
private _charSizeService: ICharSizeService | undefined; private _charSizeService: ICharSizeService | undefined;
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined; private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined; private _renderService: IRenderService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined; private _characterJoinerService: ICharacterJoinerService | undefined;
@@ -494,8 +495,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea); this._helperContainer.appendChild(this.textarea);
const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea); this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea);
this._instantiationService.setService(ICoreBrowserService, coreBrowserService); this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService); this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -586,11 +587,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
} }
if (this.options.overviewRulerWidth) { if (this.options.overviewRulerWidth) {
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
} }
this.optionsService.onOptionChange(() => { this.optionsService.onOptionChange(() => {
if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) { if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
} }
}); });
// Measure the character size // Measure the character size
+1 -1
View File
@@ -168,7 +168,7 @@ export interface ILinkWithState {
state?: ILinkState; state?: ILinkState;
} }
export interface ILinkifier2 { export interface ILinkifier2 extends IDisposable {
onShowLinkUnderline: IEvent<ILinkifierEvent>; onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>; onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined; readonly currentLink: ILinkWithState | undefined;
+59 -1
View File
@@ -13,6 +13,12 @@ import { IRenderDimensions } from 'browser/renderer/Types';
const FALLBACK_SCROLL_BAR_WIDTH = 15; const FALLBACK_SCROLL_BAR_WIDTH = 15;
interface ISmoothScrollState {
startTime: number;
origin: number;
target: number;
}
/** /**
* Represents the viewport of a terminal, the visible area within the larger buffer of output. * Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object. * Logic for the virtual scroll bar is included in this object.
@@ -36,6 +42,11 @@ export class Viewport extends Disposable implements IViewport {
private _refreshAnimationFrame: number | null = null; private _refreshAnimationFrame: number | null = null;
private _ignoreNextScrollEvent: boolean = false; private _ignoreNextScrollEvent: boolean = false;
private _smoothScrollState: ISmoothScrollState = {
startTime: 0,
origin: -1,
target: -1
};
constructor( constructor(
private readonly _scrollLines: (amount: number) => void, private readonly _scrollLines: (amount: number) => void,
@@ -168,6 +179,37 @@ export class Viewport extends Disposable implements IViewport {
this._scrollLines(diff); this._scrollLines(diff);
} }
private _smoothScroll(): void {
// Check valid state
if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) {
return;
}
// Calculate position complete
const percent = this._smoothScrollPercent();
this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin));
// Continue or finish smooth scroll
if (percent < 1) {
window.requestAnimationFrame(() => this._smoothScroll());
} else {
this._clearSmoothScrollState();
}
}
private _smoothScrollPercent(): number {
if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) {
return 1;
}
return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollDuration, 1), 0);
}
private _clearSmoothScrollState(): void {
this._smoothScrollState.startTime = 0;
this._smoothScrollState.origin = -1;
this._smoothScrollState.target = -1;
}
/** /**
* Handles bubbling of scroll event in case the viewport has reached top or bottom * Handles bubbling of scroll event in case the viewport has reached top or bottom
* @param ev The scroll event. * @param ev The scroll event.
@@ -196,7 +238,23 @@ export class Viewport extends Disposable implements IViewport {
if (amount === 0) { if (amount === 0) {
return false; return false;
} }
this._viewportElement.scrollTop += amount; if (!this._optionsService.rawOptions.smoothScrollDuration) {
this._viewportElement.scrollTop += amount;
} else {
this._smoothScrollState.startTime = Date.now();
if (this._smoothScrollPercent() < 1) {
this._smoothScrollState.origin = this._viewportElement.scrollTop;
if (this._smoothScrollState.target === -1) {
this._smoothScrollState.target = this._viewportElement.scrollTop + amount;
} else {
this._smoothScrollState.target += amount;
}
this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0);
this._smoothScroll();
} else {
this._clearSmoothScrollState();
}
}
return this._bubbleScroll(ev, amount); return this._bubbleScroll(ev, amount);
} }
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
export function observeDevicePixelDimensions(element: HTMLElement, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
// Observe any resizes to the element and extract the actual pixel size of the element if the
// devicePixelContentBoxSize API is supported. This allows correcting rounding errors when
// converting between CSS pixels and device pixels which causes blurry rendering when device
// pixel ratio is not a round number.
let observer: ResizeObserver | undefined = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === element);
if (!entry) {
return;
}
// Disconnect if devicePixelContentBoxSize isn't supported by the browser
if (!('devicePixelContentBoxSize' in entry)) {
observer?.disconnect();
observer = undefined;
return;
}
callback(
entry.devicePixelContentBoxSize[0].inlineSize,
entry.devicePixelContentBoxSize[0].blockSize
);
});
observer.observe(element, { box: ['device-pixel-content-box'] } as any);
return toDisposable(() => observer?.disconnect());
}
+2 -2
View File
@@ -200,7 +200,7 @@ export namespace rgba {
const resultA = reduceLuminance(bgRgba, fgRgba, ratio); const resultA = reduceLuminance(bgRgba, fgRgba, ratio);
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
if (resultARatio < ratio) { if (resultARatio < ratio) {
const resultB = increaseLuminance(bgRgba, bgRgba, ratio); const resultB = increaseLuminance(bgRgba, fgRgba, ratio);
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
return resultARatio > resultBRatio ? resultA : resultB; return resultARatio > resultBRatio ? resultA : resultB;
} }
@@ -209,7 +209,7 @@ export namespace rgba {
const resultA = increaseLuminance(bgRgba, fgRgba, ratio); const resultA = increaseLuminance(bgRgba, fgRgba, ratio);
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
if (resultARatio < ratio) { if (resultARatio < ratio) {
const resultB = reduceLuminance(bgRgba, bgRgba, ratio); const resultB = reduceLuminance(bgRgba, fgRgba, ratio);
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
return resultARatio > resultBRatio ? resultA : resultB; return resultARatio > resultBRatio ? resultA : resultB;
} }
+1 -4
View File
@@ -83,10 +83,7 @@ export function evaluateKeyboardEvent(
break; break;
case 8: case 8:
// backspace // backspace
if (ev.shiftKey) { if (ev.altKey) {
result.key = C0.BS; // ^H
break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^? result.key = C0.ESC + C0.DEL; // \e ^?
break; break;
} }
+1
View File
@@ -28,6 +28,7 @@ export const DEFAULT_OPTIONS: Readonly<ITerminalOptions> = {
scrollback: 1000, scrollback: 1000,
scrollSensitivity: 1, scrollSensitivity: 1,
screenReaderMode: false, screenReaderMode: false,
smoothScrollDuration: 0,
macOptionIsMeta: false, macOptionIsMeta: false,
macOptionClickForcesSelection: false, macOptionClickForcesSelection: false,
minimumContrastRatio: 1, minimumContrastRatio: 1,
+1
View File
@@ -232,6 +232,7 @@ export interface ITerminalOptions {
screenReaderMode: boolean; screenReaderMode: boolean;
scrollback: number; scrollback: number;
scrollSensitivity: number; scrollSensitivity: number;
smoothScrollDuration: number;
tabStopWidth: number; tabStopWidth: number;
theme: ITheme; theme: ITheme;
windowsMode: boolean; windowsMode: boolean;
+2 -2
View File
@@ -5,7 +5,7 @@
import * as playwright from 'playwright'; import * as playwright from 'playwright';
import deepEqual = require('deep-equal'); import deepEqual = require('deep-equal');
import { ITerminalOptions } from 'xterm'; import { ITerminalInitOnlyOptions, ITerminalOptions } from 'xterm';
import { deepStrictEqual, fail } from 'assert'; import { deepStrictEqual, fail } from 'assert';
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> { export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> {
@@ -43,7 +43,7 @@ export async function timeout(ms: number): Promise<void> {
return new Promise<void>(r => setTimeout(r, ms)); return new Promise<void>(r => setTimeout(r, ms));
} }
export async function openTerminal(page: playwright.Page, options: ITerminalOptions = {}): Promise<void> { export async function openTerminal(page: playwright.Page, options: ITerminalOptions & ITerminalInitOnlyOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })})`); await page.evaluate(`window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
await page.waitForSelector('.xterm-rows'); await page.waitForSelector('.xterm-rows');
+10 -3
View File
@@ -169,6 +169,12 @@ declare module 'xterm-headless' {
*/ */
scrollSensitivity?: number; scrollSensitivity?: number;
/**
* The duration to smoothly scroll between the origin and the target in
* milliseconds. Set to 0 to disable smooth scrolling and scroll instantly.
*/
smoothScrollDuration?: number;
/** /**
* The size of tab stops in the terminal. * The size of tab stops in the terminal.
*/ */
@@ -631,9 +637,10 @@ declare module 'xterm-headless' {
registerMarker(cursorYOffset?: number): IMarker | undefined; registerMarker(cursorYOffset?: number): IMarker | undefined;
/* /*
* Disposes of the terminal, detaching it from the DOM and removing any * Disposes of the terminal, detaching it from the DOM and removing any
* active listeners. * active listeners. Once the terminal is disposed it should not be used
*/ * again.
*/
dispose(): void; dispose(): void;
/** /**
+26 -13
View File
@@ -21,7 +21,7 @@ declare module 'xterm' {
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
/** /**
* An object containing start up options for the terminal. * An object containing options for the terminal.
*/ */
export interface ITerminalOptions { export interface ITerminalOptions {
/** /**
@@ -55,11 +55,6 @@ declare module 'xterm' {
*/ */
convertEol?: boolean; convertEol?: boolean;
/**
* The number of columns in the terminal.
*/
cols?: number;
/** /**
* Whether the cursor blinks. * Whether the cursor blinks.
*/ */
@@ -177,11 +172,6 @@ declare module 'xterm' {
*/ */
rightClickSelectsWord?: boolean; rightClickSelectsWord?: boolean;
/**
* The number of rows in the terminal.
*/
rows?: number;
/** /**
* Whether screen reader support is enabled. When on this will expose * Whether screen reader support is enabled. When on this will expose
* supporting elements in the DOM to support NVDA on Windows and VoiceOver * supporting elements in the DOM to support NVDA on Windows and VoiceOver
@@ -201,6 +191,12 @@ declare module 'xterm' {
*/ */
scrollSensitivity?: number; scrollSensitivity?: number;
/**
* The duration to smoothly scroll between the origin and the target in
* milliseconds. Set to 0 to disable smooth scrolling and scroll instantly.
*/
smoothScrollDuration?: number;
/** /**
* The size of tab stops in the terminal. * The size of tab stops in the terminal.
*/ */
@@ -242,6 +238,22 @@ declare module 'xterm' {
overviewRulerWidth?: number; overviewRulerWidth?: number;
} }
/**
* An object containing additional options for the terminal that can only be
* set on start up.
*/
export interface ITerminalInitOnlyOptions {
/**
* The number of columns in the terminal.
*/
cols?: number;
/**
* The number of rows in the terminal.
*/
rows?: number;
}
/** /**
* Contains colors to theme the terminal with. * Contains colors to theme the terminal with.
*/ */
@@ -708,7 +720,7 @@ declare module 'xterm' {
* *
* @param options An object containing a set of options. * @param options An object containing a set of options.
*/ */
constructor(options?: ITerminalOptions); constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions);
/** /**
* Adds an event listener for when the bell is triggered. * Adds an event listener for when the bell is triggered.
@@ -946,7 +958,8 @@ declare module 'xterm' {
/* /*
* Disposes of the terminal, detaching it from the DOM and removing any * Disposes of the terminal, detaching it from the DOM and removing any
* active listeners. * active listeners. Once the terminal is disposed it should not be used
* again.
*/ */
dispose(): void; dispose(): void;
+9 -9
View File
@@ -3760,10 +3760,10 @@ tr46@^3.0.0:
dependencies: dependencies:
punycode "^2.1.1" punycode "^2.1.1"
ts-loader@^9.1.2: ts-loader@^9.3.1:
version "9.2.6" version "9.3.1"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4"
integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw== integrity sha512-OkyShkcZTsTwyS3Kt7a4rsT/t2qvEVQuKCTg4LJmpj9fhFR7ukGdZwV6Qq3tRUkqcXtfGpPR7+hFKHCG/0d3Lw==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
enhanced-resolve "^5.0.0" enhanced-resolve "^5.0.0"
@@ -3831,16 +3831,16 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript@4.7:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@^4.2.3: typescript@^4.2.3:
version "4.6.4" version "4.6.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
typescript@^4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
unbox-primitive@^1.0.0: unbox-primitive@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"