Merge remote-tracking branch 'upstream/master' into pr/silamon/4004

This commit is contained in:
Daniel Imms
2022-09-02 13:05:52 -07:00
68 changed files with 1620 additions and 767 deletions
+72
View File
@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 17 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+1 -1
View File
@@ -140,7 +140,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**GoTTY**](https://github.com/sorenisanerd/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
@@ -344,7 +344,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize);
}
// Draw the character
@@ -404,12 +404,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Don't try cache the glyph if it uses any decoration foreground/background override.
let hasOverrides = false;
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.backgroundColorRGB || d.foregroundColorRGB) {
hasOverrides = true;
break;
}
}
});
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
@@ -473,7 +472,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize);
}
// Draw the character
@@ -519,9 +518,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
let bgOverride: number | undefined;
let fgOverride: number | undefined;
let isTop = false;
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
continue;
return;
}
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba;
@@ -530,7 +529,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
fgOverride = d.foregroundColorRGB.rgba;
}
isTop = d.options.layer === 'top';
}
});
// Apply selection foreground if applicable
if (!isTop) {
@@ -3,12 +3,11 @@
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService } from 'browser/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
interface ISelectionState {
start?: [number, number];
@@ -187,15 +187,15 @@ export class TextRenderLayer extends BaseRenderLayer {
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let isTop = false;
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
this._decorationService.forEachDecorationAtCell(x, this._bufferService.buffer.ydisp + y, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
continue;
return;
}
if (d.backgroundColorRGB) {
nextFillStyle = d.backgroundColorRGB.css;
}
isTop = d.options.layer === 'top';
}
});
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es6"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es6",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es6",
],
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
-1
View File
@@ -2,7 +2,6 @@
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.
### Install
```bash
+37 -34
View File
@@ -8,7 +8,7 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
@@ -70,6 +70,14 @@ const INDICES_PER_CELL = 10;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
/** Work variables to avoid garbage collection. */
const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = {
i: 0,
glyph: undefined,
leftCellPadding: 0,
clippedPixels: 0
};
export class GlyphRenderer extends Disposable {
private _atlas: WebglCharAtlas | undefined;
@@ -122,7 +130,7 @@ export class GlyphRenderer extends Disposable {
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
// unitQuadVertices to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
@@ -170,18 +178,20 @@ export class GlyphRenderer extends Disposable {
}
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
const terminal = this._terminal;
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
@@ -190,47 +200,40 @@ export class GlyphRenderer extends Disposable {
}
// Get the glyph
let rasterizedGlyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
} else {
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}
// Fill empty if no glyph was found
if (!rasterizedGlyph) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
const leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
if (bg !== lastBg && rasterizedGlyph.offset.x > leftCellPadding) {
const clippedPixels = rasterizedGlyph.offset.x - leftCellPadding;
w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) {
w.clippedPixels = w.glyph.offset.x - w.leftCellPadding;
// a_origin
array[i ] = -(rasterizedGlyph.offset.x - clippedPixels) + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft;
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = (rasterizedGlyph.size.x - clippedPixels) / this._dimensions.scaledCanvasWidth;
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth;
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x + clippedPixels / this._atlas.cacheCanvas.width;
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width;
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
// a_texsize
array[i + 6] = rasterizedGlyph.sizeClipSpace.x - clippedPixels / this._atlas.cacheCanvas.width;
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width;
array[w.i + 7] = w.glyph.sizeClipSpace.y;
} else {
// a_origin
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft;
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth;
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
array[w.i + 4] = w.glyph.texturePositionClipSpace.x;
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
// a_texsize
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
array[w.i + 6] = w.glyph.sizeClipSpace.x;
array[w.i + 7] = w.glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
@@ -28,12 +28,11 @@ layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
uniform mat4 u_projection;
uniform vec2 u_resolution;
out vec4 v_color;
void main() {
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
vec2 zeroToOne = a_position + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_color = a_color;
}`;
@@ -59,11 +58,22 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
/** Work variables to avoid garbage collection. */
const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = {
rgba: 0,
isDefault: false,
x1: 0,
y1: 0,
r: 0,
g: 0,
b: 0,
a: 0
};
export class RectangleRenderer extends Disposable {
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
private _resolutionLocation: WebGLUniformLocation;
private _attributesBuffer: WebGLBuffer;
private _projectionLocation: WebGLUniformLocation;
private _bgFloat!: Float32Array;
@@ -87,7 +97,6 @@ export class RectangleRenderer extends Disposable {
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
// Create and set the vertex array object
@@ -104,7 +113,7 @@ export class RectangleRenderer extends Disposable {
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
// unitQuadVertices to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
@@ -136,7 +145,6 @@ export class RectangleRenderer extends Disposable {
gl.bindVertexArray(this._vertexArrayObject);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
// Bind attributes buffer and draw
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
@@ -153,6 +161,10 @@ export class RectangleRenderer extends Disposable {
this._updateViewportRectangle();
}
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background);
}
@@ -174,22 +186,34 @@ export class RectangleRenderer extends Disposable {
const terminal = this._terminal;
const vertices = this._vertices;
// Declare variable ahead of time to avoid garbage collection
let rectangleCount = 1;
let y: number;
let x: number;
let currentStartX: number;
let currentBg: number;
let currentFg: number;
let currentInverse: boolean;
let modelIndex: number;
let bg: number;
let fg: number;
let inverse: boolean;
let offset: number;
for (let y = 0; y < terminal.rows; y++) {
let currentStartX = -1;
let currentBg = 0;
let currentFg = 0;
let currentInverse = false;
for (let x = 0; x < terminal.cols; x++) {
const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
const inverse = !!(fg & FgFlags.INVERSE);
for (y = 0; y < terminal.rows; y++) {
currentStartX = -1;
currentBg = 0;
currentFg = 0;
currentInverse = false;
for (x = 0; x < terminal.cols; x++) {
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
inverse = !!(fg & FgFlags.INVERSE);
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
// A rectangle needs to be drawn if going from non-default to another color
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
}
currentStartX = x;
@@ -200,7 +224,7 @@ export class RectangleRenderer extends Disposable {
}
// Finish rectangle if it's still going
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
}
}
@@ -208,55 +232,54 @@ export class RectangleRenderer extends Disposable {
}
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
let rgba: number | undefined;
let isDefault = false;
w.isDefault = false;
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (fg & Attributes.RGB_MASK) << 8;
w.rgba = (fg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.foreground.rgba;
w.rgba = this._colors.foreground.rgba;
}
} else {
switch (bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (bg & Attributes.RGB_MASK) << 8;
w.rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.background.rgba;
isDefault = true;
w.rgba = this._colors.background.rgba;
w.isDefault = true;
}
}
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
const x1 = startX * this._dimensions.scaledCellWidth;
const y1 = y * this._dimensions.scaledCellHeight;
const r = ((rgba >> 24) & 0xFF) / 255;
const g = ((rgba >> 16) & 0xFF) / 255;
const b = ((rgba >> 8 ) & 0xFF) / 255;
const a = (!isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
w.x1 = startX * this._dimensions.scaledCellWidth;
w.y1 = y * this._dimensions.scaledCellHeight;
w.r = ((w.rgba >> 24) & 0xFF) / 255;
w.g = ((w.rgba >> 16) & 0xFF) / 255;
w.b = ((w.rgba >> 8 ) & 0xFF) / 255;
w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, a);
this._addRectangle(vertices.attributes, offset, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a);
}
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
array[offset ] = x1;
array[offset + 1] = y1;
array[offset + 2] = width;
array[offset + 3] = height;
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
array[offset + 4] = r;
array[offset + 5] = g;
array[offset + 6] = b;
@@ -264,10 +287,10 @@ export class RectangleRenderer extends Disposable {
}
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
array[offset ] = x1;
array[offset + 1] = y1;
array[offset + 2] = width;
array[offset + 3] = height;
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
array[offset + 4] = color[0];
array[offset + 5] = color[1];
array[offset + 6] = color[2];
-4
View File
@@ -3,10 +3,6 @@
* @license MIT
*/
export interface IRasterizedGlyphSet {
[bg: number]: { [fg: number]: { [ext: number]: IRasterizedGlyph } } | undefined;
}
/**
* Represents a rasterized glyph within a texture atlas. Some numbers are
* tracked in CSS pixels as well in order to reduce calculations during the
+126 -56
View File
@@ -11,7 +11,7 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Disposable } from 'common/Lifecycle';
import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
@@ -22,10 +22,19 @@ import { EventEmitter } from 'common/EventEmitter';
import { CellData } from 'common/buffer/CellData';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { CharData, ICellData } from 'common/Types';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IDecorationService } from 'common/services/Services';
/** Work variables to avoid garbage collection. */
const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = {
fg: 0,
bg: 0,
hasFg: false,
hasBg: false,
isSelected: false
};
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _charAtlas: WebglCharAtlas | undefined;
@@ -37,13 +46,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: RectangleRenderer;
private _glyphRenderer: GlyphRenderer;
private _rectangleRenderer!: RectangleRenderer;
private _glyphRenderer!: GlyphRenderer;
public dimensions: IRenderDimensions;
private _core: ITerminal;
private _isAttached: boolean;
private _contextRestorationTimeout: number | undefined;
private _onChangeTextureAtlas = new EventEmitter<HTMLCanvasElement>();
public get onChangeTextureAtlas(): IEvent<HTMLCanvasElement> { return this._onChangeTextureAtlas.event; }
@@ -99,16 +109,34 @@ export class WebglRenderer extends Disposable implements IRenderer {
throw new Error('WebGL2 not supported ' + this._gl);
}
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
e.preventDefault();
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
// If not, dispatch the onContextLoss notification to observers.
this._contextRestorationTimeout = setTimeout(() => {
this._contextRestorationTimeout = undefined;
console.warn('webgl context not restored; firing onContextLoss');
this._onContextLoss.fire(e);
}, 3000 /* ms */);
}));
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
console.warn('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
// The texture atlas and glyph renderer must be fully reinitialized
// because their contents have been lost.
removeTerminalFromCache(this._terminal);
this._initializeWebGLState();
this._requestRedrawViewport();
}));
this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this._core.screenElement!.appendChild(this._canvas);
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions));
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions));
// Update dimensions and acquire char atlas
this.onCharSizeChanged();
this._initializeWebGLState();
this._isAttached = document.body.contains(this._core.screenElement!);
}
@@ -173,6 +201,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
this._rectangleRenderer.setDimensions(this.dimensions);
this._rectangleRenderer.onResize();
this._glyphRenderer.setDimensions(this.dimensions);
this._glyphRenderer.onResize();
@@ -225,6 +254,21 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._refreshCharAtlas();
}
/**
* Initializes members dependent on WebGL context state.
*/
private _initializeWebGLState(): void {
// Dispose any previous rectangle and glyph renderers before creating new ones.
this._rectangleRenderer?.dispose();
this._glyphRenderer?.dispose();
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
// Update dimensions and acquire char atlas
this.onCharSizeChanged();
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
@@ -305,14 +349,28 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _updateModel(start: number, end: number): void {
const terminal = this._core;
let cell: ICellData = this._workCell;
let lastBg: number = 0;
for (let y = start; y <= end; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row)!;
// Declare variable ahead of time to avoid garbage collection
let lastBg: number;
let y: number;
let row: number;
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let lastCharX: number;
let range: [number, number];
let chars: string;
let code: number;
let i: number;
let x: number;
let j: number;
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < terminal.cols; x++) {
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._workColors.bg;
line.loadCell(x, cell);
@@ -321,15 +379,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
isJoined = false;
lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
@@ -343,9 +401,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
lastCharX = range[1] - 1;
}
const chars = cell.getChars();
let code = cell.getCode();
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
chars = cell.getChars();
code = cell.getCode();
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Load colors/resolve overrides into work colors
this._loadColorsForCell(x, row);
@@ -381,7 +439,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
@@ -404,79 +462,91 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0;
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
let bgOverride: number | undefined;
let fgOverride: number | undefined;
let isSelected: boolean = false;
// Reset overrides work variables
w.bg = 0;
w.fg = 0;
w.hasBg = false;
w.hasFg = false;
w.isSelected = false;
// Apply decorations on the bottom layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.hasBg = true;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.hasFg = true;
}
}
});
// Apply the selection color if needed
isSelected = this._isCellSelected(x, y);
if (isSelected) {
bgOverride = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
w.isSelected = this._isCellSelected(x, y);
if (w.isSelected) {
w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
w.hasBg = true;
if (this._colors.selectionForeground) {
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
w.hasFg = true;
}
}
// Apply decorations on the top layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.hasBg = true;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
w.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 (bgOverride !== undefined) {
if (isSelected) {
if (w.hasBg) {
if (w.isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | bgOverride | Attributes.CM_RGB;
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB;
} else {
// Non-RGB attributes from model + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB;
}
}
if (fgOverride !== undefined) {
if (w.hasFg) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB;
}
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
// 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._workColors.fg & FgFlags.INVERSE) {
if (bgOverride !== undefined && fgOverride === undefined) {
if (w.hasBg && !w.hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
w.hasFg = true;
}
if (bgOverride === undefined && fgOverride !== undefined) {
if (!w.hasBg && w.hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
w.hasBg = true;
}
}
// Use the override if it exists
this._workColors.bg = bgOverride ?? this._workColors.bg;
this._workColors.fg = fgOverride ?? this._workColors.fg;
this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg;
this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg;
}
private _isCellSelected(x: number, y: number): boolean {
@@ -587,11 +657,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
if (this.dimensions.scaledCanvasWidth === width && this.dimensions.scaledCanvasHeight === height) {
if (this._canvas.width === width && this._canvas.height === height) {
return;
}
this.dimensions.scaledCanvasWidth = width;
this.dimensions.scaledCanvasHeight = height;
// While the actual canvas size has changed, keep scaledCanvasWidth/Height as the value before
// the change as it's an exact multiple of the cell sizes.
this._canvas.width = width;
this._canvas.height = height;
this._requestRedrawViewport();
@@ -7,7 +7,7 @@ import { generateConfig, configEquals } from './CharAtlasUtils';
import { WebglCharAtlas } from './WebglCharAtlas';
import { ICharAtlasConfig } from './Types';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IColorSet, ITerminal } from 'browser/Types';
interface ICharAtlasCacheEntry {
atlas: WebglCharAtlas;
@@ -64,8 +64,9 @@ export function acquireCharAtlas(
}
}
const core: ITerminal = (terminal as any)._core;
const newEntry: ICharAtlasCacheEntry = {
atlas: new WebglCharAtlas(document, newConfig),
atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
config: newConfig,
ownedBy: [terminal]
};
@@ -5,7 +5,7 @@
import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
import { IRasterizedGlyph, IBoundingBox } from '../Types';
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'common/Types';
@@ -13,7 +13,9 @@ import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { color, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
@@ -51,11 +53,16 @@ interface ICharAtlasActiveRow {
height: number;
}
/** Work variables to avoid garbage collection. */
const w: { glyph: IRasterizedGlyph | undefined } = {
glyph: undefined
};
export class WebglCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
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;
@@ -89,7 +96,8 @@ export class WebglCharAtlas implements IDisposable {
constructor(
document: Document,
private _config: ICharAtlasConfig
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
@@ -122,13 +130,7 @@ export class WebglCharAtlas implements IDisposable {
// Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
this._cacheMap[i] = {
[DEFAULT_COLOR]: {
[DEFAULT_COLOR]: {
[DEFAULT_EXT]: rasterizedGlyph
}
}
};
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
}
}
@@ -146,8 +148,8 @@ export class WebglCharAtlas implements IDisposable {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._cacheMap = {};
this._cacheMapCombined = {};
this._cacheMap.clear();
this._cacheMapCombined.clear();
this._currentRow.x = 0;
this._currentRow.y = 0;
this._currentRow.height = 0;
@@ -167,39 +169,18 @@ export class WebglCharAtlas implements IDisposable {
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
private _getFromCacheMap(
cacheMap: { [key: string | number]: IRasterizedGlyphSet },
cacheMap: FourKeyMap<string | number, number, number, number, IRasterizedGlyph>,
key: string | number,
bg: number,
fg: number,
ext: number
): IRasterizedGlyph {
let rasterizedGlyphSet = cacheMap[key];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
cacheMap[key] = rasterizedGlyphSet;
w.glyph = cacheMap.get(key, bg, fg, ext);
if (!w.glyph) {
w.glyph = this._drawToCache(key, bg, fg, ext);
cacheMap.set(key, bg, fg, ext, w.glyph);
}
let rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (!rasterizedGlyphSetBg) {
rasterizedGlyphSetBg = {};
rasterizedGlyphSet[bg] = rasterizedGlyphSetBg;
}
let rasterizedGlyph: IRasterizedGlyph | undefined;
let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg];
if (!rasterizedGlyphSetFg) {
rasterizedGlyphSetFg = {};
rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg;
} else {
rasterizedGlyph = rasterizedGlyphSetFg[ext];
}
if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(key, bg, fg, ext);
rasterizedGlyphSetFg[ext] = rasterizedGlyph;
}
return rasterizedGlyph;
return w.glyph;
}
private _getColorFromAnsiIndex(idx: number): IColor {
@@ -418,16 +399,17 @@ export class WebglCharAtlas implements IDisposable {
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 = powerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
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);
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
@@ -435,13 +417,19 @@ export class WebglCharAtlas implements IDisposable {
// 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 * window.devicePixelRatio / 15));
// When the width is odd, draw at 0.5 position. Offset by an additional 1 dpr to bring the
// underline closer to the character
const yOffset = (lineWidth % 2 === 1 ? 0.5 : 0) + window.devicePixelRatio;
// 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
@@ -462,89 +450,104 @@ export class WebglCharAtlas implements IDisposable {
// Underline style/stroke
this._tmpCtx.beginPath();
const xLeft = padding;
const xRight = padding + this._config.scaledCellWidth;
const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset;
const yMid = padding + this._config.scaledCharHeight - yOffset;
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
this._tmpCtx.moveTo(xLeft, yBot);
this._tmpCtx.lineTo(xRight, yBot);
break;
case UnderlineStyle.CURLY:
const xMid = padding + this._config.scaledCellWidth / 2;
// 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(xLeft, 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(xLeft - this._config.scaledCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xLeft - this._config.scaledCellWidth / 2, yCurlyTop,
xLeft, yCurlyTop,
xLeft, yMid
);
this._tmpCtx.bezierCurveTo(
xLeft, yCurlyBot,
xMid, yCurlyBot,
xMid, yMid
);
this._tmpCtx.bezierCurveTo(
xMid, yCurlyTop,
xRight, yCurlyTop,
xRight, yMid
);
this._tmpCtx.bezierCurveTo(
xRight, yCurlyBot,
xRight + this._config.scaledCellWidth / 2, yCurlyBot,
xRight + this._config.scaledCellWidth / 2, yMid
);
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.SINGLE:
default:
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
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([window.devicePixelRatio * 2, window.devicePixelRatio]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.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.stroke();
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
if (!customGlyph) {
// 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 !== ' ') {
// This translates to 1/2 the line width in either direction
// Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
// baseline
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 = window.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
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 = window.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.restore();
}
}
}
}
@@ -560,7 +563,10 @@ export class WebglCharAtlas implements IDisposable {
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.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
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) {
@@ -578,7 +584,7 @@ export class WebglCharAtlas implements IDisposable {
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, 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();
}
@@ -603,7 +609,7 @@ export class WebglCharAtlas implements IDisposable {
return NULL_RASTERIZED_GLYPH;
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerlineGlyph, customGlyph, padding);
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
@@ -7,7 +7,7 @@ import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet, ITerminal } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es6"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es6",
],
+22 -5
View File
@@ -4,7 +4,7 @@
*
* Script to test different mouse modes in terminal emulators.
* Tests for protocols DECSET 9, 1000, 1002, 1003 with different
* report encodings (default, UTF8, SGR, URXVT).
* report encodings (default, UTF8, SGR, URXVT, SGR-pixels).
*
* VT200 Highlight mode (DECSET 1001) is not implemented.
*
@@ -152,13 +152,27 @@ const ENC = {
],
'URXVT': [
'\x1b[?1015h',
// format: CSI <button + 32> ; Prow ; Pcol M
// format: CSI <button + 32> ; Prow ; Pcol M
report => {
// strip off introducer + M
const sReport = report.toString().slice(2, -1);
const [button, col, row] = sReport.split(';');
return {state: evalButtonCode(button - 32), row, col};
}
],
'SGR_PIXELS' : [
'\x1b[?1016h',
// format: CSI < Pbutton ; Prow ; Pcol M
report => {
// strip off introducer + M
const sReport = report.toString().slice(3, -1);
const [buttonCode, col, row] = sReport.split(';');
const state = evalButtonCode(buttonCode);
if (report[report.length - 1] === 'm'.charCodeAt(0)) {
state.action = 'release';
}
return {state, row, col};
}
]
}
@@ -203,9 +217,12 @@ function activate() {
function applyReportData(data) {
let {state, row, col} = ENC[Object.keys(ENC)[activeEnc]][1](data);
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
// apply to cursor position
process.stdout.write(`\x1b[${row};${col}H`);
if (Object.keys(ENC)[activeEnc] !== 'SGR_PIXELS') {
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
process.stdout.write(`\x1b[${row};${col}H`);
} else {
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'x:', row, 'y:', col);
}
}
printMenu();
+45 -9
View File
@@ -189,6 +189,7 @@ if (document.location.pathname === '/test') {
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
document.getElementById('underline-test').addEventListener('click', underlineTest);
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}
@@ -203,7 +204,7 @@ function createTerminal(): void {
term = new Terminal({
allowProposedApi: true,
windowsMode: isWindows,
fontFamily: 'Fira Code, courier-new, courier, monospace',
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
theme: xtermjsTheme
} as ITerminalOptions);
@@ -406,7 +407,11 @@ function initOptions(term: TerminalType): void {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'lineHeight') {
if (o === 'rows') {
term.resize(term.cols, parseInt(input.value));
} else if (o === 'cols') {
term.resize(parseInt(input.value), term.rows);
} else if (o === 'lineHeight') {
term.options.lineHeight = parseFloat(input.value);
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
@@ -601,7 +606,7 @@ function htmlSerializeButtonHandler(): void {
}
function addTextureAtlas(e: HTMLCanvasElement) {
document.querySelector('#texture-atlas').appendChild(e);
document.querySelector('#texture-atlas').replaceChildren(e);
}
function writeCustomGlyphHandler() {
@@ -766,12 +771,23 @@ function underlineTest() {
term.write('\n\n\r');
term.writeln('Underline styles:');
term.writeln('');
term.writeln(`${u(0)}4:0m - No underline`);
term.writeln(`${u(1)}4:1m - Straight`);
term.writeln(`${u(2)}4:2m - Double`);
term.writeln(`${u(3)}4:3m - Curly`);
term.writeln(`${u(4)}4:4m - Dotted`);
term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`);
function showSequence(id: number, name: string) {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
}
let numbers = '';
for (let i = 0; i < 10; i++) {
numbers += i.toString();
}
return `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`;
}
term.writeln(showSequence(0, 'No underline'));
term.writeln(showSequence(1, 'Straight'));
term.writeln(showSequence(2, 'Double'));
term.writeln(showSequence(3, 'Curly'));
term.writeln(showSequence(4, 'Dotted'));
term.writeln(showSequence(5, 'Dashed'));
term.writeln('');
term.writeln(`Underline colors (256 color mode):`);
term.writeln('');
@@ -827,6 +843,26 @@ function ansiColorsTest() {
}
}
function addAnsiHyperlink() {
term.write('\n\n\r');
term.writeln(`Regular link with no id:`);
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07');
term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell');
term.writeln(`\nAdjacent links:`);
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07\x1b[32mxterm.js\x1b[0m\x1b]8;;\x07');
term.writeln(`\nShared ID link (underline should be shared):`);
term.writeln('╔════╗');
term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║');
term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║');
term.writeln('╚════╝');
term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`);
term.writeln('╔════╗');
term.writeln('║ ║');
term.writeln('║ ║');
term.writeln('╚════╝');
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
}
function addDecoration() {
term.options['overviewRulerWidth'] = 15;
const marker = term.registerMarker(1);
+3 -2
View File
@@ -10,8 +10,8 @@
<link rel="shortcut icon" type="image/png" href="/logo.png">
<link rel="stylesheet" href="/xterm.css" />
<link rel="stylesheet" href="/style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha384-kM3+BR0fKGmv8mDasMGLSHdqcbgWHUNWV1rAL+tkqnH578xS82vhlu9gR5BHhBai" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js" integrity="sha384-bR8my2pcqqtUkMMTomrgTyDVACSJz1M9D70XdkeK2BaYbCYxG3CfngYdSAOcPkUZ" crossorigin="anonymous"></script>
</head>
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
@@ -79,6 +79,7 @@
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
<dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
+4 -4
View File
@@ -20,19 +20,19 @@ function startServer() {
logs = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/logo.png', (req, res) => {
res.sendFile(__dirname + '/logo.png');
});
app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/test', (req, res) => {
res.sendFile(__dirname + '/test.html');
});
app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/style.css', (req, res) => {
res.sendFile(__dirname + '/style.css');
});
+12 -17
View File
@@ -5,35 +5,30 @@
import { IColorContrastCache } from 'browser/Types';
import { IColor } from 'common/Types';
import { TwoKeyMap } from 'common/MultiKeyMap';
export class ColorContrastCache implements IColorContrastCache {
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
public clear(): void {
this._color = {};
this._rgba = {};
}
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
private _css: TwoKeyMap</* bg */number, /* fg */number, string | null> = new TwoKeyMap();
public setCss(bg: number, fg: number, value: string | null): void {
if (!this._rgba[bg]) {
this._rgba[bg] = {};
}
this._rgba[bg]![fg] = value;
this._css.set(bg, fg, value);
}
public getCss(bg: number, fg: number): string | null | undefined {
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
return this._css.get(bg, fg);
}
public setColor(bg: number, fg: number, value: IColor | null): void {
if (!this._color[bg]) {
this._color[bg] = {};
}
this._color[bg]![fg] = value;
this._color.set(bg, fg, value);
}
public getColor(bg: number, fg: number): IColor | null | undefined {
return this._color[bg] ? this._color[bg]![fg] : undefined;
return this._color.get(bg, fg);
}
public clear(): void {
this._color.clear();
this._css.clear();
}
}
+2
View File
@@ -3,6 +3,8 @@
* @license MIT
*/
// This file contains strings that get exported in the API so they can be localized
// eslint-disable-next-line prefer-const
export let promptLabel = 'Terminal input';
+111
View File
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
export class OscLinkProvider implements ILinkProvider {
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IOscLinkService private readonly _oscLinkService: IOscLinkService
) {
}
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
const line = this._bufferService.buffer.lines.get(y - 1);
if (!line) {
callback(undefined);
return;
}
const result: ILink[] = [];
const linkHandler = this._optionsService.rawOptions.linkHandler;
const cell = new CellData();
const lineLength = line.getTrimmedLength();
let currentLinkId = -1;
let currentStart = -1;
let finishLink = false;
for (let x = 0; x < lineLength; x++) {
// Minor optimization, only check for content if there isn't a link in case the link ends with
// a null cell
if (currentStart === -1 && !line.hasContent(x)) {
continue;
}
line.loadCell(x, cell);
if (cell.hasExtendedAttrs() && cell.extended.urlId) {
if (currentStart === -1) {
currentStart = x;
currentLinkId = cell.extended.urlId;
continue;
} else {
finishLink = cell.extended.urlId !== currentLinkId;
}
} else {
if (currentStart !== -1) {
finishLink = true;
}
}
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
if (text) {
// These ranges are 1-based
const range: IBufferRange = {
start: {
x: currentStart + 1,
y
},
end: {
// Offset end x if it's a link that ends on the last cell in the line
x: x + (!finishLink && x === lineLength - 1 ? 1 : 0),
y
}
};
// OSC links always use underline and pointer decorations
result.push({
text,
range,
activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)),
hover: (e, text) => linkHandler?.hover?.(e, text, range),
leave: (e, text) => linkHandler?.leave?.(e, text, range)
});
}
finishLink = false;
// Clear link or start a new link if one starts immediately
if (cell.hasExtendedAttrs() && cell.extended.urlId) {
currentStart = x;
currentLinkId = cell.extended.urlId;
} else {
currentStart = -1;
currentLinkId = -1;
}
}
}
// TODO: Handle fetching and returning other link ranges to underline other links with the same id
callback(result);
}
}
function defaultActivate(e: MouseEvent, uri: string): void {
const answer = confirm(`Do you want to navigate to ${uri}?`);
if (answer) {
const newWindow = window.open();
if (newWindow) {
try {
newWindow.opener = null;
} catch {
// no-op, Electron can throw
}
newWindow.location.href = uri;
} else {
console.warn('Opening link blocked as opener could not be cleared');
}
}
}
+25 -23
View File
@@ -55,6 +55,7 @@ import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRe
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
import { OscLinkProvider } from 'browser/OscLinkProvider';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -163,6 +164,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._setup();
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
this._decorationService = this._instantiationService.createInstance(DecorationService);
this._instantiationService.setService(IDecorationService, this._decorationService);
@@ -211,22 +213,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
acc = 'ansi';
ident = '4;' + req.index;
}
if (acc) {
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
? this._colorManager.colors.ansi[req.index]
: this._colorManager.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${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);
break;
case ColorRequestType.RESTORE:
this._colorManager.restoreColor(req.index);
break;
}
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
? this._colorManager.colors.ansi[req.index]
: this._colorManager.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${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);
break;
case ColorRequestType.RESTORE:
this._colorManager.restoreColor(req.index);
break;
}
}
this._renderService?.setColors(this._colorManager.colors);
@@ -645,7 +645,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// send event to CoreMouseService
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
// get mouse coordinates
const pos = self._mouseService!.getRawByteCoords(ev, self.screenElement!, self.cols, self.rows);
const pos = self._mouseService!.getMouseReportCoords(ev, self.screenElement!);
if (!pos) {
return false;
}
@@ -699,8 +699,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
return self.coreMouseService.triggerMouseEvent({
col: pos.x - 33, // FIXME: why -33 here?
row: pos.y - 33,
col: pos.col,
row: pos.row,
x: pos.x,
y: pos.y,
button: but,
action,
ctrl: ev.ctrlKey,
@@ -1320,13 +1322,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
switch (type) {
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0);
const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0);
const canvasWidth = this._renderService.dimensions.canvasWidth.toFixed(0);
const canvasHeight = this._renderService.dimensions.canvasHeight.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
break;
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0);
const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0);
const cellWidth = this._renderService.dimensions.actualCellWidth.toFixed(0);
const cellHeight = this._renderService.dimensions.actualCellHeight.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
break;
}
+2 -2
View File
@@ -7,7 +7,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration }
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, IRenderDebouncer, IBufferRange } from 'browser/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
@@ -359,7 +359,7 @@ export class MockMouseService implements IMouseService {
throw new Error('Not implemented');
}
public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined {
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
throw new Error('Not implemented');
}
}
@@ -7,7 +7,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IRenderService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
// static values for each render and can be accessed using the decoration position as the key.
-14
View File
@@ -48,17 +48,3 @@ export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clie
return coords;
}
/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
*/
export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined {
if (!coords) {
return undefined;
}
// xterm sends raw bytes and starts at 32 (SP) for each.
return { x: coords[0] + 32, y: coords[1] + 32 };
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm';
import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, IModes, IDecorationOptions, IDecoration } from 'xterm';
import { IBufferRange, ITerminal } from 'browser/Types';
import { Terminal as TerminalCore } from 'browser/Terminal';
import * as Strings from 'browser/LocalizableStrings';
+32 -17
View File
@@ -349,25 +349,27 @@ const enum VectorType {
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
* issues can occur around AA/SPAA.
*
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not visible.
*
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
// Right triangle solid
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
// Right triangle line
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
'\u{E0B1}': { d: 'M-1,-.5 L1,.5 L-1,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
// Left triangle solid
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL, leftPadding: 2 },
// Left triangle line
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
'\u{E0B3}': { d: 'M2,-.5 L0,.5 L2,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
// Right semi-circle solid,
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL },
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 },
// Right semi-circle line,
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: window.devicePixelRatio / 2 },
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: 1 },
// Left semi-circle solid,
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL },
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 },
// Left semi-circle line,
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2 }
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 }
};
/**
@@ -380,7 +382,8 @@ export function tryDrawCustomChar(
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number
scaledCellHeight: number,
fontSize: number
): boolean {
const blockElementDefinition = blockElementDefinitions[c];
if (blockElementDefinition) {
@@ -402,7 +405,7 @@ export function tryDrawCustomChar(
const powerlineDefinition = powerlineDefinitions[c];
if (powerlineDefinition) {
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize);
return true;
}
@@ -562,7 +565,7 @@ function drawBoxDrawingChar(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset));
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true));
}
ctx.stroke();
ctx.closePath();
@@ -575,10 +578,13 @@ function drawPowerlineChar(
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number
scaledCellHeight: number,
fontSize: number
): void {
ctx.beginPath();
ctx.lineWidth = window.devicePixelRatio;
// Scale the stroke with DPR and font size
const cssLineWidth = fontSize / 12;
ctx.lineWidth = window.devicePixelRatio * cssLineWidth;
for (const instruction of charDefinition.d.split(' ')) {
const type = instruction[0];
const f = svgToCanvasInstructionMap[type];
@@ -590,7 +596,16 @@ function drawPowerlineChar(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.leftPadding, charDefinition.rightPadding));
f(ctx, translateArgs(
args,
scaledCellWidth,
scaledCellHeight,
xOffset,
yOffset,
false,
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
));
}
if (charDefinition.type === VectorType.STROKE) {
ctx.strokeStyle = ctx.fillStyle;
@@ -611,7 +626,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
};
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, leftPadding: number = 0, rightPadding: number = 0): number[] {
const result = args.map(e => parseFloat(e) || parseInt(e));
if (result.length < 2) {
@@ -623,7 +638,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
result[x] *= cellWidth - (leftPadding * window.devicePixelRatio) - (rightPadding * window.devicePixelRatio);
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
if (result[x] !== 0) {
if (doClamp && result[x] !== 0) {
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
}
// Apply the cell's offset (ie. x*cellWidth)
@@ -635,7 +650,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
result[y] *= cellHeight;
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
if (result[y] !== 0) {
if (doClamp && result[y] !== 0) {
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
}
// Apply the cell's offset (ie. x*cellHeight)
+6 -4
View File
@@ -24,10 +24,12 @@ export function observeDevicePixelDimensions(element: HTMLElement, callback: (de
return;
}
callback(
entry.devicePixelContentBoxSize[0].inlineSize,
entry.devicePixelContentBoxSize[0].blockSize
);
// Fire the callback, ignore events where the dimensions are 0x0 as the canvas is likely hidden
const width = entry.devicePixelContentBoxSize[0].inlineSize;
const height = entry.devicePixelContentBoxSize[0].blockSize;
if (width > 0 && height > 0) {
callback(width, height);
}
});
observer.observe(element, { box: ['device-pixel-content-box'] } as any);
return toDisposable(() => observer?.disconnect());
+6 -2
View File
@@ -14,11 +14,15 @@ export function isPowerlineGlyph(codepoint: number): boolean {
// Only return true for Powerline symbols which require
// different padding and should be excluded from minimum contrast
// ratio standards
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
}
export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
}
function isBoxOrBlockGlyph(codepoint: number): boolean {
return (0x2500 <= codepoint && codepoint <= 0x259F);
return 0x2500 <= codepoint && codepoint <= 0x259F;
}
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
+1 -1
View File
@@ -9,7 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'common/Color';
import { removeElementFromParent } from 'browser/Dom';
@@ -204,9 +204,9 @@ export class DomRendererRowFactory {
let bgOverride: IColor | undefined;
let fgOverride: IColor | undefined;
let isTop = false;
for (const d of this._decorationService.getDecorationsAtCell(x, row)) {
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
continue;
return;
}
if (d.backgroundColorRGB) {
bgColorMode = Attributes.CM_RGB;
@@ -219,7 +219,7 @@ export class DomRendererRowFactory {
fgOverride = d.foregroundColorRGB;
}
isTop = d.options.layer === 'top';
}
});
// Apply selection foreground if applicable
const isInSelection = this._isCellInSelection(x, row);
+20 -4
View File
@@ -4,7 +4,7 @@
*/
import { ICharSizeService, IRenderService, IMouseService } from './Services';
import { getCoords, getRawByteCoords } from 'browser/input/Mouse';
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
export class MouseService implements IMouseService {
public serviceBrand: undefined;
@@ -29,8 +29,24 @@ export class MouseService implements IMouseService {
);
}
public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined {
const coords = this.getCoords(event, element, colCount, rowCount);
return getRawByteCoords(coords);
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
const coords = getCoordsRelativeToElement(window, event, element);
// due to rounding issues in zoom states pixel values might be negative or overflow actual canvas
// ignore those events effectively narrowing mouse area a tiny bit at the edges
if (!this._charSizeService.hasValidSize
|| coords[0] < 0
|| coords[1] < 0
|| coords[0] >= this._renderService.dimensions.canvasWidth
|| coords[1] >= this._renderService.dimensions.canvasHeight) {
return undefined;
}
return {
col: Math.floor(coords[0] / this._renderService.dimensions.actualCellWidth),
row: Math.floor(coords[1] / this._renderService.dimensions.actualCellHeight),
x: Math.floor(coords[0]),
y: Math.floor(coords[1])
};
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types';
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
+2 -4
View File
@@ -5,12 +5,10 @@
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet, IRenderDebouncer } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDisposable } from 'common/Types';
import { IDecorationOptions, IDecoration } from 'xterm';
import { IBufferService } from 'common/services/Services';
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
export interface ICharSizeService {
@@ -37,7 +35,7 @@ export interface IMouseService {
serviceBrand: undefined;
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined;
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
}
export const IRenderService = createDecorator<IRenderService>('RenderService');
+7 -3
View File
@@ -22,12 +22,12 @@
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, 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';
import { OptionsService } from 'common/services/OptionsService';
import { IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource, ITerminalOptions as IPublicTerminalOptions } from 'common/Types';
import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource } from 'common/Types';
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
@@ -39,6 +39,7 @@ import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { IBufferSet } from 'common/buffer/Types';
import { InputHandler } from 'common/InputHandler';
import { WriteBuffer } from 'common/input/WriteBuffer';
import { OscLinkService } from 'common/services/OscLinkService';
// Only trigger this warning a single time per session
let hasWriteSyncWarnHappened = false;
@@ -49,6 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _logService: ILogService;
protected readonly _charsetService: ICharsetService;
protected readonly _dirtyRowService: IDirtyRowService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
public readonly coreService: ICoreService;
@@ -118,9 +120,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
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.coreMouseService, this.unicodeService);
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
this.register(this._inputHandler);
+12 -6
View File
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockDirtyRowService, 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 MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), 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 MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), 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);
@@ -261,6 +261,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -307,6 +308,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -357,6 +359,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -394,6 +397,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -444,6 +448,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -570,6 +575,7 @@ describe('InputHandler', () => {
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
@@ -593,7 +599,7 @@ describe('InputHandler', () => {
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService());
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), 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');
@@ -790,7 +796,7 @@ describe('InputHandler', () => {
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
@@ -2156,7 +2162,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 MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
+74 -4
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, IOscLinkData } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -17,7 +17,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId
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 } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, 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';
@@ -214,8 +214,6 @@ class DECRQSS implements IDcsHandler {
* @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
*/
/**
* The terminal's standard implementation of IInputHandler, this handles all
* input from the Parser.
@@ -230,6 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _workCell: CellData = new CellData();
private _windowTitle = '';
private _iconName = '';
private _currentLinkId?: number;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -281,6 +280,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _dirtyRowService: IDirtyRowService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
private readonly _coreMouseService: ICoreMouseService,
private readonly _unicodeService: IUnicodeService,
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()
@@ -403,6 +403,8 @@ export class InputHandler extends Disposable implements IInputHandler {
// 5 - Change Special Color Number
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));
// 10 - Change VT100 text foreground color to Pt.
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
// 11 - Change VT100 text background color to Pt.
@@ -637,6 +639,9 @@ export class InputHandler extends Disposable implements IInputHandler {
if (screenReaderMode) {
this._onA11yChar.fire(stringFromCodePoint(code));
}
if (this._currentLinkId !== undefined) {
this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y);
}
// insert combining char at last cursor position
// this._activeBuffer.x should never be 0 for a combining char
@@ -1903,6 +1908,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 1005 | Enable UTF-8 Mouse Mode. | #N |
* | 1006 | Enable SGR Mouse Mode. | #Y |
* | 1015 | Enable urxvt Mouse Mode. | #N |
* | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |
* | 1047 | Use Alternate Screen Buffer. | #Y |
* | 1048 | Save cursor as in DECSC. | #Y |
* | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |
@@ -1984,6 +1990,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1015: // urxvt ext mode mouse - removed in #2507
this._logService.debug('DECSET 1015 not supported (see #2507)');
break;
case 1016: // sgr pixels mode mouse
this._coreMouseService.activeEncoding = 'SGR_PIXELS';
break;
case 25: // show cursor
this._coreService.isCursorHidden = false;
break;
@@ -2144,6 +2153,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 1005 | Disable UTF-8 Mouse Mode. | #N |
* | 1006 | Disable SGR Mouse Mode. | #Y |
* | 1015 | Disable urxvt Mouse Mode. | #N |
* | 1006 | Disable SGR-Pixels Mouse Mode. | #Y |
* | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |
* | 1048 | Restore cursor as in DECRC. | #Y |
* | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |
@@ -2205,6 +2215,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1015: // urxvt ext mode mouse - removed in #2507
this._logService.debug('DECRST 1015 not supported (see #2507)');
break;
case 1006: // sgr pixels mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
break;
case 25: // hide cursor
this._coreService.isCursorHidden = true;
break;
@@ -2495,6 +2508,7 @@ export class InputHandler extends Disposable implements IInputHandler {
} else if (p === 24) {
// not underlined
attr.fg &= ~FgFlags.UNDERLINE;
this._processUnderline(UnderlineStyle.NONE, attr);
} else if (p === 25) {
// not blink
attr.fg &= ~FgFlags.BLINK;
@@ -2889,6 +2903,62 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
/**
* OSC 8 ; <params> ; <uri> ST - create hyperlink
* OSC 8 ; ; ST - finish hyperlink
*
* Test case:
*
* ```sh
* printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
* ```
*
* @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`."
* `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an
* optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`.
* Currently only the id key is defined. Cells that share the same ID and URI share hover feedback.
* Use `OSC 8 ; ; BEL` to finish the current hyperlink.
*/
public setHyperlink(data: string): boolean {
const args = data.split(';');
if (args.length < 2) {
return false;
}
if (args[1]) {
return this._createHyperlink(args[0], args[1]);
}
if (args[0]) {
return false;
}
return this._finishHyperlink();
}
private _createHyperlink(params: string, uri: string): boolean {
// It's legal to open a new hyperlink without explicitly finishing the previous one
if (this._currentLinkId !== undefined) {
this._finishHyperlink();
}
const parsedParams = params.split(':');
let id: string | undefined;
const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));
if (idParamIndex !== -1) {
id = parsedParams[idParamIndex].slice(3) || undefined;
}
this._curAttrData.extended = this._curAttrData.extended.clone();
this._currentLinkId = this._oscLinkService.registerLink({ id, uri });
this._curAttrData.extended.urlId = this._currentLinkId;
this._curAttrData.updateExtended();
return true;
}
private _finishHyperlink(): boolean {
this._curAttrData.extended = this._curAttrData.extended.clone();
this._curAttrData.extended.urlId = 0;
this._curAttrData.updateExtended();
this._currentLinkId = undefined;
return true;
}
// special colors - OSC 10 | 11 | 12
private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR];
+69
View File
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { FourKeyMap, TwoKeyMap } from 'common/MultiKeyMap';
const strictEqual = assert.strictEqual;
describe('TwoKeyMap', () => {
let map: TwoKeyMap<number | string, number | string, string>;
beforeEach(() => {
map = new TwoKeyMap();
});
it('set, get', () => {
strictEqual(map.get(1, 2), undefined);
map.set(1, 2, 'foo');
strictEqual(map.get(1, 2), 'foo');
map.set(1, 3, 'bar');
strictEqual(map.get(1, 2), 'foo');
strictEqual(map.get(1, 3), 'bar');
map.set(2, 2, 'foo2');
map.set(2, 3, 'bar2');
strictEqual(map.get(1, 2), 'foo');
strictEqual(map.get(1, 3), 'bar');
strictEqual(map.get(2, 2), 'foo2');
strictEqual(map.get(2, 3), 'bar2');
});
it('clear', () => {
strictEqual(map.get(1, 2), undefined);
map.set(1, 2, 'foo');
strictEqual(map.get(1, 2), 'foo');
map.clear();
strictEqual(map.get(1, 2), undefined);
});
});
describe('FourKeyMap', () => {
let map: FourKeyMap<number | string, number | string, number | string, number | string, string>;
beforeEach(() => {
map = new FourKeyMap();
});
it('set, get', () => {
strictEqual(map.get(1, 2, 3, 4), undefined);
map.set(1, 2, 3, 4, 'foo');
strictEqual(map.get(1, 2, 3, 4), 'foo');
map.set(1, 3, 3, 4, 'bar');
strictEqual(map.get(1, 2, 3, 4), 'foo');
strictEqual(map.get(1, 3, 3, 4), 'bar');
map.set(2, 2, 3, 4, 'foo2');
map.set(2, 3, 3, 4, 'bar2');
strictEqual(map.get(1, 2, 3, 4), 'foo');
strictEqual(map.get(1, 3, 3, 4), 'bar');
strictEqual(map.get(2, 2, 3, 4), 'foo2');
strictEqual(map.get(2, 3, 3, 4), 'bar2');
});
it('clear', () => {
strictEqual(map.get(1, 2, 3, 4), undefined);
map.set(1, 2, 3, 4, 'foo');
strictEqual(map.get(1, 2, 3, 4), 'foo');
map.clear();
strictEqual(map.get(1, 2, 3, 4), undefined);
});
});
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {};
public set(first: TFirst, second: TSecond, value: TValue): void {
if (!this._data[first]) {
this._data[first] = {};
}
this._data[first as string | number]![second] = value;
}
public get(first: TFirst, second: TSecond): TValue | undefined {
return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined;
}
public clear(): void {
this._data = {};
}
}
export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();
public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
if (!this._data.get(first, second)) {
this._data.set(first, second, new TwoKeyMap());
}
this._data.get(first, second)!.set(third, fourth, value);
}
public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
return this._data.get(first, second)?.get(third, fourth);
}
public clear(): void {
this._data.clear();
}
}
+22 -3
View File
@@ -3,6 +3,9 @@
* @license MIT
*/
// Work variables to avoid garbage collection.
let i = 0;
/**
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
* list is based on binary search and as such locating a key will take O(log n) amortized, this
@@ -25,7 +28,7 @@ export class SortedList<T> {
this._array.push(value);
return;
}
const i = this._search(this._getKey(value), 0, this._array.length - 1);
i = this._search(this._getKey(value), 0, this._array.length - 1);
this._array.splice(i, 0, value);
}
@@ -37,7 +40,7 @@ export class SortedList<T> {
if (key === undefined) {
return false;
}
let i = this._search(key, 0, this._array.length - 1);
i = this._search(key, 0, this._array.length - 1);
if (i === -1) {
return false;
}
@@ -57,7 +60,7 @@ export class SortedList<T> {
if (this._array.length === 0) {
return;
}
let i = this._search(key, 0, this._array.length - 1);
i = this._search(key, 0, this._array.length - 1);
if (i < 0 || i >= this._array.length) {
return;
}
@@ -69,6 +72,22 @@ export class SortedList<T> {
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
}
public forEachByKey(key: number, callback: (value: T) => void): void {
if (this._array.length === 0) {
return;
}
i = this._search(key, 0, this._array.length - 1);
if (i < 0 || i >= this._array.length) {
return;
}
if (this._getKey(this._array[i]) !== key) {
return;
}
do {
callback(this._array[i]);
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
}
public values(): IterableIterator<T> {
return this._array.values();
}
+15 -4
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, 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 } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
import { IDecorationOptions, IDecoration } from 'xterm';
@@ -138,6 +138,18 @@ export class MockOptionsService implements IOptionsService {
}
}
export class MockOscLinkService implements IOscLinkService {
public serviceBrand: any;
public registerLink(linkData: IOscLinkData): number {
return 1;
}
public getLinkData(linkId: number): IOscLinkData | undefined {
return undefined;
}
public addLineToLink(linkId: number, y: number): void {
}
}
// defaults to V6 always to keep tests passing
export class MockUnicodeService implements IUnicodeService {
public serviceBrand: any;
@@ -161,7 +173,6 @@ export class MockDecorationService implements IDecorationService {
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
public reset(): void { }
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> { }
public *getDecorationsAtCell(x: number, line: number): IterableIterator<IInternalDecoration> { }
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { }
public dispose(): void { }
}
+17 -1
View File
@@ -9,6 +9,7 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { IParams } from 'common/parser/Types';
import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
import { IBufferSet } from 'common/buffer/Types';
import { UnderlineStyle } from 'common/buffer/Constants';
export interface ICoreTerminal {
coreMouseService: ICoreMouseService;
@@ -114,12 +115,24 @@ export type IColorRGB = [number, number, number];
export interface IExtendedAttrs {
ext: number;
underlineStyle: number;
underlineStyle: UnderlineStyle;
underlineColor: number;
urlId: number;
clone(): IExtendedAttrs;
isEmpty(): boolean;
}
/**
* Tracks the current hyperlink. Since these are treated as extended attirbutes, these get passed on
* to the linkifier when anything is printed. Doing it this way ensures that even when the cursor
* moves around unexpectedly the link is tracked, as opposed to using a start position and
* finalizing it at the end.
*/
export interface IOscLinkData {
id?: string;
uri: string;
}
/** Attribute data */
export interface IAttributeData {
fg: number;
@@ -268,6 +281,9 @@ export interface ICoreMouseEvent {
col: number;
/** row (zero based). */
row: number;
/** xy pixel positions. */
x: number;
y: number;
/**
* Button the action occured. Due to restrictions of the tracking protocols
* it is not possible to report multiple buttons at once.
+33 -8
View File
@@ -35,7 +35,12 @@ export class AttributeData implements IAttributeData {
// flags
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
public isBold(): number { return this.fg & FgFlags.BOLD; }
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
public isUnderline(): number {
if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {
return 1;
}
return this.fg & FgFlags.UNDERLINE;
}
public isBlink(): number { return this.fg & FgFlags.BLINK; }
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
@@ -128,10 +133,22 @@ export class AttributeData implements IAttributeData {
*/
export class ExtendedAttrs implements IExtendedAttrs {
private _ext: number = 0;
public get ext(): number { return this._ext; }
public get ext(): number {
if (this._urlId) {
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }
public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
@@ -147,16 +164,24 @@ export class ExtendedAttrs implements IExtendedAttrs {
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}
constructor(
underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
underlineColor: number = Attributes.CM_DEFAULT
ext: number = 0,
urlId: number = 0
) {
this.underlineStyle = underlineStyle;
this.underlineColor = underlineColor;
this._ext = ext;
this._urlId = urlId;
}
public clone(): IExtendedAttrs {
return new ExtendedAttrs(this.underlineStyle, this.underlineColor);
return new ExtendedAttrs(this._ext, this._urlId);
}
/**
@@ -164,6 +189,6 @@ export class ExtendedAttrs implements IExtendedAttrs {
* that needs to be persistant in the buffer.
*/
public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE;
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;
}
}
+9 -4
View File
@@ -37,6 +37,11 @@ const enum Cell {
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
/** Work variables to avoid garbage collection. */
const w: { startIndex: number } = {
startIndex: 0
};
/**
* Typed array based bufferline implementation.
*
@@ -168,10 +173,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 {
const 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];
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];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
+2 -2
View File
@@ -58,14 +58,14 @@ export class BufferSet extends Disposable implements IBufferSet {
}
/**
* Returns the normal Buffer of the BufferSet
* Returns the currently active Buffer of the BufferSet
*/
public get active(): Buffer {
return this._activeBuffer;
}
/**
* Returns the currently active Buffer of the BufferSet
* Returns the normal Buffer of the BufferSet
*/
public get normal(): Buffer {
return this._normal;
+4 -6
View File
@@ -32,13 +32,11 @@ export class BufferService extends Disposable implements IBufferService {
/** An IBufferline to clone/copy from for new blank lines */
private _cachedBlankLine: IBufferLine | undefined;
constructor(
@IOptionsService private _optionsService: IOptionsService
) {
constructor(@IOptionsService optionsService: IOptionsService) {
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);
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 {
+70 -62
View File
@@ -34,7 +34,7 @@ describe('CoreMouseService', () => {
});
it('default encodings - DEFAULT, SGR', () => {
const cms = new CoreMouseService(bufferService, coreService);
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR']);
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
});
it('protocol/encoding setter, reset', () => {
const cms = new CoreMouseService(bufferService, coreService);
@@ -82,68 +82,68 @@ describe('CoreMouseService', () => {
coreService.triggerBinaryEvent = (data: string) => reports.push(data);
});
it('NONE', () => {
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('X10', () => {
cms.activeProtocol = 'X10';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('VT200', () => {
cms.activeProtocol = 'VT200';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('DRAG', () => {
cms.activeProtocol = 'DRAG';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
});
it('ANY', () => {
cms.activeProtocol = 'ANY';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
// should not report in any case
// invalid button + action combinations
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
// invalid coords
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
});
describe('coords', () => {
it('DEFAULT encoding', () => {
cms.activeProtocol = 'ANY';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
if (i > 222) {
// supress mouse reports if we are out of addressible range (max. 222)
assert.deepEqual(toBytes(reports.pop()), []);
@@ -156,46 +156,54 @@ describe('CoreMouseService', () => {
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'SGR';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
}
});
it('SGR_PIXELS encoding', () => {
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'SGR_PIXELS';
for (let i = 0; i < 500; ++i) {
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`);
}
});
});
it('eventCodes with modifiers (DEFAULT encoding)', () => {
// TODO: implement AUX button tests
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'DEFAULT';
// all buttons + down + no modifer
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
reports = [];
// all buttons + up + no modifier
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
reports = [];
// all buttons + move + no modifier
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
reports = [];
// button none + move + modifiers
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
reports = [];
});
+23 -11
View File
@@ -9,7 +9,7 @@ import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventT
/**
* Supported default protocols.
*/
const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
const DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {
/**
* NONE
* Events: none
@@ -117,7 +117,7 @@ const S = String.fromCharCode;
/**
* Supported default encodings.
*/
const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
/**
* DEFAULT - CSI M Pb Px Py
* Single byte encoding for coords and event code.
@@ -142,6 +142,10 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
SGR: (e: ICoreMouseEvent) => {
const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
},
SGR_PIXELS: (e: ICoreMouseEvent) => {
const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
return `\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;
}
};
@@ -162,8 +166,8 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
* To send a mouse event call `triggerMouseEvent`.
*/
export class CoreMouseService implements ICoreMouseService {
private _protocols: {[name: string]: ICoreMouseProtocol} = {};
private _encodings: {[name: string]: CoreMouseEncoding} = {};
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
private _encodings: { [name: string]: CoreMouseEncoding } = {};
private _activeProtocol: string = '';
private _activeEncoding: string = '';
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
@@ -241,7 +245,7 @@ export class CoreMouseService implements ICoreMouseService {
public triggerMouseEvent(e: ICoreMouseEvent): boolean {
// range check for col/row
if (e.col < 0 || e.col >= this._bufferService.cols
|| e.row < 0 || e.row >= this._bufferService.rows) {
|| e.row < 0 || e.row >= this._bufferService.rows) {
return false;
}
@@ -260,8 +264,11 @@ export class CoreMouseService implements ICoreMouseService {
e.col++;
e.row++;
// debounce move at grid level
if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
// debounce move events at grid or pixel level
if (e.action === CoreMouseAction.MOVE
&& this._lastEvent
&& this._equalEvents(this._lastEvent, e, this._activeEncoding === 'SGR_PIXELS')
) {
return false;
}
@@ -286,7 +293,7 @@ export class CoreMouseService implements ICoreMouseService {
return true;
}
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
return {
down: !!(events & CoreMouseEventType.DOWN),
up: !!(events & CoreMouseEventType.UP),
@@ -296,9 +303,14 @@ export class CoreMouseService implements ICoreMouseService {
};
}
private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
if (e1.col !== e2.col) return false;
if (e1.row !== e2.row) return false;
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
if (pixels) {
if (e1.x !== e2.x) return false;
if (e1.y !== e2.y) return false;
} else {
if (e1.col !== e2.col) return false;
if (e1.row !== e2.row) return false;
}
if (e1.button !== e2.button) return false;
if (e1.action !== e2.action) return false;
if (e1.ctrl !== e2.ctrl) return false;
+16 -4
View File
@@ -11,6 +11,12 @@ 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
};
export class DecorationService extends Disposable implements IDecorationService {
public serviceBrand: any;
@@ -56,10 +62,6 @@ export class DecorationService extends Disposable implements IDecorationService
this._decorations.clear();
}
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> {
return this._decorations.getKeyIterator(line);
}
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
let xmin = 0;
let xmax = 0;
@@ -72,6 +74,16 @@ 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)) {
callback(d);
}
});
}
public dispose(): void {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
+1 -1
View File
@@ -24,6 +24,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
fontWeightBold: 'bold',
lineHeight: 1.0,
letterSpacing: 0,
linkHandler: null,
logLevel: 'info',
scrollback: 1000,
scrollSensitivity: 1,
@@ -119,7 +120,6 @@ export class OptionsService implements IOptionsService {
throw new Error(`"${value}" is not a valid value for ${key}`);
}
break;
case 'cursorStyle':
case 'wordSeparator':
if (!value) {
value = DEFAULT_OPTIONS[key];
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { AttributeData } from 'common/buffer/AttributeData';
import { BufferService } from 'common/services/BufferService';
import { OptionsService } from 'common/services/OptionsService';
import { OscLinkService } from 'common/services/OscLinkService';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
describe('OscLinkService', () => {
describe('constructor', () => {
let bufferService: IBufferService;
let optionsService: IOptionsService;
let oscLinkService: IOscLinkService;
beforeEach(() => {
optionsService = new OptionsService({ rows: 3, cols: 10 });
bufferService = new BufferService(optionsService);
oscLinkService = new OscLinkService(bufferService);
});
it('link IDs are created and fetched consistently', () => {
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
assert.ok(linkId);
assert.equal(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId);
});
it('should dispose the link ID when the last marker is trimmed from the buffer', () => {
// Activate the alt buffer to get 0 scrollback
bufferService.buffers.activateAltBuffer();
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
assert.ok(linkId);
bufferService.scroll(new AttributeData());
assert.notStrictEqual(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId);
});
it('should fetch link data from link id', () => {
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
assert.deepStrictEqual(oscLinkService.getLinkData(linkId), { id: 'foo', uri: 'bar' });
});
});
});
+115
View File
@@ -0,0 +1,115 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferService, IOscLinkService } from 'common/services/Services';
import { IMarker, IOscLinkData } from 'common/Types';
export class OscLinkService implements IOscLinkService {
public serviceBrand: any;
private _nextId = 1;
/**
* A map of the link key to link entry. This is used to add additional lines to links with ids.
*/
private _entriesWithId: Map<string, IOscLinkEntryWithId> = new Map();
/**
* A map of the link id to the link entry. The "link id" (number) which is the numberic
* representation of a unique link should not be confused with "id" (string) which comes in with
* `id=` in the OSC link's properties.
*/
private _dataByLinkId: Map<number, IOscLinkEntryNoId | IOscLinkEntryWithId> = new Map();
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
}
public registerLink(data: IOscLinkData): number {
const buffer = this._bufferService.buffer;
// Links with no id will only ever be registered a single time
if (data.id === undefined) {
const marker = buffer.addMarker(buffer.ybase + buffer.y);
const entry: IOscLinkEntryNoId = {
data,
id: this._nextId++,
lines: [marker]
};
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
this._dataByLinkId.set(entry.id, entry);
return entry.id;
}
// Add the line to the link if it already exists
const castData = data as Required<IOscLinkData>;
const key = this._getEntryIdKey(castData);
const match = this._entriesWithId.get(key);
if (match) {
this.addLineToLink(match.id, buffer.ybase + buffer.y);
return match.id;
}
// Create the link
const marker = buffer.addMarker(buffer.ybase + buffer.y);
const entry: IOscLinkEntryWithId = {
id: this._nextId++,
key: this._getEntryIdKey(castData),
data: castData,
lines: [marker]
};
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
this._entriesWithId.set(entry.key, entry);
this._dataByLinkId.set(entry.id, entry);
return entry.id;
}
public addLineToLink(linkId: number, y: number): void {
const entry = this._dataByLinkId.get(linkId);
if (!entry) {
return;
}
if (entry.lines.every(e => e.line !== y)) {
const marker = this._bufferService.buffer.addMarker(y);
entry.lines.push(marker);
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
}
}
public getLinkData(linkId: number): IOscLinkData | undefined {
return this._dataByLinkId.get(linkId)?.data;
}
private _getEntryIdKey(linkData: Required<IOscLinkData>): string {
return `${linkData.id};;${linkData.uri}`;
}
private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {
const index = entry.lines.indexOf(marker);
if (index === -1) {
return;
}
entry.lines.splice(index, 1);
if (entry.lines.length === 0) {
if (entry.data.id !== undefined) {
this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);
}
this._dataByLinkId.delete(entry.id);
}
}
}
interface IOscLinkEntry<T extends IOscLinkData> {
data: T;
id: number;
lines: IMarker[];
}
interface IOscLinkEntryNoId extends IOscLinkEntry<IOscLinkData> {
}
interface IOscLinkEntryWithId extends IOscLinkEntry<Required<IOscLinkData>> {
key: string;
}
+24 -6
View File
@@ -5,9 +5,9 @@
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDecorationOptions, IDecoration } from 'xterm';
import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm';
export const IBufferService = createDecorator<IBufferService>('BufferService');
export interface IBufferService {
@@ -223,6 +223,7 @@ export interface ITerminalOptions {
fontWeightBold?: FontWeight;
letterSpacing?: number;
lineHeight?: number;
linkHandler?: ILinkHandler | null;
logLevel?: LogLevel;
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
@@ -272,6 +273,22 @@ export interface ITheme {
extendedAnsi?: string[];
}
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');
export interface IOscLinkService {
serviceBrand: undefined;
/**
* Registers a link to the service, returning the link ID. The link data is managed by this
* service and will be freed when this current cursor position is trimmed off the buffer.
*/
registerLink(linkData: IOscLinkData): number;
/**
* Adds a line to a link if needed.
*/
addLineToLink(linkId: number, y: number): void;
/** Get the link data associated with a link ID. */
getLinkData(linkId: number): IOscLinkData | undefined;
}
export const IUnicodeService = createDecorator<IUnicodeService>('UnicodeService');
export interface IUnicodeService {
serviceBrand: undefined;
@@ -304,10 +321,11 @@ export interface IDecorationService extends IDisposable {
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
reset(): void;
/** Iterates over the decorations at a line (in no particular order). */
getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration>;
/** Iterates over the decorations at a cell (in no particular order). */
getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration>;
/**
* Trigger a callback over the decoration at a cell (in no particular order). This uses a callback
* instead of an iterator as it's typically used in hot code paths.
*/
forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;
}
export interface IInternalDecoration extends IDecoration {
readonly options: IDecorationOptions;
+2 -2
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [ "es5" ],
"module": "commonjs",
"target": "es2015",
"rootDir": ".",
"sourceMap": true,
+223 -236
View File
@@ -21,9 +21,6 @@ const fontSize = 6;
const cols = 260;
const rows = 50;
// Wheel events are hacked using private API that is only available in Chromium
const isChromium = false;
// for some reason shift gets not caught by selection manager on macos
const noShift = process.platform === 'darwin' ? false : true;
@@ -74,26 +71,16 @@ async function mouseDown(button: 'left' | 'right' | 'middle' | undefined): Promi
async function mouseUp(button: 'left' | 'right' | 'middle' | undefined): Promise<void> {
return await page.mouse.up({ button });
}
async function wheelUp(): Promise<void> {
const self = (page.mouse as any);
return await self._raw._client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x: self._x,
y: self._y,
deltaX: 0,
deltaY: -10
});
}
async function wheelDown(): Promise<void> {
const self = (page.mouse as any);
return await self._raw._client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x: self._x,
y: self._y,
deltaX: 0,
deltaY: 10
});
}
// FIXME: wheel event dont work anymore (playwright issue?)
// --> commented out in the tests below
// async function wheelUp(): Promise<void> {
// return await page.mouse.wheel(0, -10);
// }
// async function wheelDown(): Promise<void> {
// return await page.mouse.wheel(0, 10);
// }
// FIXME: complex Shift tests dont work anymore, also commented out
function toModifiersMask(modifiers: Set<String>): number {
let mask = 0;
@@ -208,7 +195,7 @@ function parseReport(encoding: string, msg: number[]): { state: any, row: number
describe('Mouse Tracking Tests', async () => {
const browserType = getBrowserType();
browserType.name() === 'chromium';
const itMouse = isChromium ? it : it.skip;
const itMouse = it;
before(async function(): Promise<void> {
browser = await launchBrowser();
@@ -302,12 +289,12 @@ describe('Mouse Tracking Tests', async () => {
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), []);
await wheelDown();
await pollFor(page, () => getReports(encoding), []);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), []);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), []);
// modifiers
// CTRL
@@ -317,7 +304,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]);
@@ -329,7 +316,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]);
@@ -343,7 +330,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), []);
@@ -363,7 +350,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -422,12 +409,12 @@ describe('Mouse Tracking Tests', async () => {
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), []);
await wheelDown();
await pollFor(page, () => getReports(encoding), []);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), []);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), []);
// modifiers
// CTRL
@@ -437,7 +424,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]);
@@ -449,7 +436,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]);
@@ -461,7 +448,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), []);
@@ -481,7 +468,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -566,12 +553,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -581,12 +568,12 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -596,36 +583,36 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release caught by selection manager
// bug: modifier not reported for passed events
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await page.keyboard.down('Shift'); // defaults to ShiftLeft
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// // await wheelDown();
// await page.keyboard.up('Shift');
// // if (noShift) {
// // await pollFor(page, () => getReports(encoding), [
// // // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// // ]);
// // } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// // }
// all modifiers
// bug: Shift not working - selection manager?
@@ -637,14 +624,14 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
itMouse('SGR encoding', async () => {
@@ -716,12 +703,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -731,12 +718,12 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -746,35 +733,35 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release caught by selection manager
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await page.keyboard.down('Shift'); // defaults to ShiftLeft
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// await wheelDown();
// await page.keyboard.up('Shift');
// if (noShift) {
// await pollFor(page, () => getReports(encoding), [
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// }
// all modifiers
// bug: Shift not working - selection manager?
@@ -786,14 +773,14 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
});
@@ -877,12 +864,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -892,13 +879,13 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -908,37 +895,37 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release/drag caught by selection manager
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await page.keyboard.down('Shift'); // defaults to ShiftLeft
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// await wheelDown();
// await page.keyboard.up('Shift');
// if (noShift) {
// await pollFor(page, () => getReports(encoding), [
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// }
// all modifiers
// bug: Shift not working
@@ -950,7 +937,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -958,7 +945,7 @@ describe('Mouse Tracking Tests', async () => {
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
itMouse('SGR encoding', async () => {
@@ -1033,12 +1020,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -1048,13 +1035,13 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -1064,37 +1051,37 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release/drag caught by selection manager
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await page.keyboard.down('Shift'); // defaults to ShiftLeft
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// await wheelDown();
// await page.keyboard.up('Shift');
// if (noShift) {
// await pollFor(page, () => getReports(encoding), [
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// }
// all modifiers
// bug: this is totally broken with wrong coords and messed up modifiers
@@ -1106,7 +1093,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -1114,7 +1101,7 @@ describe('Mouse Tracking Tests', async () => {
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
});
@@ -1198,12 +1185,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -1212,14 +1199,14 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: true, shift: false, meta: false } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -1228,39 +1215,39 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: false, meta: true } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release/drag caught by selection manager
await page.keyboard.down('Shift');
await mouseMove(43, 24);
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await page.keyboard.down('Shift');
// await mouseMove(43, 24);
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// await wheelDown();
// await page.keyboard.up('Shift');
// if (noShift) {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// }
// all modifiers
// bug: Shift not working
@@ -1271,7 +1258,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -1280,7 +1267,7 @@ describe('Mouse Tracking Tests', async () => {
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: '<none>', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
itMouse('SGR encoding', async () => {
@@ -1359,12 +1346,12 @@ describe('Mouse Tracking Tests', async () => {
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
await wheelDown();
await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await mouseMove(43, 24);
// await getReports(encoding); // clear reports
// await wheelUp();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// await wheelDown();
// await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]);
// modifiers
// CTRL
@@ -1373,14 +1360,14 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: true, shift: false, meta: false } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } }
]);
// ALT
@@ -1389,39 +1376,39 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Alt');
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: false, meta: true } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } }
]);
// SHIFT
// note: press/release/drag caught by selection manager
await page.keyboard.down('Shift');
await mouseMove(43, 24);
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
} else {
await pollFor(page, () => getReports(encoding), [
{ col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
]);
}
// await page.keyboard.down('Shift');
// await mouseMove(43, 24);
// await mouseDown('left');
// await mouseMove(44, 24);
// await mouseUp('left');
// await wheelDown();
// await page.keyboard.up('Shift');
// if (noShift) {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// } else {
// await pollFor(page, () => getReports(encoding), [
// { col: 44, row: 25, state: { action: 'move', button: '<none>', modifier: { control: false, shift: true, meta: false } } },
// { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } },
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } }
// ]);
// }
// all modifiers
// bug: Shift not working
@@ -1432,7 +1419,7 @@ describe('Mouse Tracking Tests', async () => {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
// await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
// await page.keyboard.up('Shift');
@@ -1441,7 +1428,7 @@ describe('Mouse Tracking Tests', async () => {
{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } },
{ col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
// { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } }
]);
});
});
+39
View File
@@ -128,6 +128,14 @@ declare module 'xterm' {
*/
lineHeight?: number;
/**
* The handler for OSC 8 hyperlinks. Links will use the `confirm` browser
* API if no link handler is set. Consider the security of users when using
* this, there should be some tooltip or prompt when hovering or activating
* the link.
*/
linkHandler?: ILinkHandler | null;
/**
* What log level to use, this will log for all levels below and including
* what is set:
@@ -1106,6 +1114,37 @@ declare module 'xterm' {
y: number;
}
/**
* A link handler for OSC 8 hyperlinks.
*/
interface ILinkHandler {
/**
* Calls when the link is activated.
* @param event The mouse event triggering the callback.
* @param text The text of the link.
* @param range The buffer range of the link.
*/
activate(event: MouseEvent, text: string, range: IBufferRange): void;
/**
* Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip,
* create the hover element within `Terminal.element` and add the `xterm-hover` class to it,
* that will cause mouse events to not fall through and activate other links.
* @param event The mouse event triggering the callback.
* @param text The text of the link.
* @param range The buffer range of the link.
*/
hover?(event: MouseEvent, text: string, range: IBufferRange): void;
/**
* Called when the mouse leaves the link.
* @param event The mouse event triggering the callback.
* @param text The text of the link.
* @param range The buffer range of the link.
*/
leave?(event: MouseEvent, text: string, range: IBufferRange): void;
}
/**
* A custom link provider.
*/