mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into master
This commit is contained in:
@@ -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 `<ng-terminal></ng-terminal>` 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)
|
||||
|
||||
|
||||
@@ -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<any> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
+21
-3
@@ -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'
|
||||
|
||||
+3
-1
@@ -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);
|
||||
@@ -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 = {
|
||||
'<none>': -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 = '<none>';
|
||||
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 <button + 32> <row + 32> <col + 32>
|
||||
report => ({
|
||||
state: evalButtonCode(report[3] - 32),
|
||||
col: report[4] - 32,
|
||||
row: report[5] - 32
|
||||
})
|
||||
],
|
||||
'UTF8' : [
|
||||
'\x1b[?1005h',
|
||||
// format: CSI M <button + 32> <row + 32> <col + 32>
|
||||
// + utf8 encoding on row/col
|
||||
report => {
|
||||
const sReport = report.toString(); // decode with utf8
|
||||
return {
|
||||
state: evalButtonCode(sReport.charCodeAt(3) - 32),
|
||||
col: sReport.charCodeAt(4) - 32,
|
||||
row: sReport.charCodeAt(5) - 32
|
||||
};
|
||||
}
|
||||
],
|
||||
'SGR' : [
|
||||
'\x1b[?1006h',
|
||||
// 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};
|
||||
}
|
||||
],
|
||||
'URXVT': [
|
||||
'\x1b[?1015h',
|
||||
// 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};
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function printMenu() {
|
||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log();
|
||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||
const protocols = Object.keys(PROTOCOLS);
|
||||
for (let i = 0; i < protocols.length; ++i) {
|
||||
console.log(` ${activeProtocol === i ? '->' : ' '} ${protocols[i]}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(' Selected encoding [Ctrl-B to switch]');
|
||||
const encs = Object.keys(ENC);
|
||||
for (let i = 0; i < encs.length; ++i) {
|
||||
console.log(` ${activeEnc === i ? '->' : ' '} ${encs[i]}`);
|
||||
}
|
||||
process.stdout.write('\x1b[100;H');
|
||||
}
|
||||
|
||||
function switchActiveProtocol() {
|
||||
activeProtocol++;
|
||||
activeProtocol %= Object.keys(PROTOCOLS).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function switchActiveEnc() {
|
||||
activeEnc++;
|
||||
activeEnc %= Object.keys(ENC).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function activate() {
|
||||
// clear all protocols and encodings
|
||||
process.stdout.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l');
|
||||
process.stdout.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l');
|
||||
// apply new protocol and encoding
|
||||
process.stdout.write(PROTOCOLS[Object.keys(PROTOCOLS)[activeProtocol]]);
|
||||
process.stdout.write(ENC[Object.keys(ENC)[activeEnc]][0]);
|
||||
console.log('\x1b[100;H\x1b[2A\x1b[2KReport:');
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
printMenu();
|
||||
activate();
|
||||
+5
-8
@@ -60,11 +60,12 @@ function setPadding(): void {
|
||||
term.fit();
|
||||
}
|
||||
|
||||
function getSearchOptions(): ISearchOptions {
|
||||
function getSearchOptions(e: KeyboardEvent): ISearchOptions {
|
||||
return {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
|
||||
incremental: e.key !== `Enter`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,15 +138,11 @@ function createTerminal(): void {
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
const searchOptions = getSearchOptions();
|
||||
searchOptions.incremental = e.key !== `Enter`;
|
||||
searchAddon.findNext(actionElements.findNext.value, searchOptions);
|
||||
searchAddon.findNext(actionElements.findNext.value, getSearchOptions(e));
|
||||
});
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
if (e.key === `Enter`) {
|
||||
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
}
|
||||
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
|
||||
+11
-11
@@ -79,7 +79,7 @@ class DECRQSS implements IDcsHandler {
|
||||
return this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
default:
|
||||
// invalid: DCS 0 $ r Pt ST (xterm)
|
||||
this._logService.error('Unknown DCS $q %s', data);
|
||||
this._logService.debug('Unknown DCS $q %s', data);
|
||||
this._coreService.triggerDataEvent(`${C0.ESC}P0$r${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
@@ -144,16 +144,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* custom fallback handlers
|
||||
*/
|
||||
this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
|
||||
this._logService.error('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) });
|
||||
this._logService.debug('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) });
|
||||
});
|
||||
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
|
||||
this._logService.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
|
||||
this._logService.debug('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
|
||||
});
|
||||
this._parser.setExecuteHandlerFallback((code: number) => {
|
||||
this._logService.error('Unknown EXECUTE code: ', { code });
|
||||
this._logService.debug('Unknown EXECUTE code: ', { code });
|
||||
});
|
||||
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||
this._logService.error('Unknown OSC code: ', { identifier, data });
|
||||
this._logService.debug('Unknown OSC code: ', { identifier, data });
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1268,7 +1268,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// this.cursorBlink = true;
|
||||
break;
|
||||
case 66:
|
||||
this._logService.info('Serial port requested application keypad.');
|
||||
this._logService.debug('Serial port requested application keypad.');
|
||||
this._terminal.applicationKeypad = true;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
@@ -1296,7 +1296,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (this._selectionService) {
|
||||
this._selectionService.disable();
|
||||
}
|
||||
this._logService.info('Binding to mouse events.');
|
||||
this._logService.debug('Binding to mouse events.');
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
// focusin: ^[[I
|
||||
@@ -1471,7 +1471,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// this.cursorBlink = false;
|
||||
break;
|
||||
case 66:
|
||||
this._logService.info('Switching back to normal keypad.');
|
||||
this._logService.debug('Switching back to normal keypad.');
|
||||
this._terminal.applicationKeypad = false;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
@@ -1762,7 +1762,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
} else {
|
||||
this._logService.error('Unknown SGR attribute: %d.', p);
|
||||
this._logService.debug('Unknown SGR attribute: %d.', p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1976,7 +1976,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Enables the numeric keypad to send application sequences to the host.
|
||||
*/
|
||||
public keypadApplicationMode(): void {
|
||||
this._logService.info('Serial port requested application keypad.');
|
||||
this._logService.debug('Serial port requested application keypad.');
|
||||
this._terminal.applicationKeypad = true;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
@@ -1989,7 +1989,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Enables the keypad to send numeric characters to the host.
|
||||
*/
|
||||
public keypadNumericMode(): void {
|
||||
this._logService.info('Switching back to normal keypad.');
|
||||
this._logService.debug('Switching back to normal keypad.');
|
||||
this._terminal.applicationKeypad = false;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
|
||||
@@ -94,6 +94,7 @@ describe('Terminal', () => {
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
(<any>term).textarea = { value: '' };
|
||||
const evKeyDown = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
@@ -578,10 +579,11 @@ describe('Terminal', () => {
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 192;
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
});
|
||||
|
||||
it('should interefere with the alt + arrow keys', () => {
|
||||
it('should interfere with the alt + arrow keys', () => {
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 37;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
@@ -646,16 +648,18 @@ describe('Terminal', () => {
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.ctrlKey = true;
|
||||
evKeyDown.keyCode = 81;
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal(term.keyDown(evKeyPress), true);
|
||||
});
|
||||
|
||||
it('should interefere with the alt + ctrl + arrow keys', () => {
|
||||
it('should interfere with the alt + ctrl + arrow keys', () => {
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.ctrlKey = true;
|
||||
|
||||
evKeyDown.keyCode = 37;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
evKeyDown.keyCode = 39;
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
});
|
||||
|
||||
|
||||
+38
-12
@@ -38,7 +38,6 @@ import { SoundService } from 'browser/services/SoundService';
|
||||
import { MouseZoneManager } from 'browser/MouseZoneManager';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
|
||||
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
|
||||
import { DomRenderer } from './renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
@@ -179,6 +178,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
// Store if user went browsing history in scrollback
|
||||
private _userScrolling: boolean;
|
||||
|
||||
/**
|
||||
* Records whether the keydown event has already been handled and triggered a data event, if so
|
||||
* the keypress event should not trigger a data event but should still print to the textarea so
|
||||
* screen readers will announce it.
|
||||
*/
|
||||
private _keyDownHandled: boolean = false;
|
||||
|
||||
private _inputHandler: InputHandler;
|
||||
public linkifier: ILinkifier;
|
||||
public viewport: IViewport;
|
||||
@@ -246,26 +252,31 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._instantiationService.setService(IOptionsService, this.optionsService);
|
||||
this._bufferService = this._instantiationService.createInstance(BufferService);
|
||||
this._instantiationService.setService(IBufferService, this._bufferService);
|
||||
this._logService = this._instantiationService.createInstance(LogService);
|
||||
this._instantiationService.setService(ILogService, this._logService);
|
||||
this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
|
||||
this._instantiationService.setService(ICoreService, this._coreService);
|
||||
this._coreService.onData(e => this._onData.fire(e));
|
||||
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
|
||||
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
|
||||
this._logService = this._instantiationService.createInstance(LogService);
|
||||
this._instantiationService.setService(ILogService, this._logService);
|
||||
|
||||
this._setupOptionsListeners();
|
||||
this._setup();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
super.dispose();
|
||||
if (this._windowsMode) {
|
||||
this._windowsMode.dispose();
|
||||
this._windowsMode = undefined;
|
||||
}
|
||||
if (this._renderService) {
|
||||
this._renderService.dispose();
|
||||
}
|
||||
this._customKeyEventHandler = null;
|
||||
removeTerminalFromCache(this);
|
||||
this.write = () => {};
|
||||
if (this.element && this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
@@ -685,7 +696,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
|
||||
private _createRenderer(): IRenderer {
|
||||
switch (this.options.rendererType) {
|
||||
case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService);
|
||||
case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService);
|
||||
case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService);
|
||||
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
||||
}
|
||||
@@ -1515,6 +1526,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
* @param ev The keydown event to be handled.
|
||||
*/
|
||||
protected _keyDown(event: KeyboardEvent): boolean {
|
||||
this._keyDownHandled = false;
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
||||
return false;
|
||||
}
|
||||
@@ -1530,12 +1543,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
|
||||
this.updateCursorStyle(event);
|
||||
|
||||
// if (result.key === C0.DC3) { // XOFF
|
||||
// this._writeStopped = true;
|
||||
// } else if (result.key === C0.DC1) { // XON
|
||||
// this._writeStopped = false;
|
||||
// }
|
||||
|
||||
if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
|
||||
const scrollCount = this.rows - 1;
|
||||
this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
|
||||
@@ -1559,11 +1566,26 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
return true;
|
||||
}
|
||||
|
||||
// If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers
|
||||
// will announce deleted characters. This will not work 100% of the time but it should cover
|
||||
// most scenarios.
|
||||
if (result.key === C0.ETX || result.key === C0.CR) {
|
||||
this.textarea.value = '';
|
||||
}
|
||||
|
||||
this._onKey.fire({ key: result.key, domEvent: event });
|
||||
this.showCursor();
|
||||
this._coreService.triggerDataEvent(result.key, true);
|
||||
|
||||
return this.cancel(event, true);
|
||||
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
||||
// other listeners. When screen reader mode is enabled, this could cause issues if the event
|
||||
// is handled at a higher level, this is a compromise in order to echo keys to the screen
|
||||
// reader.
|
||||
if (!this.optionsService.options.screenReaderMode) {
|
||||
return this.cancel(event, true);
|
||||
}
|
||||
|
||||
this._keyDownHandled = true;
|
||||
}
|
||||
|
||||
private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean {
|
||||
@@ -1621,6 +1643,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
protected _keyPress(ev: KeyboardEvent): boolean {
|
||||
let key;
|
||||
|
||||
if (this._keyDownHandled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -24,6 +24,15 @@ export interface IColorSet {
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
export interface IPartialColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor?: IColor;
|
||||
cursorAccent?: IColor;
|
||||
selection?: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
export interface IViewport extends IDisposable {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(): void;
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderLayer } from './Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/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 { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { BaseCharAtlas } from './atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx: CanvasRenderingContext2D;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
@@ -26,7 +26,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
protected _charAtlas: BaseCharAtlas;
|
||||
protected _charAtlas: BaseCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||
@@ -46,7 +46,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
protected _colors: IColorSet,
|
||||
private _rendererId: number,
|
||||
protected readonly _bufferService: IBufferService,
|
||||
protected readonly _optionsService: IOptionsService
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
@@ -63,25 +66,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {}
|
||||
public onBlur(terminal: ITerminal): void {}
|
||||
public onFocus(terminal: ITerminal): void {}
|
||||
public onCursorMove(terminal: ITerminal): void {}
|
||||
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||
public onOptionsChanged(): void {}
|
||||
public onBlur(): void {}
|
||||
public onFocus(): void {}
|
||||
public onCursorMove(): void {}
|
||||
public onGridChanged(startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||
|
||||
public setColors(terminal: ITerminal, colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
public setColors(colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(colorSet);
|
||||
}
|
||||
|
||||
protected _setTransparency(terminal: ITerminal, alpha: boolean): void {
|
||||
protected _setTransparency(alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
@@ -96,24 +99,23 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
this._refreshCharAtlas(this._colors);
|
||||
this.onGridChanged(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
|
||||
private _refreshCharAtlas(colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
@@ -130,10 +132,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this._refreshCharAtlas(this._colors);
|
||||
}
|
||||
|
||||
public abstract reset(terminal: ITerminal): void;
|
||||
public abstract reset(): void;
|
||||
|
||||
/**
|
||||
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||
@@ -233,16 +235,15 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* Draws a truecolor character at the cell. The character will be clipped to
|
||||
* ensure that it fits with the cell, including the cell to the right if it's
|
||||
* a wide character. This uses the existing fillStyle on the context.
|
||||
* @param terminal The terminal.
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: ITerminal, cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._clipRow(terminal, y);
|
||||
this._clipRow(y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
@@ -252,7 +253,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
/**
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
* @param terminal The terminal.
|
||||
* @param chars The character or characters.
|
||||
* @param code The character code.
|
||||
* @param width The width of the characters.
|
||||
@@ -263,14 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected _drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void {
|
||||
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
||||
|
||||
// skip cache right away if we draw in RGB
|
||||
// Note: to avoid bad runtime JoinedCellData will be skipped
|
||||
// in the cache handler itself (atlasDidDraw == false) and
|
||||
// fall through to uncached later down below
|
||||
if (cell.isFgRGB() || cell.isBgRGB()) {
|
||||
this._drawUncachedChars(terminal, cell, x, y);
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
|
||||
}
|
||||
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
@@ -302,7 +302,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(terminal, cell, x, y);
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,16 +310,15 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* Draws one or more characters at one or more cells. The character(s) will be
|
||||
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||
* right if the last character is a wide character.
|
||||
* @param terminal The terminal.
|
||||
* @param chars The character.
|
||||
* @param width The width of the character.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void {
|
||||
private _drawUncachedChars(cell: ICellData, x: number, y: number): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, !!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.textBaseline = 'middle';
|
||||
|
||||
if (cell.isInverse()) {
|
||||
@@ -337,14 +336,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
this._clipRow(terminal, y);
|
||||
this._clipRow(y);
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (cell.isDim()) {
|
||||
@@ -360,29 +359,27 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param terminal The terminal.
|
||||
* @param y The row to clip.
|
||||
*/
|
||||
private _clipRow(terminal: ITerminal, y: number): void {
|
||||
private _clipRow(y: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
terminal.cols * this._scaledCellWidth,
|
||||
this._bufferService.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current font.
|
||||
* @param terminal The terminal.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
protected _getFont(isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,37 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ILinkifierAccessor } from '../Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent = null;
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||
super(container, 'link', zIndex, true, colors);
|
||||
terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
linkifier: ILinkifier,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = null;
|
||||
this._state = undefined;
|
||||
}
|
||||
|
||||
public reset(terminal: ITerminal): void {
|
||||
public reset(): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
|
||||
@@ -37,14 +45,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
|
||||
}
|
||||
this._clearCells(0, this._state.y2, this._state.x2, 1);
|
||||
this._state = null;
|
||||
this._state = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (is256Color(e.fg)) {
|
||||
} else if (e.fg && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
} else {
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
+34
-27
@@ -3,51 +3,58 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
columnSelectMode: boolean;
|
||||
ydisp: number;
|
||||
start?: [number, number];
|
||||
end?: [number, number];
|
||||
columnSelectMode?: boolean;
|
||||
ydisp?: number;
|
||||
}
|
||||
|
||||
export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state: ISelectionState;
|
||||
private _state!: ISelectionState;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'selection', zIndex, true, colors);
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
private _clearState(): void {
|
||||
this._state = {
|
||||
start: null,
|
||||
end: null,
|
||||
columnSelectMode: null,
|
||||
ydisp: null
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
columnSelectMode: undefined,
|
||||
ydisp: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
public reset(terminal: ITerminal): void {
|
||||
public reset(): void {
|
||||
if (this._state.start && this._state.end) {
|
||||
this._clearState();
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
// Selection has not changed
|
||||
if (!this._didStateChange(start, end, columnSelectMode, terminal.buffer.ydisp)) {
|
||||
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,13 +68,13 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.ydisp;
|
||||
const viewportEndRow = end[1] - terminal.buffer.ydisp;
|
||||
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,17 +88,17 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
||||
this._fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
|
||||
this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount);
|
||||
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
this._fillCells(0, viewportCappedEndRow, endCol, 1);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +107,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
this._state.start = [start[0], start[1]];
|
||||
this._state.end = [end[0], end[1]];
|
||||
this._state.columnSelectMode = columnSelectMode;
|
||||
this._state.ydisp = terminal.buffer.ydisp;
|
||||
this._state.ydisp = this._bufferService.buffer.ydisp;
|
||||
}
|
||||
|
||||
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean, ydisp: number): boolean {
|
||||
@@ -110,7 +117,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
ydisp !== this._state.ydisp;
|
||||
}
|
||||
|
||||
private _areCoordinatesEqual(coord1: [number, number], coord2: [number, number]): boolean {
|
||||
private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean {
|
||||
if (!coord1 || !coord2) {
|
||||
return false;
|
||||
}
|
||||
@@ -4,15 +4,15 @@
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -23,23 +23,32 @@ import { CellData } from 'common/buffer/CellData';
|
||||
|
||||
export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _state: GridCache<CharData>;
|
||||
private _characterWidth: number;
|
||||
private _characterFont: string;
|
||||
private _characterWidth: number = 0;
|
||||
private _characterFont: string = '';
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
characterJoinerRegistry: ICharacterJoinerRegistry,
|
||||
alpha: boolean,
|
||||
rendererId: number,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
|
||||
// Clear the character width cache if the font or width has changed
|
||||
const terminalFont = this._getFont(terminal, false, false);
|
||||
const terminalFont = this._getFont(false, false);
|
||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
this._characterFont = terminalFont;
|
||||
@@ -47,16 +56,15 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state.clear();
|
||||
this._state.resize(terminal.cols, terminal.rows);
|
||||
this._state.resize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
|
||||
public reset(terminal: ITerminal): void {
|
||||
public reset(): void {
|
||||
this._state.clear();
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
private _forEachCell(
|
||||
terminal: ITerminal,
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
@@ -67,11 +75,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
) => void
|
||||
): void {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const line = this._bufferService.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
for (let x = 0; x < this._bufferService.cols; x++) {
|
||||
line!.loadCell(x, this._workCell);
|
||||
let cell = this._workCell;
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
@@ -89,14 +97,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift();
|
||||
const 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
|
||||
|
||||
cell = new JoinedCellData(
|
||||
this._workCell,
|
||||
line.translateToString(true, range[0], range[1]),
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
range[1] - range[0]
|
||||
);
|
||||
|
||||
@@ -116,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// get removed, and `a` would not re-render because it thinks it's
|
||||
// already in the correct state.
|
||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
|
||||
if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
|
||||
// patch width to 2
|
||||
cell.content &= ~Content.WIDTH_MASK;
|
||||
cell.content |= 2 << Content.WIDTH_SHIFT;
|
||||
@@ -143,16 +151,16 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
|
||||
* same color together to reduce draw calls.
|
||||
*/
|
||||
private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
private _drawBackground(firstRow: number, lastRow: number): void {
|
||||
const ctx = this._ctx;
|
||||
const cols = terminal.cols;
|
||||
const cols = this._bufferService.cols;
|
||||
let startX: number = 0;
|
||||
let startY: number = 0;
|
||||
let prevFillStyle: string | null = null;
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (cell, x, y) => {
|
||||
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
@@ -176,15 +184,17 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// don't need to draw anything.
|
||||
startX = x;
|
||||
startY = y;
|
||||
} if (y !== startY) {
|
||||
}
|
||||
|
||||
if (y !== startY) {
|
||||
// our row changed, draw the previous row
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
|
||||
this._fillCells(startX, startY, cols - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
} else if (prevFillStyle !== nextFillStyle) {
|
||||
// our color changed, draw the previous characters in this row
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
|
||||
this._fillCells(startX, startY, x - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
@@ -202,12 +212,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
|
||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
return;
|
||||
}
|
||||
this._drawChars(terminal, cell, x, y);
|
||||
this._drawChars(cell, x, y);
|
||||
if (cell.isUnderline()) {
|
||||
this._ctx.save();
|
||||
|
||||
@@ -226,7 +236,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
@@ -239,7 +249,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
});
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
public onGridChanged(firstRow: number, lastRow: number): void {
|
||||
// Resize has not been called yet
|
||||
if (this._state.cache.length === 0) {
|
||||
return;
|
||||
@@ -249,13 +259,13 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._charAtlas.beginFrame();
|
||||
}
|
||||
|
||||
this._clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
|
||||
this._drawBackground(terminal, firstRow, lastRow);
|
||||
this._drawForeground(terminal, firstRow, lastRow);
|
||||
this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
|
||||
this._drawBackground(firstRow, lastRow);
|
||||
this._drawForeground(firstRow, lastRow);
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {
|
||||
this._setTransparency(terminal, terminal.options.allowTransparency);
|
||||
public onOptionsChanged(): void {
|
||||
this._setTransparency(this._optionsService.options.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
Vendored
+58
@@ -56,3 +56,61 @@ export interface ICharacterJoinerRegistry {
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(colorSet: IColorSet): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
resize(dim: IRenderDimensions): void;
|
||||
|
||||
/**
|
||||
* Clear the state of the render layer.
|
||||
*/
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IGlyphIdentifier } from './Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export abstract class BaseCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
+18
-20
@@ -3,19 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../../Types';
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import { BaseCharAtlas } from './BaseCharAtlas';
|
||||
import { DynamicCharAtlas } from './DynamicCharAtlas';
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas';
|
||||
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ITerminalOptions } from 'common/services/Services';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
atlas: BaseCharAtlas;
|
||||
config: ICharAtlasConfig;
|
||||
// N.B. This implementation potentially holds onto copies of the terminal forever, so
|
||||
// this may cause memory leaks.
|
||||
ownedBy: ITerminal[];
|
||||
ownedBy: number[];
|
||||
}
|
||||
|
||||
const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
@@ -23,26 +23,25 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireCharAtlas(
|
||||
terminal: ITerminal,
|
||||
options: ITerminalOptions,
|
||||
rendererId: number,
|
||||
colors: IColorSet,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number
|
||||
): BaseCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
// Check to see if the renderer already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(terminal);
|
||||
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.atlas;
|
||||
}
|
||||
// The configs differ, release the terminal from the entry
|
||||
// The configs differ, release the renderer from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
entry.atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
@@ -57,8 +56,8 @@ export function acquireCharAtlas(
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the terminal to the cache entry and return
|
||||
entry.ownedBy.push(terminal);
|
||||
// Add the renderer to the cache entry and return
|
||||
entry.ownedBy.push(rendererId);
|
||||
return entry.atlas;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +68,7 @@ export function acquireCharAtlas(
|
||||
newConfig
|
||||
),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
ownedBy: [rendererId]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.atlas;
|
||||
@@ -77,14 +76,13 @@ export function acquireCharAtlas(
|
||||
|
||||
/**
|
||||
* Removes a terminal reference from the cache, allowing its memory to be freed.
|
||||
* @param terminal The terminal to remove.
|
||||
*/
|
||||
export function removeTerminalFromCache(terminal: ITerminal): void {
|
||||
export function removeTerminalFromCache(rendererId: number): void {
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
|
||||
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
|
||||
if (index !== -1) {
|
||||
if (charAtlasCache[i].ownedBy.length === 1) {
|
||||
// Remove the cache entry if it's the only terminal
|
||||
// Remove the cache entry if it's the only renderer
|
||||
charAtlasCache[i].atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user