diff --git a/README.md b/README.md
index d01a0f33..2853696f 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `` into your component.
+- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts
new file mode 100644
index 00000000..f970fcc8
--- /dev/null
+++ b/addons/xterm-addon-search/src/SearchAddon.api.ts
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import * as puppeteer from 'puppeteer';
+import { assert } from 'chai';
+import { ITerminalOptions } from 'xterm';
+
+const APP = 'http://127.0.0.1:3000/test';
+
+let browser: puppeteer.Browser;
+let page: puppeteer.Page;
+const width = 800;
+const height = 600;
+
+describe('Search Tests', function (): void {
+ this.timeout(200000);
+
+ before(async function (): Promise {
+ browser = await puppeteer.launch({
+ headless: process.argv.indexOf('--headless') !== -1,
+ slowMo: 80,
+ args: [`--window-size=${width},${height}`]
+ });
+ page = (await browser.pages())[0];
+ await page.setViewport({ width, height });
+ await page.goto(APP);
+ await openTerminal();
+ await page.evaluate(`window.search = new SearchAddon();`);
+ await page.evaluate(`window.term.loadAddon(window.search);`);
+ });
+
+ after(() => {
+ browser.close();
+ });
+
+ beforeEach(async () => {
+ await page.evaluate(`window.term.reset()`);
+ });
+
+ it('Simple Search', async () => {
+ await writeSync('dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
+ assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
+ });
+
+ it('Scrolling Search', async () => {
+ let dataString = '';
+ for (let i = 0; i < 100; i++) {
+ if (i === 52) {
+ dataString += '$^1_3{}test$#';
+ }
+ dataString += makeData(50);
+ }
+ await writeSync(dataString);
+ assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
+ });
+ it ('Incremental Find Previous', async () => {
+ await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
+ await writeSync('package.json pack package.lock');
+ await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
+ let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
+ let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
+ // We look further ahead in the line to ensure that pack was selected from package.lock
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
+ await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
+ await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
+ // We have to reevaluate line because it should have switched starting rows at this point
+ line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
+ });
+ it ('Incremental Find Next', async () => {
+ await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
+ await writeSync('package.jsonc');
+ await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
+ let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
+ let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
+ // We look further ahead in the line to ensure that pack was selected from package.lock
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
+ await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
+ await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
+ // We have to reevaluate line because it should have switched starting rows at this point
+ line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
+ });
+ it ('Simple Regex', async () => {
+ await writeSync('abc123defABCD');
+ await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
+ await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
+ });
+});
+
+async function openTerminal(options: ITerminalOptions = {}): Promise {
+ await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
+ await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
+ if (options.rendererType === 'dom') {
+ await page.waitForSelector('.xterm-rows');
+ } else {
+ await page.waitForSelector('.xterm-text-layer');
+ }
+}
+
+async function writeSync(data: string): Promise {
+ await page.evaluate(`window.term.write('${data}');`);
+ while (true) {
+ if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
+ break;
+ }
+ }
+}
+
+function makeData(length: number): string {
+ let result = '';
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
+ }
+ return result;
+}
diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts
index 8fcfc690..e9f8a9b4 100644
--- a/addons/xterm-addon-search/src/SearchAddon.ts
+++ b/addons/xterm-addon-search/src/SearchAddon.ts
@@ -57,8 +57,8 @@ export class SearchAddon implements ITerminalAddon {
return false;
}
- let startCol: number = 0;
- let startRow = this._terminal.buffer.viewportY;
+ let startCol = 0;
+ let startRow = 0;
if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false;
@@ -71,20 +71,8 @@ export class SearchAddon implements ITerminalAddon {
this._initLinesCache();
- // A row that has isWrapped = false
- let findingRow = startRow;
- // index of beginning column that _findInLine need to scan.
- let cumulativeCols = startCol;
- // If startRow is wrapped row, scan for unwrapped row above.
- // So we can start matching on wrapped line from long unwrapped line.
- let currentLine = this._terminal.buffer.getLine(findingRow);
- while (currentLine && currentLine.isWrapped) {
- cumulativeCols += this._terminal.cols;
- currentLine = this._terminal.buffer.getLine(--findingRow);
- }
-
// Search startRow
- let result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
+ let result = this._findInLine(term, startRow, startCol, searchOptions);
// Search from startRow + 1 to end
if (!result) {
@@ -99,11 +87,9 @@ export class SearchAddon implements ITerminalAddon {
}
}
}
-
- // Search from the top to the startRow (search the whole startRow again in
- // case startCol > 0)
- if (!result) {
- for (let y = 0; y < findingRow; y++) {
+ // If we hit the bottom and didn't search from the very top wrap back up
+ if (!result && startRow !== 0) {
+ for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
@@ -133,61 +119,45 @@ export class SearchAddon implements ITerminalAddon {
}
const isReverseSearch = true;
- let startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1;
+ let startRow = this._terminal.buffer.baseY + this._terminal.rows;
let startCol = this._terminal.cols;
-
+ let result: ISearchResult | undefined = undefined;
+ const incremental = searchOptions ? searchOptions.incremental : false;
if (this._terminal.hasSelection()) {
- // Start from the selection start if there is a selection
const currentSelection = this._terminal.getSelectionPosition()!;
+ // Start from selection start if there is a selection
startRow = currentSelection.startRow;
startCol = currentSelection.startColumn;
}
this._initLinesCache();
- // Search startRow
- let result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
+ if (incremental) {
+ result = this._findInLine(term, startRow, startCol, searchOptions, false);
+ if (!(result && result.row === startRow && result.col === startCol)) {
+ result = this._findInLine(term, startRow, startCol, searchOptions, true);
+ }
+ } else {
+ result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
+ }
// Search from startRow - 1 to top
if (!result) {
- // If the line is wrapped line, increase number of columns that is needed to be scanned
- // Se we can scan on wrapped line from unwrapped line
- let cumulativeCols = this._terminal.cols;
- if (this._terminal.buffer.getLine(startRow)!.isWrapped) {
- cumulativeCols += startCol;
- }
+ startCol = this._terminal.cols;
for (let y = startRow - 1; y >= 0; y--) {
- result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
+ result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch);
if (result) {
break;
}
- // If the current line is wrapped line, increase scanning range,
- // preparing for scanning on unwrapped line
- const line = this._terminal.buffer.getLine(y);
- if (line && line.isWrapped) {
- cumulativeCols += this._terminal.cols;
- } else {
- cumulativeCols = this._terminal.cols;
- }
}
}
-
- // Search from the bottom to startRow (search the whole startRow again in
- // case startCol > 0)
- if (!result) {
- const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;
- let cumulativeCols = this._terminal.cols;
- for (let y = searchFrom; y >= startRow; y--) {
- result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
+ // If we hit the top and didn't search from the very bottom wrap back down
+ if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) {
+ for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) {
+ result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch);
if (result) {
break;
}
- const line = this._terminal.buffer.getLine(y);
- if (line && line.isWrapped) {
- cumulativeCols += this._terminal.cols;
- } else {
- cumulativeCols = this._terminal.cols;
- }
}
}
diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
index df0cdbab..0e2d645e 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
@@ -4,16 +4,12 @@
*/
import { IRenderLayer } from './Types';
-import { ICellData } from 'common/Types';
-import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IGlyphIdentifier } from '../atlas/Types';
-import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { CellData } from 'common/buffer/CellData';
-import { AttributeData } from 'common/buffer/AttributeData';
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
import { throwIfFalsy } from '../WebglUtils';
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index e8b23d34..c784a611 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -62,7 +62,7 @@ jobs:
yarn lint
displayName: 'Lint'
-- job: IntegrationTests
+- job: Linux_IntegrationTests
pool:
vmImage: 'ubuntu-16.04'
steps:
@@ -81,14 +81,32 @@ jobs:
yarn start &
sleep 10
yarn test-api --headless
- displayName: 'Integration tests'
+ displayName: 'Linux Integration tests'
+
+- job: macOS_IntegrationTests
+ pool:
+ vmImage: 'xcode9-macos10.13'
+ steps:
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '8.x'
+ displayName: 'Install Node.js'
+ - script: |
+ yarn
+ displayName: 'Install dependencies and build'
+ - script: |
+ yarn start &
+ sleep 10
+ yarn test-api --headless
+ displayName: 'MacOS Integration tests'
- job: Release
dependsOn:
- Linux
- macOS
- Windows
- - IntegrationTests
+ - Linux_IntegrationTests
+ - macOS_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*')))
pool:
vmImage: 'ubuntu-16.04'
diff --git a/bin/test.js b/bin/test.js
index 2d816097..7beb4f9a 100644
--- a/bin/test.js
+++ b/bin/test.js
@@ -22,7 +22,7 @@ if (process.argv.length > 2) {
testFiles = process.argv.slice(2);
}
-cp.spawnSync(
+const run = cp.spawnSync(
path.resolve(__dirname, '../node_modules/.bin/mocha'),
testFiles,
{
@@ -31,3 +31,5 @@ cp.spawnSync(
stdio: 'inherit'
}
);
+
+process.exit(run.status);
\ No newline at end of file
diff --git a/bin/test_mousemodes.js b/bin/test_mousemodes.js
new file mode 100644
index 00000000..976a38e0
--- /dev/null
+++ b/bin/test_mousemodes.js
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ *
+ * 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).
+ *
+ * VT200 Highlight mode (DECSET 1001) is not implemented.
+ *
+ * The test basically applies the report data to the cursor, thus
+ * a mouse report should move the cursor to the cell under the mouse.
+ * Furthermore the reports are printed in the left lower corner as
+ * raw data and their meaning.
+ *
+ * A failing test might show:
+ * - wrong coords: the cursor will jump to some other place
+ * - wrong buttons: see meaning output and check whether it makes sense
+ * - faulty reports: inspect the raw data and compare with other emulators
+ * - missing events: compare with spec / other emulators
+ */
+
+let activeProtocol = 0;
+let activeEnc = 0;
+
+const stdin = process.openStdin();
+process.stdin.setRawMode(true);
+
+// close handler - reset terminal on exit
+stdin.addListener('data', function(data) {
+ if (data[0] === 0x03) {
+ process.stdin.setRawMode(false);
+ process.stdout.write('\x1bc');
+ process.exit();
+ }
+ if (data[0] === 0x01) {
+ switchActiveProtocol();
+ printMenu();
+ }
+ if (data[0] === 0x02) {
+ switchActiveEnc();
+ printMenu();
+ }
+ console.log('\x1b[100;H\x1b[2A\x1b[2KReport:', data, [data.toString('binary')]);
+ // filter mouse reports
+ if (data[0] === 0x1b && data[1] === '['.charCodeAt(0)) {
+ applyReportData(data);
+ }
+});
+
+// button definitions
+const buttons = {
+ '': -1,
+ left: 0,
+ middle: 1,
+ right: 2,
+ released: 3,
+ wheelUp: 4,
+ wheelDown: 5,
+ wheelLeft: 6,
+ wheelRight: 7,
+ aux1: 8,
+ aux2: 9,
+ aux3: 10,
+ aux4: 11,
+ aux5: 12,
+ aux6: 13,
+ aux7: 14,
+ aux8: 15
+};
+const reverseButtons = {};
+for (const el in buttons) {
+ reverseButtons[buttons[el]] = el;
+}
+
+// extract button data from buttonCode
+function evalButtonCode(code) {
+ // more than 15 buttons are not supported
+ if (code > 255) {
+ return {button: 'invalid', action: 'invalid', modifier: {}};
+ }
+ const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)};
+ const move = code & 32;
+ let button = code & 3;
+ if (code & 128) {
+ button |= 8;
+ }
+ if (code & 64) {
+ button |= 4
+ }
+ let actionS = 'press';
+ let buttonS = reverseButtons[button];
+ if (button === 3) {
+ buttonS = '';
+ actionS = 'release';
+ }
+ if (move) {
+ actionS = 'move';
+ } else if (4 <= button && button <= 7) {
+ buttonS = 'wheel';
+ actionS = button === 4 ? 'up' : button === 5 ? 'down' : button === 6 ? 'left' : 'right';
+ }
+ return {button: buttonS, action: actionS, modifier};
+}
+
+// protocols
+const PROTOCOLS = {
+ '9 (X10: press only)': '\x1b[?9h',
+ '1000 (VT200: press, release, wheel)': '\x1b[?1000h',
+ // '1001 (VT200 highlight)': '\x1b[?1001h', // handle of backreport - not implemented
+ '1002 (press, release, move on pressed, wheel)': '\x1b[?1002h',
+ '1003 (press, relase, move, wheel)': '\x1b[?1003h'
+}
+
+// encodings: ENCODING_NAME => [sequence, parse_report]
+const ENC = {
+ 'DEFAULT' : [
+ '',
+ // format: CSI M