mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into clusters
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
FROM node:14-buster
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install --no-install-recommends apt-utils 2>&1
|
||||
|
||||
# Verify git and process tools are installed
|
||||
RUN apt-get install -y git procps
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get install -y curl apt-transport-https lsb-release \
|
||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
yarn
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
@@ -1,10 +1,20 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"dockerFile": "Dockerfile",
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {} // yarn
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000
|
||||
],
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
|
||||
- [**graSSHopper**](https://grasshopper.coding.kiwi): A simple SSH client with file explorer, history and many more features.
|
||||
- [**DomTerm**](https://domterm.org/xtermjs.html): Tiles and tabs. Detachable sessions (like tmux). [Remote connections](https://domterm.org/Remoting-over-ssh.html) using a nice ssh wrapper with predictive echo. Qt, Electron, Tauri/Wry, or desktop browser front-ends. Choose between xterm.js engine (faster) or native DomTerm (more functionality and graphics) - or both.
|
||||
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-canvas",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-search",
|
||||
"version": "0.11.0",
|
||||
"version": "0.12.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
pr:
|
||||
branches:
|
||||
include: ["main", "v5"]
|
||||
include: ["main"]
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include: ["main", "v5"]
|
||||
include: ["main"]
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
displayName: Install required packages
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -111,11 +111,11 @@ jobs:
|
||||
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
|
||||
# - job: macOS_IntegrationTests
|
||||
# pool:
|
||||
# vmImage: 'macOS-10.15'
|
||||
# vmImage: 'macOS-11'
|
||||
# steps:
|
||||
# - task: NodeTool@0
|
||||
# inputs:
|
||||
# versionSpec: '14.x'
|
||||
# versionSpec: '18.x'
|
||||
# displayName: 'Install Node.js'
|
||||
# - script: yarn --frozen-lockfile
|
||||
# displayName: 'Install dependencies and build'
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
|
||||
+5
-1
@@ -248,7 +248,11 @@ function createTerminal(): void {
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
allowProposedApi: true,
|
||||
windowsMode: isWindows,
|
||||
windowsPty: isWindows ? {
|
||||
// In a real scenario, these values should be verified on the backend
|
||||
backend: 'conpty',
|
||||
buildNumber: 22621
|
||||
} : undefined,
|
||||
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
|
||||
theme: xtermjsTheme
|
||||
} as ITerminalOptions);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.1",
|
||||
"main": "lib/xterm.js",
|
||||
"style": "css/xterm.css",
|
||||
"types": "typings/xterm.d.ts",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.44",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/utf8": "^3.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
|
||||
@@ -8,16 +8,32 @@ import { ITerminal, IRenderDebouncer } from 'browser/Types';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
const enum BoundaryPosition {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
export class AccessibilityManager extends Disposable {
|
||||
private _accessibilityContainer: HTMLElement;
|
||||
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[];
|
||||
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
private _liveRegionDebouncer: IRenderDebouncer;
|
||||
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
/**
|
||||
* This queue has a character pushed to it for keys that are pressed, if the
|
||||
* next character added to the terminal is equal to the key char then it is
|
||||
@@ -32,12 +48,30 @@ export class AccessibilityManager extends Disposable {
|
||||
private _charsToAnnounce: string = '';
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: ITerminal
|
||||
private readonly _terminal: ITerminal,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
super();
|
||||
this._accessibilityContainer = document.createElement('div');
|
||||
this._accessibilityContainer.classList.add('xterm-accessibility');
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.setAttribute('role', 'list');
|
||||
this._rowContainer.classList.add('xterm-accessibility-tree');
|
||||
this._rowElements = [];
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityContainer.appendChild(this._rowContainer);
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
this._liveRegion.classList.add('live-region');
|
||||
this._liveRegion.setAttribute('aria-live', 'assertive');
|
||||
@@ -50,6 +84,7 @@ export class AccessibilityManager extends Disposable {
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
|
||||
|
||||
this.register(this._liveRegionDebouncer);
|
||||
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
@@ -58,7 +93,20 @@ export class AccessibilityManager extends Disposable {
|
||||
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
|
||||
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
|
||||
this.register(toDisposable(() => this._accessibilityContainer.remove()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor(window);
|
||||
this.register(this._screenDprMonitor);
|
||||
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the ScreenDprMonitor isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
|
||||
this._refreshRows();
|
||||
this.register(toDisposable(() => {
|
||||
this._accessibilityContainer.remove();
|
||||
this._rowElements.length = 0;
|
||||
}));
|
||||
}
|
||||
|
||||
private _handleTab(spaceCount: number): void {
|
||||
@@ -126,4 +174,107 @@ export class AccessibilityManager extends Disposable {
|
||||
this._liveRegion.textContent += this._charsToAnnounce;
|
||||
this._charsToAnnounce = '';
|
||||
}
|
||||
|
||||
private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = e.target as HTMLElement;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
const posInSet = boundaryElement.getAttribute('aria-posinset');
|
||||
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
|
||||
if (posInSet === lastRowPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't scroll when the last focused item was not the second row (focus is going the other
|
||||
// direction)
|
||||
if (e.relatedTarget !== beforeBoundaryElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old boundary element from array
|
||||
let topBoundaryElement: HTMLElement;
|
||||
let bottomBoundaryElement: HTMLElement;
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
topBoundaryElement = boundaryElement;
|
||||
bottomBoundaryElement = this._rowElements.pop()!;
|
||||
this._rowContainer.removeChild(bottomBoundaryElement);
|
||||
} else {
|
||||
topBoundaryElement = this._rowElements.shift()!;
|
||||
bottomBoundaryElement = boundaryElement;
|
||||
this._rowContainer.removeChild(topBoundaryElement);
|
||||
}
|
||||
|
||||
// Remove listeners from old boundary elements
|
||||
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
|
||||
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Add new element to array/DOM
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.unshift(newElement);
|
||||
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
|
||||
} else {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.push(newElement);
|
||||
this._rowContainer.appendChild(newElement);
|
||||
}
|
||||
|
||||
// Add listeners to new boundary elements
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Scroll up
|
||||
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
|
||||
|
||||
// Focus new boundary before element
|
||||
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
|
||||
|
||||
// Prevent the standard behavior
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _handleResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Grow rows as required
|
||||
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
// Shrink rows as required
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop()!);
|
||||
}
|
||||
|
||||
// Add bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
private _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
this._refreshRowDimensions(element);
|
||||
return element;
|
||||
}
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._renderService.dimensions.css.cell.height) {
|
||||
return;
|
||||
}
|
||||
this._accessibilityContainer.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._handleResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
}
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1041,6 +1041,47 @@ describe('Terminal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Windows Pty', () => {
|
||||
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
|
||||
const data = [
|
||||
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
|
||||
'aaaaaaaaa\n\r', // wrapped (windows mode only)
|
||||
'aaaaaaaaa' // not wrapped
|
||||
];
|
||||
|
||||
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: {} });
|
||||
await normalTerminal.writeP(data.join(''));
|
||||
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
|
||||
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: { backend: 'conpty', buildNumber: 19000 } });
|
||||
await windowsModeTerminal.writeP(data.join(''));
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
});
|
||||
|
||||
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => {
|
||||
const data = [
|
||||
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
|
||||
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
|
||||
'aaaaaaaaa' // not wrapped
|
||||
];
|
||||
|
||||
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: {} });
|
||||
await normalTerminal.writeP(data.join(''));
|
||||
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
|
||||
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: { backend: 'conpty', buildNumber: 19000 } });
|
||||
await windowsModeTerminal.writeP(data.join(''));
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
});
|
||||
});
|
||||
describe('Windows Mode', () => {
|
||||
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
|
||||
const data = [
|
||||
|
||||
@@ -393,8 +393,37 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
|
||||
x = this._cellToRowElements[y][x];
|
||||
x2 = this._cellToRowElements[y2][x2];
|
||||
/**
|
||||
* NOTE: The linkifier may send out of viewport y-values if:
|
||||
* - negative y-value: the link started at a higher line
|
||||
* - y-value >= maxY: the link ends at a line below viewport
|
||||
*
|
||||
* For negative y-values we can simply adjust x = 0,
|
||||
* as higher up link start means, that everything from
|
||||
* (0,0) is a link under top-down-left-right char progression
|
||||
*
|
||||
* Additionally there might be a small chance of out-of-sync x|y-values
|
||||
* from a race condition of render updates vs. link event handler execution:
|
||||
* - (sync) resize: chances terminal buffer in sync, schedules render update async
|
||||
* - (async) link handler race condition: new buffer metrics, but still on old render state
|
||||
* - (async) render update: brings term metrics and render state back in sync
|
||||
*/
|
||||
if (y < 0) x = 0;
|
||||
if (y2 < 0) x2 = 0;
|
||||
|
||||
// avoid out-of-sync y-values, simply clamp into valid area
|
||||
const maxY = this._cellToRowElements.length - 1;
|
||||
y = Math.max(Math.min(y, maxY), 0);
|
||||
y2 = Math.max(Math.min(y2, maxY), 0);
|
||||
const elemY = this._cellToRowElements[y];
|
||||
const elemY2 = this._cellToRowElements[y2];
|
||||
if (x >= elemY.length || x2 >= elemY2.length) {
|
||||
// avoid out-of-sync x-values
|
||||
// simply exit early, gets fixed by the next render update
|
||||
return;
|
||||
}
|
||||
x = elemY[x];
|
||||
x2 = elemY2[x2];
|
||||
|
||||
if (x === -1 || x2 === -1) {
|
||||
return;
|
||||
|
||||
+23
-19
@@ -57,7 +57,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
|
||||
protected _inputHandler: InputHandler;
|
||||
private _writeBuffer: WriteBuffer;
|
||||
private _windowsMode: IDisposable | undefined;
|
||||
private _windowsWrappingHeuristics: IDisposable | undefined;
|
||||
|
||||
private readonly _onBinary = this.register(new EventEmitter<string>());
|
||||
public readonly onBinary = this._onBinary.event;
|
||||
@@ -131,7 +131,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
|
||||
this.register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom()));
|
||||
this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
|
||||
this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e)));
|
||||
this.register(this.optionsService.onMultipleOptionChange(['windowsMode', 'windowsPty'], () => this._handleWindowsPtyOptionChange()));
|
||||
this.register(this._bufferService.onScroll(event => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
@@ -146,8 +146,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
this._windowsMode?.dispose();
|
||||
this._windowsMode = undefined;
|
||||
this._windowsWrappingHeuristics?.dispose();
|
||||
this._windowsWrappingHeuristics = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -250,9 +250,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
if (this.optionsService.rawOptions.windowsMode) {
|
||||
this._enableWindowsMode();
|
||||
}
|
||||
this._handleWindowsPtyOptionChange();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
@@ -263,30 +261,36 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.coreMouseService.reset();
|
||||
}
|
||||
|
||||
private _handleWindowsModeOptionChange(value: boolean): void {
|
||||
|
||||
private _handleWindowsPtyOptionChange(): void {
|
||||
let value = false;
|
||||
const windowsPty = this.optionsService.rawOptions.windowsPty;
|
||||
if (windowsPty && windowsPty.buildNumber !== undefined && windowsPty.buildNumber !== undefined) {
|
||||
value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);
|
||||
} else if (this.optionsService.rawOptions.windowsMode) {
|
||||
value = true;
|
||||
}
|
||||
if (value) {
|
||||
this._enableWindowsMode();
|
||||
this._enableWindowsWrappingHeuristics();
|
||||
} else {
|
||||
this._windowsMode?.dispose();
|
||||
this._windowsMode = undefined;
|
||||
this._windowsWrappingHeuristics?.dispose();
|
||||
this._windowsWrappingHeuristics = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected _enableWindowsMode(): void {
|
||||
if (!this._windowsMode) {
|
||||
protected _enableWindowsWrappingHeuristics(): void {
|
||||
if (!this._windowsWrappingHeuristics) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
|
||||
disposables.push(this.registerCsiHandler({ final: 'H' }, () => {
|
||||
updateWindowsModeWrappedState(this._bufferService);
|
||||
return false;
|
||||
}));
|
||||
this._windowsMode = {
|
||||
dispose: () => {
|
||||
for (const d of disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
this._windowsWrappingHeuristics = toDisposable(() => {
|
||||
for (const d of disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2222,7 +2222,7 @@ describe('InputHandler', () => {
|
||||
});
|
||||
it('ANSI 2 (keyboard action mode)', async () => {
|
||||
await inputHandler.parseP('\x1b[2$p');
|
||||
assert.deepEqual(reportStack.pop(), '\x1b[2;3$y'); // always set
|
||||
assert.deepEqual(reportStack.pop(), '\x1b[2;4$y'); // always reset
|
||||
});
|
||||
it('ANSI 4 (insert mode)', async () => {
|
||||
await inputHandler.parseP('\x1b[4$p');
|
||||
@@ -2236,7 +2236,7 @@ describe('InputHandler', () => {
|
||||
});
|
||||
it('ANSI 12 (send/receive)', async () => {
|
||||
await inputHandler.parseP('\x1b[12$p');
|
||||
assert.deepEqual(reportStack.pop(), '\x1b[12;4$y'); // always reset
|
||||
assert.deepEqual(reportStack.pop(), '\x1b[12;3$y'); // always set
|
||||
});
|
||||
it('ANSI 20 (newline mode)', async () => {
|
||||
await inputHandler.parseP('\x1b[20$p');
|
||||
@@ -2280,7 +2280,7 @@ describe('InputHandler', () => {
|
||||
});
|
||||
it('DEC privates perma modes', async () => {
|
||||
// [mode number, state value]
|
||||
const perma = [[3, 0], [8, 3], [1005, 4], [1015, 4], [1048, 1]];
|
||||
const perma = [[3, 0], [8, 3], [67, 4], [1005, 4], [1015, 4], [1048, 1]];
|
||||
for (const [mode, value] of perma) {
|
||||
await inputHandler.parseP(`\x1b[?${mode}$p`);
|
||||
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};${value}$y`);
|
||||
|
||||
@@ -1163,10 +1163,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* @param y row index
|
||||
*/
|
||||
private _resetBufferLine(y: number, respectProtect: boolean = false): void {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);
|
||||
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
|
||||
line.isWrapped = false;
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);
|
||||
if (line) {
|
||||
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);
|
||||
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
|
||||
line.isWrapped = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2234,9 +2236,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
const p = params.params[0];
|
||||
|
||||
if (ansi) {
|
||||
if (p === 2) return f(p, V.PERMANENTLY_SET);
|
||||
if (p === 2) return f(p, V.PERMANENTLY_RESET);
|
||||
if (p === 4) return f(p, b2v(cs.modes.insertMode));
|
||||
if (p === 12) return f(p, V.PERMANENTLY_RESET);
|
||||
if (p === 12) return f(p, V.PERMANENTLY_SET);
|
||||
if (p === 20) return f(p, b2v(opts.convertEol));
|
||||
return f(p, V.NOT_RECOGNIZED);
|
||||
}
|
||||
@@ -2251,6 +2253,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (p === 25) return f(p, b2v(!cs.isCursorHidden));
|
||||
if (p === 45) return f(p, b2v(dm.reverseWraparound));
|
||||
if (p === 66) return f(p, b2v(dm.applicationKeypad));
|
||||
if (p === 67) return f(p, V.PERMANENTLY_RESET);
|
||||
if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));
|
||||
if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));
|
||||
if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));
|
||||
|
||||
@@ -177,7 +177,7 @@ export class Buffer implements IBuffer {
|
||||
if (this._rows < newRows) {
|
||||
for (let y = this._rows; y < newRows; y++) {
|
||||
if (this.lines.length < newRows + this.ybase) {
|
||||
if (this._optionsService.rawOptions.windowsMode) {
|
||||
if (this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
|
||||
// Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
@@ -290,6 +290,10 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private get _isReflowEnabled(): boolean {
|
||||
const windowsPty = this._optionsService.rawOptions.windowsPty;
|
||||
if (windowsPty && windowsPty.buildNumber) {
|
||||
return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;
|
||||
}
|
||||
return this._hasScrollback && !this._optionsService.rawOptions.windowsMode;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ const LOG_PREFIX = 'xterm.js: ';
|
||||
export class LogService extends Disposable implements ILogService {
|
||||
public serviceBrand: any;
|
||||
|
||||
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
|
||||
private _logLevel: LogLevelEnum = LogLevelEnum.OFF;
|
||||
public get logLevel(): LogLevelEnum { return this._logLevel; }
|
||||
|
||||
constructor(
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
@@ -44,7 +45,7 @@ export class LogService extends Disposable implements ILogService {
|
||||
}
|
||||
|
||||
private _updateLogLevel(): void {
|
||||
this.logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];
|
||||
this._logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];
|
||||
}
|
||||
|
||||
private _evalLazyOptionalParams(optionalParams: any[]): void {
|
||||
@@ -57,30 +58,30 @@ export class LogService extends Disposable implements ILogService {
|
||||
|
||||
private _log(type: LogType, message: string, optionalParams: any[]): void {
|
||||
this._evalLazyOptionalParams(optionalParams);
|
||||
type.call(console, LOG_PREFIX + message, ...optionalParams);
|
||||
type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams);
|
||||
}
|
||||
|
||||
public debug(message: string, ...optionalParams: any[]): void {
|
||||
if (this.logLevel <= LogLevelEnum.DEBUG) {
|
||||
this._log(console.log, message, optionalParams);
|
||||
if (this._logLevel <= LogLevelEnum.DEBUG) {
|
||||
this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public info(message: string, ...optionalParams: any[]): void {
|
||||
if (this.logLevel <= LogLevelEnum.INFO) {
|
||||
this._log(console.info, message, optionalParams);
|
||||
if (this._logLevel <= LogLevelEnum.INFO) {
|
||||
this._log(this._optionsService.options.logger?.info.bind(this._optionsService.options.logger) ?? console.info, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public warn(message: string, ...optionalParams: any[]): void {
|
||||
if (this.logLevel <= LogLevelEnum.WARN) {
|
||||
this._log(console.warn, message, optionalParams);
|
||||
if (this._logLevel <= LogLevelEnum.WARN) {
|
||||
this._log(this._optionsService.options.logger?.warn.bind(this._optionsService.options.logger) ?? console.warn, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public error(message: string, ...optionalParams: any[]): void {
|
||||
if (this.logLevel <= LogLevelEnum.ERROR) {
|
||||
this._log(console.error, message, optionalParams);
|
||||
if (this._logLevel <= LogLevelEnum.ERROR) {
|
||||
this._log(this._optionsService.options.logger?.error.bind(this._optionsService.options.logger) ?? console.error, message, optionalParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
letterSpacing: 0,
|
||||
linkHandler: null,
|
||||
logLevel: 'info',
|
||||
logger: null,
|
||||
scrollback: 1000,
|
||||
scrollOnUserInput: true,
|
||||
scrollSensitivity: 1,
|
||||
@@ -43,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
rightClickSelectsWord: isMac,
|
||||
windowOptions: {},
|
||||
windowsMode: false,
|
||||
windowsPty: {},
|
||||
wordSeparator: ' ()[]{}\',"`',
|
||||
altClickMovesCursor: true,
|
||||
convertEol: false,
|
||||
@@ -177,12 +179,16 @@ export class OptionsService extends Disposable implements IOptionsService {
|
||||
if (value <= 0) {
|
||||
throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);
|
||||
}
|
||||
break;
|
||||
case 'rows':
|
||||
case 'cols':
|
||||
if (!value && value !== 0) {
|
||||
throw new Error(`${key} must be numeric, value: ${value}`);
|
||||
}
|
||||
break;
|
||||
case 'windowsPty':
|
||||
value = value ?? {};
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ 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, IColor, CursorStyle, IOscLinkData } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm';
|
||||
import { IDecorationOptions, IDecoration, ILinkHandler, IWindowsPty, ILogger } from 'xterm';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
export interface IBufferService {
|
||||
@@ -157,7 +157,7 @@ export const ILogService = createDecorator<ILogService>('LogService');
|
||||
export interface ILogService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
logLevel: LogLevelEnum;
|
||||
readonly logLevel: LogLevelEnum;
|
||||
|
||||
debug(message: any, ...optionalParams: any[]): void;
|
||||
info(message: any, ...optionalParams: any[]): void;
|
||||
@@ -229,6 +229,7 @@ export interface ITerminalOptions {
|
||||
lineHeight?: number;
|
||||
linkHandler?: ILinkHandler | null;
|
||||
logLevel?: LogLevel;
|
||||
logger?: ILogger | null;
|
||||
macOptionIsMeta?: boolean;
|
||||
macOptionClickForcesSelection?: boolean;
|
||||
minimumContrastRatio?: number;
|
||||
@@ -242,6 +243,7 @@ export interface ITerminalOptions {
|
||||
tabStopWidth?: number;
|
||||
theme?: ITheme;
|
||||
windowsMode?: boolean;
|
||||
windowsPty?: IWindowsPty;
|
||||
windowOptions?: IWindowOptions;
|
||||
wordSeparator?: string;
|
||||
overviewRulerWidth?: number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user