Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d04125ef | ||
|
|
d936a81c1f | ||
|
|
14a065e7c0 | ||
|
|
3ad45b72b6 | ||
|
|
925cdaef67 | ||
|
|
fa2a32e431 |
@@ -1,22 +0,0 @@
|
||||
FROM node:10
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install --no-install-recommends apt-utils 2>&1
|
||||
|
||||
# Verify git and process tools are installed
|
||||
RUN apt-get install -y git procps
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get install -y curl apt-transport-https lsb-release \
|
||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends yarn
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"dockerFile": "Dockerfile",
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,6 @@ lib/test/
|
||||
docs/
|
||||
/.idea/
|
||||
.vscode/
|
||||
bin/
|
||||
build/
|
||||
fixtures/
|
||||
coverage/
|
||||
|
||||
Vendored
+5
-6
@@ -23,7 +23,10 @@
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Demo Client",
|
||||
"url": "http://127.0.0.1:3000",
|
||||
"url": "http://0.0.0.0:3000",
|
||||
"windows": {
|
||||
"url": "http://127.0.0.1:3000"
|
||||
},
|
||||
"webRoot": "${workspaceFolder}/"
|
||||
},
|
||||
{
|
||||
@@ -35,11 +38,7 @@
|
||||
"run",
|
||||
"start-debug"
|
||||
],
|
||||
"port": 9229,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "App listening to (http://.*?:[0-9]+)"
|
||||
}
|
||||
"port": 9229
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
}
|
||||
Vendored
-51
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"group": "test",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": ["npm: watch", "npm: start"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -136,7 +136,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
|
||||
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
|
||||
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
|
||||
- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web.
|
||||
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
|
||||
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS.
|
||||
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
|
||||
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux.
|
||||
@@ -154,10 +154,6 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
|
||||
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
|
||||
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
|
||||
- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD
|
||||
- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access.
|
||||
- [**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.
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
@@ -169,16 +165,6 @@ Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
|
||||
|
||||
```
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||
|
||||
@@ -66,28 +66,3 @@ jobs:
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
|
||||
- job: Release
|
||||
dependsOn:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: "1.9.4"
|
||||
displayName: 'Install Yarn'
|
||||
- script: |
|
||||
yarn
|
||||
BUILD_DIR=dist npm run build
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
|
||||
displayName: 'Publish to npm'
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// Setup auth
|
||||
fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
|
||||
|
||||
// Determine if this is a stable or beta release
|
||||
const publishedVersions = getPublishedVersions();
|
||||
const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
|
||||
|
||||
// Get the next version
|
||||
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
|
||||
console.log(`Publishing version: ${nextVersion}`);
|
||||
|
||||
// Set the version in package.json
|
||||
const packageJsonFile = path.resolve(__dirname, '..', 'package.json');
|
||||
packageJson.version = nextVersion;
|
||||
fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Publish
|
||||
const args = ['publish'];
|
||||
if (!isStableRelease) {
|
||||
args.push('--tag', 'beta');
|
||||
}
|
||||
const result = cp.spawn('npm', args, { stdio: 'inherit' });
|
||||
result.on('exit', code => process.exit(code));
|
||||
|
||||
function getNextBetaVersion() {
|
||||
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
|
||||
console.error('The package.json version must be of the form x.y.z');
|
||||
process.exit(1);
|
||||
}
|
||||
const tag = 'beta';
|
||||
const stableVersion = packageJson.version.split('.');
|
||||
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.${stableVersion[2]}`;
|
||||
const publishedVersions = getPublishedVersions(nextStableVersion, tag);
|
||||
if (publishedVersions.length === 0) {
|
||||
return `${packageJson.version}-${tag}1`;
|
||||
}
|
||||
const latestPublishedVersion = publishedVersions.sort((a, b) => {
|
||||
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
|
||||
const bVersion = parseInt(b.substr(b.search(/[0-9]+$/)));
|
||||
return aVersion > bVersion ? -1 : 1;
|
||||
})[0];
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
|
||||
return `${nextStableVersion}-${tag}${latestTagVersion + 1}`;
|
||||
}
|
||||
|
||||
function getPublishedVersions(version, tag) {
|
||||
const versionsProcess = cp.spawnSync('npm', ['view', 'xterm', 'versions', '--json']);
|
||||
const versionsJson = JSON.parse(versionsProcess.stdout);
|
||||
if (tag) {
|
||||
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
|
||||
}
|
||||
return versionsJson;
|
||||
}
|
||||
+15
-13
@@ -13,11 +13,12 @@ import * as fit from '../lib/addons/fit/fit';
|
||||
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||
import * as search from '../lib/addons/search/search';
|
||||
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||
import { ISearchOptions } from '../lib/addons/search/Interfaces';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
|
||||
import { Terminal as TerminalType } from 'xterm';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: TerminalType;
|
||||
@@ -29,6 +30,7 @@ Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
let term;
|
||||
@@ -81,12 +83,9 @@ function createTerminal(): void {
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
windowsMode: isWindows
|
||||
} as ITerminalOptions);
|
||||
term = new Terminal({});
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.onResize((size: { cols: number, rows: number }) => {
|
||||
term.on('resize', (size: { cols: number, rows: number }) => {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +99,7 @@ function createTerminal(): void {
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
@@ -165,21 +164,24 @@ function runFakeTerminal(): void {
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
||||
const ev = e.domEvent;
|
||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
|
||||
term._core.register(term.addDisposableListener('key', (key, ev) => {
|
||||
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Do not delete the prompt
|
||||
if (term._core.buffer.x > 2) {
|
||||
if (term.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(e.key);
|
||||
term.write(key);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
term._core.register(term.addDisposableListener('paste', (data, ev) => {
|
||||
term.write(data);
|
||||
}));
|
||||
}
|
||||
|
||||
function initOptions(term: TerminalType): void {
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="/src/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ function startServer() {
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.use('/src', express.static(__dirname + '/../src'));
|
||||
app.use('/build', express.static(__dirname + '/../build'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
@@ -99,7 +99,7 @@ function startServer() {
|
||||
var port = process.env.PORT || 3000,
|
||||
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
|
||||
|
||||
console.log('App listening to http://127.0.0.1:' + port);
|
||||
console.log('App listening to http://' + host + ':' + port);
|
||||
app.listen(port, host);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var expressWs = require('express-ws')(app);
|
||||
var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.use('/build', express.static(__dirname + '/../../build'));
|
||||
app.use('/demo', express.static(__dirname + '/../../demo'));
|
||||
app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '../style.css');
|
||||
});
|
||||
|
||||
app.get('/main.js', function(req, res){
|
||||
res.sendFile(__dirname + '/main.js');
|
||||
});
|
||||
|
||||
app.post('/terminals', function (req, res) {
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
encoding: null,
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd: process.env.PWD,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.post('/terminals/:pid/size', function (req, res) {
|
||||
var pid = parseInt(req.params.pid),
|
||||
cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = terminals[pid];
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (ex) {
|
||||
// The WebSocket is not open, ignore
|
||||
}
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
|
||||
var port = process.env.PORT || 3000,
|
||||
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
|
||||
|
||||
console.log('App listening to http://' + host + ':' + port);
|
||||
app.listen(port, host);
|
||||
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="/demo/style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
<script src="/build/xterm.js" ></script>
|
||||
|
||||
<script src="/build/addons/attach/attach.js" ></script>
|
||||
<script src="/zmodemjs/zmodem.js"></script>
|
||||
<script src="/build/addons/zmodem/zmodem.js" ></script>
|
||||
|
||||
<script src="/build/addons/fit/fit.js" ></script>
|
||||
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
|
||||
<script src="/build/addons/search/search.js" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>xterm.js: xterm, in the browser</h1>
|
||||
|
||||
<div id="terminal-container"></div>
|
||||
|
||||
<div id="zmodem_controls">
|
||||
<form id="zm_start" style="display: none" action="javascript:void(0)">
|
||||
ZMODEM detected: Start ZMODEM session?
|
||||
<label><input id="zmstart_yes" name="zmstart" type=radio checked value="1"> Yes</label>
|
||||
|
||||
<label><input name="zmstart" type=radio value=""> No</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="zm_offer" style="display: none" action="javascript:void(0)">
|
||||
<p>ZMODEM File offered!</p>
|
||||
|
||||
<label><input id="zmaccept_yes" name="zmaccept" type=radio checked value="1"> Accept</label>
|
||||
|
||||
<label><input name="zmaccept" type=radio value=""> Skip</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<div id="zm_file" style="display: none">
|
||||
<div>Name: <span id="name"></span></div>
|
||||
<div>Size: <span id="size"></span></div>
|
||||
<div>Last modified: <span id="mtime"></span></div>
|
||||
<div>Mode: <span id="mode"></span></div>
|
||||
<br>
|
||||
<div>Conversion: <span id="zfile_conversion"></span></div>
|
||||
<div>Management: <span id="zfile_management"></span></div>
|
||||
<div>Transport: <span id="zfile_transport"></span></div>
|
||||
<div>Sparse? <span id="zfile_sparse"></span></div>
|
||||
<br>
|
||||
<div>Files remaining in batch: <span id="files_remaining"></span></div>
|
||||
<div>Bytes remaining in batch: <span id="bytes_remaining"></span></div>
|
||||
</div>
|
||||
|
||||
<form id="zm_progress" style="display: none" action="javascript:void(0)">
|
||||
<div><span id="percent_received"></span>% (<span id="bytes_received"></span> bytes) received</div>
|
||||
<button id="zm_progress_skipper" type="button" onclick="skip_current_file();">Skip File</button>
|
||||
</form>
|
||||
|
||||
<form id="zm_choose" style="display: none" action="javascript:void(0)">
|
||||
<label>Choose file(s): <input id="zm_files" type="file" multiple></label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" checked id="zmodem-auto"> Accept all ZMODEM prompts<sup>*</sup></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
cursorStyle
|
||||
<select id="option-cursor-style">
|
||||
<option value="block">block</option>
|
||||
<option value="underline">underline</option>
|
||||
<option value="bar">bar</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
bellStyle
|
||||
<select id="option-bell-style">
|
||||
<option value="">none</option>
|
||||
<option value="sound">sound</option>
|
||||
<option value="visual">visual</option>
|
||||
<option value="both">both</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
|
||||
</p>
|
||||
<div>
|
||||
<h3>Size</h3>
|
||||
<div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="cols">Columns</label>
|
||||
<input type="number" id="cols" />
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="rows">Rows</label>
|
||||
<input type="number" id="rows" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||
<p><sup>*</sup> ZMODEM file transfers are supported via an addon. To try it out, install <a href="https://ohse.de/uwe/software/lrzsz.html"><code>lrzsz</code></a> onto the remote peer, then run <code>rz</code> to send from your browser or <code>sz <file></code> to send from the remote peer.</p>
|
||||
<script src="main.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,388 @@
|
||||
"use strict";
|
||||
|
||||
var term,
|
||||
protocol,
|
||||
socketURL,
|
||||
socket,
|
||||
pid;
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(zmodem);
|
||||
Terminal.applyAddon(search);
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
findNext: document.querySelector('#find-next'),
|
||||
findPrevious: document.querySelector('#find-previous')
|
||||
},
|
||||
optionElements = {
|
||||
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||
scrollback: document.querySelector('#option-scrollback'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth'),
|
||||
bellStyle: document.querySelector('#option-bell-style')
|
||||
},
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows');
|
||||
|
||||
function setTerminalSize() {
|
||||
var cols = parseInt(colsElement.value, 10);
|
||||
var rows = parseInt(rowsElement.value, 10);
|
||||
var viewportElement = document.querySelector('.xterm-viewport');
|
||||
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
|
||||
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
|
||||
var height = (rows * term.charMeasure.height).toString() + 'px';
|
||||
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
|
||||
colsElement.addEventListener('change', setTerminalSize);
|
||||
rowsElement.addEventListener('change', setTerminalSize);
|
||||
|
||||
actionElements.findNext.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findNext(actionElements.findNext.value);
|
||||
}
|
||||
});
|
||||
actionElements.findPrevious.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findPrevious(actionElements.findPrevious.value);
|
||||
}
|
||||
});
|
||||
|
||||
optionElements.cursorBlink.addEventListener('change', function () {
|
||||
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
|
||||
});
|
||||
optionElements.cursorStyle.addEventListener('change', function () {
|
||||
term.setOption('cursorStyle', optionElements.cursorStyle.value);
|
||||
});
|
||||
optionElements.bellStyle.addEventListener('change', function () {
|
||||
term.setOption('bellStyle', optionElements.bellStyle.value);
|
||||
});
|
||||
optionElements.scrollback.addEventListener('change', function () {
|
||||
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
|
||||
});
|
||||
optionElements.tabstopwidth.addEventListener('change', function () {
|
||||
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
|
||||
});
|
||||
|
||||
createTerminal();
|
||||
|
||||
function createTerminal() {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({
|
||||
cursorBlink: optionElements.cursorBlink.checked,
|
||||
scrollback: parseInt(optionElements.scrollback.value, 10),
|
||||
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
|
||||
});
|
||||
term.on('resize', function (size) {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
var cols = size.cols,
|
||||
rows = size.rows,
|
||||
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.fit();
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
|
||||
term.zmodemAttach(socket, {
|
||||
noTerminalWriteOutsideSession: true,
|
||||
} );
|
||||
|
||||
term.on("zmodemRetract", () => {
|
||||
start_form.style.display = "none";
|
||||
start_form.onsubmit = null;
|
||||
});
|
||||
|
||||
term.on("zmodemDetect", (detection) => {
|
||||
function do_zmodem() {
|
||||
term.detach();
|
||||
let zsession = detection.confirm();
|
||||
|
||||
var promise;
|
||||
|
||||
if (zsession.type === "receive") {
|
||||
promise = _handle_receive_session(zsession);
|
||||
}
|
||||
else {
|
||||
promise = _handle_send_session(zsession);
|
||||
}
|
||||
|
||||
promise.catch( console.error.bind(console) ).then( () => {
|
||||
term.attach(socket);
|
||||
} );
|
||||
}
|
||||
|
||||
if (_auto_zmodem()) {
|
||||
do_zmodem();
|
||||
}
|
||||
else {
|
||||
start_form.style.display = "";
|
||||
start_form.onsubmit = function(e) {
|
||||
start_form.style.display = "none";
|
||||
|
||||
if (document.getElementById("zmstart_yes").checked) {
|
||||
do_zmodem();
|
||||
}
|
||||
else {
|
||||
detection.deny();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// UI STUFF
|
||||
|
||||
function _show_file_info(xfer) {
|
||||
var file_info = xfer.get_details();
|
||||
|
||||
document.getElementById("name").textContent = file_info.name;
|
||||
document.getElementById("size").textContent = file_info.size;
|
||||
document.getElementById("mtime").textContent = file_info.mtime;
|
||||
document.getElementById("files_remaining").textContent = file_info.files_remaining;
|
||||
document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining;
|
||||
|
||||
document.getElementById("mode").textContent = "0" + file_info.mode.toString(8);
|
||||
|
||||
var xfer_opts = xfer.get_options();
|
||||
["conversion", "management", "transport", "sparse"].forEach( (lbl) => {
|
||||
document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl];
|
||||
} );
|
||||
|
||||
document.getElementById("zm_file").style.display = "";
|
||||
}
|
||||
function _hide_file_info() {
|
||||
document.getElementById("zm_file").style.display = "none";
|
||||
}
|
||||
|
||||
function _save_to_disk(xfer, buffer) {
|
||||
return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name);
|
||||
}
|
||||
|
||||
var skipper_button = document.getElementById("zm_progress_skipper");
|
||||
var skipper_button_orig_text = skipper_button.textContent;
|
||||
|
||||
function _show_progress() {
|
||||
skipper_button.disabled = false;
|
||||
skipper_button.textContent = skipper_button_orig_text;
|
||||
|
||||
document.getElementById("bytes_received").textContent = 0;
|
||||
document.getElementById("percent_received").textContent = 0;
|
||||
|
||||
document.getElementById("zm_progress").style.display = "";
|
||||
}
|
||||
|
||||
function _update_progress(xfer) {
|
||||
var total_in = xfer.get_offset();
|
||||
|
||||
document.getElementById("bytes_received").textContent = total_in;
|
||||
|
||||
var percent_received = 100 * total_in / xfer.get_details().size;
|
||||
document.getElementById("percent_received").textContent = percent_received.toFixed(2);
|
||||
}
|
||||
|
||||
function _hide_progress() {
|
||||
document.getElementById("zm_progress").style.display = "none";
|
||||
}
|
||||
|
||||
var start_form = document.getElementById("zm_start");
|
||||
|
||||
function _auto_zmodem() {
|
||||
return document.getElementById("zmodem-auto").checked;
|
||||
}
|
||||
|
||||
// END UI STUFF
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
function _handle_receive_session(zsession) {
|
||||
zsession.on("offer", function(xfer) {
|
||||
current_receive_xfer = xfer;
|
||||
|
||||
_show_file_info(xfer);
|
||||
|
||||
var offer_form = document.getElementById("zm_offer");
|
||||
|
||||
function on_form_submit() {
|
||||
offer_form.style.display = "none";
|
||||
|
||||
//START
|
||||
//if (offer_form.zmaccept.value) {
|
||||
if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) {
|
||||
_show_progress();
|
||||
|
||||
var FILE_BUFFER = [];
|
||||
xfer.on("input", (payload) => {
|
||||
_update_progress(xfer);
|
||||
FILE_BUFFER.push( new Uint8Array(payload) );
|
||||
});
|
||||
xfer.accept().then(
|
||||
() => {
|
||||
_save_to_disk(xfer, FILE_BUFFER);
|
||||
},
|
||||
console.error.bind(console)
|
||||
);
|
||||
}
|
||||
else {
|
||||
xfer.skip();
|
||||
}
|
||||
//END
|
||||
}
|
||||
|
||||
if (_auto_zmodem()) {
|
||||
on_form_submit();
|
||||
}
|
||||
else {
|
||||
offer_form.onsubmit = on_form_submit;
|
||||
offer_form.style.display = "";
|
||||
}
|
||||
} );
|
||||
|
||||
var promise = new Promise( (res) => {
|
||||
zsession.on("session_end", () => {
|
||||
_hide_file_info();
|
||||
_hide_progress();
|
||||
res();
|
||||
} );
|
||||
} );
|
||||
|
||||
zsession.start();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function _handle_send_session(zsession) {
|
||||
var choose_form = document.getElementById("zm_choose");
|
||||
choose_form.style.display = "";
|
||||
|
||||
var file_el = document.getElementById("zm_files");
|
||||
|
||||
var promise = new Promise( (res) => {
|
||||
file_el.onchange = function(e) {
|
||||
choose_form.style.display = "none";
|
||||
|
||||
var files_obj = file_el.files;
|
||||
|
||||
Zmodem.Browser.send_files(
|
||||
zsession,
|
||||
files_obj,
|
||||
{
|
||||
on_offer_response(obj, xfer) {
|
||||
if (xfer) _show_progress();
|
||||
//console.log("offer", xfer ? "accepted" : "skipped");
|
||||
},
|
||||
on_progress(obj, xfer) {
|
||||
_update_progress(xfer);
|
||||
},
|
||||
on_file_complete(obj) {
|
||||
//console.log("COMPLETE", obj);
|
||||
_hide_progress();
|
||||
},
|
||||
}
|
||||
).then(_hide_progress).then(
|
||||
zsession.close.bind(zsession),
|
||||
console.error.bind(console)
|
||||
).then( () => {
|
||||
_hide_file_info();
|
||||
_hide_progress();
|
||||
res();
|
||||
} );
|
||||
};
|
||||
} );
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
//This is here to allow canceling of an in-progress ZMODEM transfer.
|
||||
var current_receive_xfer;
|
||||
|
||||
//Called from HTML directly.
|
||||
function skip_current_file() {
|
||||
current_receive_xfer.skip();
|
||||
|
||||
skipper_button.disabled = true;
|
||||
skipper_button.textContent = "Waiting for server to acknowledge skip …";
|
||||
}
|
||||
|
||||
function runRealTerminal() {
|
||||
term.attach(socket);
|
||||
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
function runFakeTerminal() {
|
||||
if (term._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
term._initialized = true;
|
||||
|
||||
var shellprompt = '$ ';
|
||||
|
||||
term.prompt = function () {
|
||||
term.write('\r\n' + shellprompt);
|
||||
};
|
||||
|
||||
term.writeln('Welcome to xterm.js');
|
||||
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term.writeln('Type some keys and commands to play around.');
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term.on('key', function (key, ev) {
|
||||
var printable = (
|
||||
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
|
||||
);
|
||||
|
||||
if (ev.keyCode == 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode == 8) {
|
||||
// Do not delete the prompt
|
||||
if (term.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
}
|
||||
});
|
||||
|
||||
term.on('paste', function (data, ev) {
|
||||
term.write(data);
|
||||
});
|
||||
}
|
||||
+5
-5
@@ -16,9 +16,9 @@ const ts = require('gulp-typescript');
|
||||
const util = require('gulp-util');
|
||||
|
||||
const buildDir = process.env.BUILD_DIR || 'build';
|
||||
const tsProject = ts.createProject('src/tsconfig.json');
|
||||
let srcDir = './src';
|
||||
let outDir = './lib';
|
||||
const tsProject = ts.createProject('tsconfig.json');
|
||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let outDir = tsProject.config.compilerOptions.outDir;
|
||||
|
||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||
|
||||
@@ -61,7 +61,7 @@ gulp.task('browserify', function() {
|
||||
};
|
||||
let bundleStream = browserify(browserifyOptions)
|
||||
.bundle()
|
||||
.pipe(source(`xterm.js`))
|
||||
.pipe(source('xterm.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
@@ -136,6 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
})
|
||||
});
|
||||
|
||||
gulp.task('build', ['css', 'sorcery', 'sorcery-addons']);
|
||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
||||
gulp.task('test', ['mocha']);
|
||||
gulp.task('default', ['build']);
|
||||
|
||||
+9
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.13.2",
|
||||
"version": "3.12.2",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@types/webpack": "^4.4.11",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
"concurrently": "^3.5.1",
|
||||
"coveralls": "^3.0.1",
|
||||
"express": "4.13.4",
|
||||
"express-ws": "2.0.0-rc.1",
|
||||
@@ -37,7 +38,7 @@
|
||||
"ts-loader": "^4.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "3.4",
|
||||
"typescript": "3.1",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
@@ -49,16 +50,20 @@
|
||||
"start-debug": "node --inspect-brk demo/start",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||
"pretest": "npm run layering",
|
||||
"test": "npm run mocha",
|
||||
"posttest": "npm run lint",
|
||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||
"test-suite": "gulp mocha-suite --test",
|
||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||
"mocha": "gulp test",
|
||||
"prebuild": "tsc -b ./src/tsconfig.all.json",
|
||||
"tsc": "tsc",
|
||||
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
|
||||
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
|
||||
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"",
|
||||
"layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\""
|
||||
}
|
||||
}
|
||||
|
||||
+12
-14
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './common/Platform';
|
||||
import { isMac } from './core/Platform';
|
||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
@@ -61,7 +61,7 @@ export class AccessibilityManager extends Disposable {
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
@@ -72,20 +72,20 @@ export class AccessibilityManager extends Disposable {
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
|
||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
|
||||
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
|
||||
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
|
||||
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._onKey(e.key)));
|
||||
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
|
||||
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
|
||||
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
|
||||
// should be listened to instead? That would mean that the order of events are always
|
||||
// guarenteed
|
||||
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
||||
this.register(this._terminal.renderer.onCanvasResize(() => this._refreshRowsDimensions()));
|
||||
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the dprchange event isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
@@ -239,7 +239,7 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
|
||||
this._renderRowsDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
@@ -249,11 +249,9 @@ export class AccessibilityManager extends Disposable {
|
||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||
const element = this._rowElements[i];
|
||||
if (element) {
|
||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-39
@@ -5,10 +5,10 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer, DEFAULT_ATTR_DATA } from './Buffer';
|
||||
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { MockTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { BufferLine, CellData } from './BufferLine';
|
||||
import { MockTerminal, TestTerminal } from './ui/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -37,13 +37,13 @@ describe('Buffer', () => {
|
||||
|
||||
describe('fillViewportRows', () => {
|
||||
it('should fill the buffer with blank lines based on the size of the viewport', () => {
|
||||
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData();
|
||||
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0);
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let y = 0; y < INIT_ROWS; y++) {
|
||||
assert.equal(buffer.lines.get(y).length, INIT_COLS);
|
||||
for (let x = 0; x < INIT_COLS; x++) {
|
||||
assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData(), blankLineChar);
|
||||
assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -155,15 +155,15 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
buffer.fillViewportRows();
|
||||
let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData();
|
||||
let chData = buffer.lines.get(5).get(0);
|
||||
chData[1] = 'a';
|
||||
buffer.lines.get(5).setCell(0, CellData.fromCharData(chData));
|
||||
chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData();
|
||||
buffer.lines.get(5).set(0, chData);
|
||||
chData = buffer.lines.get(INIT_ROWS - 1).get(0);
|
||||
chData[1] = 'b';
|
||||
buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData));
|
||||
buffer.lines.get(INIT_ROWS - 1).set(0, chData);
|
||||
buffer.resize(INIT_COLS, INIT_ROWS - 5);
|
||||
assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b');
|
||||
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -184,7 +184,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -204,7 +204,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -732,7 +732,7 @@ describe('Buffer', () => {
|
||||
beforeEach(() => {
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
@@ -791,7 +791,7 @@ describe('Buffer', () => {
|
||||
terminal.options.scrollback = 10;
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.y = 9;
|
||||
buffer.ybase = 10;
|
||||
@@ -926,7 +926,7 @@ describe('Buffer', () => {
|
||||
beforeEach(() => {
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
@@ -989,7 +989,7 @@ describe('Buffer', () => {
|
||||
terminal.options.scrollback = 10;
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
@@ -1074,7 +1074,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 1);
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 2);
|
||||
});
|
||||
it('should dispose of a marker if it is trimmed off the buffer', () => {
|
||||
@@ -1085,7 +1085,7 @@ describe('Buffer', () => {
|
||||
const marker = buffer.addMarker(0);
|
||||
assert.equal(marker.isDisposed, false);
|
||||
assert.equal(buffer.markers.length, 1);
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.isDisposed, true);
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
});
|
||||
@@ -1094,10 +1094,10 @@ describe('Buffer', () => {
|
||||
describe ('translateBufferLineToString', () => {
|
||||
it('should handle selecting a section of ascii text', () => {
|
||||
const line = new BufferLine(4);
|
||||
line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)]));
|
||||
line.set(0, [ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [ null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [ null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [ null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
@@ -1106,9 +1106,9 @@ describe('Buffer', () => {
|
||||
|
||||
it('should handle a cut-off double width character by including it', () => {
|
||||
const line = new BufferLine(3);
|
||||
line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ]));
|
||||
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
|
||||
line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.set(0, [ null, '語', 2, 35486 ]);
|
||||
line.set(1, [ null, '', 0, null]);
|
||||
line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
@@ -1117,9 +1117,9 @@ describe('Buffer', () => {
|
||||
|
||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||
const line = new BufferLine(3);
|
||||
line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ]));
|
||||
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
|
||||
line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.set(0, [ null, '語', 2, '語'.charCodeAt(0) ]);
|
||||
line.set(1, [ null, '', 0, null]);
|
||||
line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
@@ -1134,8 +1134,8 @@ describe('Buffer', () => {
|
||||
|
||||
it('should handle single width emojis', () => {
|
||||
const line = new BufferLine(2);
|
||||
line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ]));
|
||||
line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
@@ -1147,8 +1147,8 @@ describe('Buffer', () => {
|
||||
|
||||
it('should handle double width emojis', () => {
|
||||
const line = new BufferLine(2);
|
||||
line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ]));
|
||||
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
|
||||
line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line.set(1, [ null, '', 0, null]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
@@ -1158,9 +1158,9 @@ describe('Buffer', () => {
|
||||
assert.equal(str2, '😁');
|
||||
|
||||
const line2 = new BufferLine(3);
|
||||
line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ]));
|
||||
line2.setCell(1, CellData.fromCharData([ null, '', 0, null]));
|
||||
line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line2.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line2.set(1, [ null, '', 0, null]);
|
||||
line2.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line2);
|
||||
|
||||
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||
@@ -1313,7 +1313,7 @@ describe('Buffer', () => {
|
||||
assert.equal(input, s);
|
||||
const stringIndex = s.match(/😃/).index;
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
|
||||
assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
|
||||
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
|
||||
});
|
||||
|
||||
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
|
||||
@@ -1340,7 +1340,7 @@ describe('Buffer', () => {
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
|
||||
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1358,7 +1358,7 @@ describe('Buffer', () => {
|
||||
: (i % 3 === 1)
|
||||
? input.substr(i, 2)
|
||||
: input.substr(i - 1, 2),
|
||||
terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
|
||||
terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+30
-72
@@ -3,43 +3,31 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CircularList, IInsertEvent } from './common/CircularList';
|
||||
import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types';
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine, CellData, AttributeData } from './BufferLine';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow';
|
||||
import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { DEFAULT_COLOR } from './renderer/atlas/Types';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { Disposable } from '../lib/common/Lifecycle';
|
||||
import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types';
|
||||
|
||||
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
|
||||
|
||||
export const DEFAULT_ATTR_DATA = new AttributeData();
|
||||
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
/**
|
||||
* Null cell - a real empty cell (containing nothing).
|
||||
* Note that code should always be 0 for a null cell as
|
||||
* several test condition of the buffer line rely on this.
|
||||
*/
|
||||
export const NULL_CELL_CHAR = '';
|
||||
export const NULL_CELL_WIDTH = 1;
|
||||
export const NULL_CELL_CODE = 0;
|
||||
|
||||
/**
|
||||
* Whitespace cell.
|
||||
* This is meant as a replacement for empty cells when needed
|
||||
* during rendering lines to preserve correct aligment.
|
||||
*/
|
||||
export const WHITESPACE_CELL_CHAR = ' ';
|
||||
export const WHITESPACE_CELL_WIDTH = 1;
|
||||
export const WHITESPACE_CELL_CODE = 32;
|
||||
|
||||
export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
* following information is stored (in high-level):
|
||||
@@ -58,10 +46,8 @@ export class Buffer implements IBuffer {
|
||||
public tabs: any;
|
||||
public savedY: number;
|
||||
public savedX: number;
|
||||
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
|
||||
public savedCurAttr: number;
|
||||
public markers: Marker[] = [];
|
||||
private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
|
||||
private _cols: number;
|
||||
private _rows: number;
|
||||
|
||||
@@ -80,30 +66,9 @@ export class Buffer implements IBuffer {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
if (attr) {
|
||||
this._nullCell.fg = attr.fg;
|
||||
this._nullCell.bg = attr.bg;
|
||||
} else {
|
||||
this._nullCell.fg = 0;
|
||||
this._nullCell.bg = 0;
|
||||
}
|
||||
return this._nullCell;
|
||||
}
|
||||
|
||||
public getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
if (attr) {
|
||||
this._whitespaceCell.fg = attr.fg;
|
||||
this._whitespaceCell.bg = attr.bg;
|
||||
} else {
|
||||
this._whitespaceCell.fg = 0;
|
||||
this._whitespaceCell.bg = 0;
|
||||
}
|
||||
return this._whitespaceCell;
|
||||
}
|
||||
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped);
|
||||
public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
|
||||
const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
return new BufferLine(this._cols, fillCharData, isWrapped);
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
@@ -134,10 +99,10 @@ export class Buffer implements IBuffer {
|
||||
/**
|
||||
* Fills the buffer's viewport with blank lines.
|
||||
*/
|
||||
public fillViewportRows(fillAttr?: IAttributeData): void {
|
||||
public fillViewportRows(fillAttr?: number): void {
|
||||
if (this.lines.length === 0) {
|
||||
if (fillAttr === undefined) {
|
||||
fillAttr = DEFAULT_ATTR_DATA;
|
||||
fillAttr = DEFAULT_ATTR;
|
||||
}
|
||||
let i = this._rows;
|
||||
while (i--) {
|
||||
@@ -166,9 +131,6 @@ export class Buffer implements IBuffer {
|
||||
* @param newRows The new number of rows.
|
||||
*/
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
// store reference to null cell with default attrs
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
|
||||
// Increase max length if needed before adjustments to allow space to fill
|
||||
// as required.
|
||||
const newMaxLength = this._getCorrectBufferLength(newRows);
|
||||
@@ -182,7 +144,7 @@ export class Buffer implements IBuffer {
|
||||
// Deal with columns increasing (reducing needs to happen after reflow)
|
||||
if (this._cols < newCols) {
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
this.lines.get(i).resize(newCols, nullCell);
|
||||
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +165,7 @@ export class Buffer implements IBuffer {
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,7 +217,7 @@ export class Buffer implements IBuffer {
|
||||
// Trim the end of the line off if cols shrunk
|
||||
if (this._cols > newCols) {
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
this.lines.get(i).resize(newCols, nullCell);
|
||||
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +227,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private get _isReflowEnabled(): boolean {
|
||||
return this._hasScrollback && !this._terminal.options.windowsMode;
|
||||
return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled;
|
||||
}
|
||||
|
||||
private _reflow(newCols: number, newRows: number): void {
|
||||
@@ -282,7 +244,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private _reflowLarger(newCols: number, newRows: number): void {
|
||||
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
|
||||
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y);
|
||||
if (toRemove.length > 0) {
|
||||
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
|
||||
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
|
||||
@@ -291,7 +253,6 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
// Adjust viewport based on number of items removed
|
||||
let viewportAdjustments = countRemoved;
|
||||
while (viewportAdjustments-- > 0) {
|
||||
@@ -301,7 +262,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
if (this.lines.length < newRows) {
|
||||
// Add an extra row at the bottom of the viewport
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
|
||||
}
|
||||
} else {
|
||||
if (this.ydisp === this.ybase) {
|
||||
@@ -313,7 +274,6 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private _reflowSmaller(newCols: number, newRows: number): void {
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
||||
// batched up and only committed once
|
||||
const toInsert = [];
|
||||
@@ -354,7 +314,7 @@ export class Buffer implements IBuffer {
|
||||
// Add the new lines
|
||||
const newLines: BufferLine[] = [];
|
||||
for (let i = 0; i < linesToAdd; i++) {
|
||||
const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;
|
||||
const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine;
|
||||
newLines.push(newLine);
|
||||
}
|
||||
if (newLines.length > 0) {
|
||||
@@ -396,7 +356,7 @@ export class Buffer implements IBuffer {
|
||||
// Null out the end of the line ends if a wide character wrapped to the following line
|
||||
for (let i = 0; i < wrappedLines.length; i++) {
|
||||
if (destLineLengths[i] < newCols) {
|
||||
wrappedLines[i].setCell(destLineLengths[i], nullCell);
|
||||
wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +415,7 @@ export class Buffer implements IBuffer {
|
||||
insertEvents.push({
|
||||
index: originalLineIndex + 1,
|
||||
amount: nextToInsert.newLines.length
|
||||
});
|
||||
} as IInsertEvent);
|
||||
|
||||
countInsertedSoFar += nextToInsert.newLines.length;
|
||||
nextToInsert = toInsert[++nextToInsertIndex];
|
||||
@@ -468,12 +428,12 @@ export class Buffer implements IBuffer {
|
||||
let insertCountEmitted = 0;
|
||||
for (let i = insertEvents.length - 1; i >= 0; i--) {
|
||||
insertEvents[i].index += insertCountEmitted;
|
||||
this.lines.onInsertEmitter.fire(insertEvents[i]);
|
||||
this.lines.emit('insert', insertEvents[i]);
|
||||
insertCountEmitted += insertEvents[i].amount;
|
||||
}
|
||||
const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);
|
||||
if (amountToTrim > 0) {
|
||||
this.lines.onTrimEmitter.fire(amountToTrim);
|
||||
this.lines.emitMayRemoveListeners('trim', amountToTrim);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,19 +553,19 @@ export class Buffer implements IBuffer {
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
marker.register(this.lines.onTrim(amount => {
|
||||
marker.register(this.lines.addDisposableListener('trim', amount => {
|
||||
marker.line -= amount;
|
||||
// The marker should be disposed when the line is trimmed from the buffer
|
||||
if (marker.line < 0) {
|
||||
marker.dispose();
|
||||
}
|
||||
}));
|
||||
marker.register(this.lines.onInsert(event => {
|
||||
marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => {
|
||||
if (marker.line >= event.index) {
|
||||
marker.line += event.amount;
|
||||
}
|
||||
}));
|
||||
marker.register(this.lines.onDelete(event => {
|
||||
marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => {
|
||||
// Delete the marker if it's within the range
|
||||
if (marker.line >= event.index && marker.line < event.index + event.amount) {
|
||||
marker.dispose();
|
||||
@@ -616,7 +576,7 @@ export class Buffer implements IBuffer {
|
||||
marker.line -= event.amount;
|
||||
}
|
||||
}));
|
||||
marker.register(marker.onDispose(() => this._removeMarker(marker)));
|
||||
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
|
||||
return marker;
|
||||
}
|
||||
|
||||
@@ -629,7 +589,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
export class Marker extends Disposable implements IMarker {
|
||||
export class Marker extends EventEmitter implements IMarker {
|
||||
private static _nextId = 1;
|
||||
|
||||
private _id: number = Marker._nextId++;
|
||||
@@ -637,9 +597,6 @@ export class Marker extends Disposable implements IMarker {
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
private _onDispose = new EventEmitter2<void>();
|
||||
public get onDispose(): IEvent<void> { return this._onDispose.event; }
|
||||
|
||||
constructor(
|
||||
public line: number
|
||||
) {
|
||||
@@ -652,7 +609,8 @@ export class Marker extends Disposable implements IMarker {
|
||||
}
|
||||
this.isDisposed = true;
|
||||
// Emit before super.dispose such that dispose listeners get a change to react
|
||||
this._onDispose.fire();
|
||||
this.emit('dispose');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+99
-163
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import { BufferLine, CellData, Content } from './BufferLine';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
|
||||
|
||||
@@ -16,38 +16,12 @@ class TestBufferLine extends BufferLine {
|
||||
public toArray(): CharData[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
result.push(this.loadCell(i, new CellData()).getAsCharData());
|
||||
result.push(this.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('CellData', () => {
|
||||
it('CharData <--> CellData equality', () => {
|
||||
const cell = new CellData();
|
||||
// ASCII
|
||||
cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
// combining
|
||||
cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
// surrogate
|
||||
cell.setFromCharData([123, '𝄞', 1, 0x1D11E]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
// surrogate + combining
|
||||
cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
// wide char
|
||||
cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BufferLine', function(): void {
|
||||
it('ctor', function(): void {
|
||||
let line: IBufferLine = new TestBufferLine(0);
|
||||
@@ -55,23 +29,23 @@ describe('BufferLine', function(): void {
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10, null, true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
|
||||
line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
});
|
||||
it('insertCells', function(): void {
|
||||
const line = new TestBufferLine(3);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
@@ -80,12 +54,12 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
it('deleteCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
@@ -96,12 +70,12 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
it('replaceCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[2, 'b', 0, 'b'.charCodeAt(0)],
|
||||
@@ -112,12 +86,12 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
it('fill', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.fill([123, 'z', 0, 'z'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
@@ -128,11 +102,11 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
it('clone', function(): void {
|
||||
const line = new TestBufferLine(5, null, true);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
const line2 = line.clone();
|
||||
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
@@ -140,12 +114,12 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
it('copyFrom', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
@@ -155,9 +129,9 @@ describe('BufferLine', function(): void {
|
||||
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
|
||||
// --> set code to the last charCodeAt value of the string
|
||||
// Note: needs to be fixed once the string pointer is in place
|
||||
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
|
||||
const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
const line3 = line.clone();
|
||||
@@ -165,81 +139,81 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
describe('resize', function(): void {
|
||||
it('enlarge(false)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge(true)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(true) - should apply new size', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink to 0 length', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
|
||||
chai.expect(Object.keys(line.combined).length).eql(2);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString()).eql('aa😁aa');
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
|
||||
chai.expect(Object.keys(line.combined).length).eql(1);
|
||||
});
|
||||
});
|
||||
describe('getTrimLength', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
chai.expect(line.getTrimmedLength()).equal(0);
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([0, '', 0, undefined]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(3, [0, '', 0, undefined]);
|
||||
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
|
||||
});
|
||||
});
|
||||
describe('translateToString with and w\'o trimming', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
chai.expect(line.translateToString(false)).equal(' ');
|
||||
chai.expect(line.translateToString(true)).equal('');
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
|
||||
@@ -251,11 +225,11 @@ describe('BufferLine', function(): void {
|
||||
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
|
||||
@@ -266,11 +240,11 @@ describe('BufferLine', function(): void {
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
|
||||
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
@@ -281,14 +255,14 @@ describe('BufferLine', function(): void {
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([0, '', 0, undefined]));
|
||||
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(6, CellData.fromCharData([0, '', 0, undefined]));
|
||||
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(8, CellData.fromCharData([0, '', 0, undefined]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(3, [0, '', 0, undefined]);
|
||||
line.set(5, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(6, [0, '', 0, undefined]);
|
||||
line.set(7, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(8, [0, '', 0, undefined]);
|
||||
chai.expect(line.translateToString(false)).equal('a 1 11 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 1 11');
|
||||
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
|
||||
@@ -305,12 +279,12 @@ describe('BufferLine', function(): void {
|
||||
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
|
||||
});
|
||||
it('space at end', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa ');
|
||||
});
|
||||
@@ -318,52 +292,14 @@ describe('BufferLine', function(): void {
|
||||
// sanity check - broken line with invalid out of bound null width cells
|
||||
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
|
||||
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
|
||||
chai.expect(line.translateToString(false)).equal(' ');
|
||||
chai.expect(line.translateToString(true)).equal('');
|
||||
});
|
||||
it('should work with endCol=0', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(true, 0, 0)).equal('');
|
||||
});
|
||||
});
|
||||
describe('addCharToCell', () => {
|
||||
it('should set width to 1 for empty cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
const cell = line.loadCell(0, new CellData());
|
||||
// chars contains single combining char
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
});
|
||||
it('should add char to combining string in cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const cell = line .loadCell(0, new CellData());
|
||||
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
|
||||
line.setCell(0, cell);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
line.loadCell(0, cell);
|
||||
// chars contains 3 chars
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
});
|
||||
it('should create combining string on taken cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const cell = line .loadCell(0, new CellData());
|
||||
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
|
||||
line.setCell(0, cell);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
line.loadCell(0, cell);
|
||||
// chars contains 2 chars
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+49
-457
@@ -2,521 +2,112 @@
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
import { stringFromCodePoint } from './core/input/TextDecoder';
|
||||
|
||||
|
||||
/**
|
||||
* buffer memory layout:
|
||||
*
|
||||
* | uint32_t | uint32_t | uint32_t |
|
||||
* | `content` | `FG` | `BG` |
|
||||
* | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
|
||||
*/
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
|
||||
|
||||
/** typed array slots taken by one cell */
|
||||
const CELL_SIZE = 3;
|
||||
|
||||
/**
|
||||
* Cell member indices.
|
||||
*
|
||||
* Direct access:
|
||||
* `content = data[column * CELL_SIZE + Cell.CONTENT];`
|
||||
* `fg = data[column * CELL_SIZE + Cell.FG];`
|
||||
* `bg = data[column * CELL_SIZE + Cell.BG];`
|
||||
*/
|
||||
/** cell member indices */
|
||||
const enum Cell {
|
||||
CONTENT = 0,
|
||||
FG = 1, // currently simply holds all known attrs
|
||||
BG = 2 // currently unused
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitmasks for accessing data in `content`.
|
||||
*/
|
||||
export const enum Content {
|
||||
/**
|
||||
* bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken)
|
||||
* read: `codepoint = content & Content.codepointMask;`
|
||||
* write: `content |= codepoint & Content.codepointMask;`
|
||||
* shortcut if precondition `codepoint <= 0x10FFFF` is met:
|
||||
* `content |= codepoint;`
|
||||
*/
|
||||
CODEPOINT_MASK = 0x1FFFFF,
|
||||
|
||||
/**
|
||||
* bit 22 flag indication whether a cell contains combined content
|
||||
* read: `isCombined = content & Content.isCombined;`
|
||||
* set: `content |= Content.isCombined;`
|
||||
* clear: `content &= ~Content.isCombined;`
|
||||
*/
|
||||
IS_COMBINED_MASK = 0x200000, // 1 << 21
|
||||
|
||||
/**
|
||||
* bit 1..22 mask to check whether a cell contains any string data
|
||||
* we need to check for codepoint and isCombined bits to see
|
||||
* whether a cell contains anything
|
||||
* read: `isEmpty = !(content & Content.hasContent)`
|
||||
*/
|
||||
HAS_CONTENT_MASK = 0x3FFFFF,
|
||||
|
||||
/**
|
||||
* bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2)
|
||||
* read: `width = (content & Content.widthMask) >> Content.widthShift;`
|
||||
* `hasWidth = content & Content.widthMask;`
|
||||
* as long as wcwidth is highest value in `content`:
|
||||
* `width = content >> Content.widthShift;`
|
||||
* write: `content |= (width << Content.widthShift) & Content.widthMask;`
|
||||
* shortcut if precondition `0 <= width <= 3` is met:
|
||||
* `content |= width << Content.widthShift;`
|
||||
*/
|
||||
WIDTH_MASK = 0xC00000, // 3 << 22
|
||||
WIDTH_SHIFT = 22
|
||||
}
|
||||
|
||||
|
||||
export const enum Attributes {
|
||||
/**
|
||||
* bit 1..8 blue in RGB, color in P256 and P16
|
||||
*/
|
||||
BLUE_MASK = 0xFF,
|
||||
BLUE_SHIFT = 0,
|
||||
PCOLOR_MASK = 0xFF,
|
||||
PCOLOR_SHIFT = 0,
|
||||
|
||||
/**
|
||||
* bit 9..16 green in RGB
|
||||
*/
|
||||
GREEN_MASK = 0xFF00,
|
||||
GREEN_SHIFT = 8,
|
||||
|
||||
/**
|
||||
* bit 17..24 red in RGB
|
||||
*/
|
||||
RED_MASK = 0xFF0000,
|
||||
RED_SHIFT = 16,
|
||||
|
||||
/**
|
||||
* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)
|
||||
*/
|
||||
CM_MASK = 0x3000000,
|
||||
CM_DEFAULT = 0,
|
||||
CM_P16 = 0x1000000,
|
||||
CM_P256 = 0x2000000,
|
||||
CM_RGB = 0x3000000,
|
||||
|
||||
/**
|
||||
* bit 1..24 RGB room
|
||||
*/
|
||||
RGB_MASK = 0xFFFFFF
|
||||
}
|
||||
|
||||
export const enum FgFlags {
|
||||
/**
|
||||
* bit 27..31 (32th bit unused)
|
||||
*/
|
||||
INVERSE = 0x4000000,
|
||||
BOLD = 0x8000000,
|
||||
UNDERLINE = 0x10000000,
|
||||
BLINK = 0x20000000,
|
||||
INVISIBLE = 0x40000000
|
||||
}
|
||||
|
||||
export const enum BgFlags {
|
||||
/**
|
||||
* bit 27..32 (upper 4 unused)
|
||||
*/
|
||||
ITALIC = 0x4000000,
|
||||
DIM = 0x8000000
|
||||
}
|
||||
|
||||
export class AttributeData implements IAttributeData {
|
||||
static toColorRGB(value: number): IColorRGB {
|
||||
return [
|
||||
value >>> Attributes.RED_SHIFT & 255,
|
||||
value >>> Attributes.GREEN_SHIFT & 255,
|
||||
value & 255
|
||||
];
|
||||
}
|
||||
static fromColorRGB(value: IColorRGB): number {
|
||||
return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;
|
||||
}
|
||||
|
||||
public clone(): IAttributeData {
|
||||
const newObj = new AttributeData();
|
||||
newObj.fg = this.fg;
|
||||
newObj.bg = this.bg;
|
||||
return newObj;
|
||||
}
|
||||
|
||||
// data
|
||||
public fg: number = 0;
|
||||
public bg: number = 0;
|
||||
|
||||
// flags
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
public isBold(): number { return this.fg & FgFlags.BOLD; }
|
||||
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
|
||||
public isBlink(): number { return this.fg & FgFlags.BLINK; }
|
||||
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
|
||||
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
|
||||
public isDim(): number { return this.bg & BgFlags.DIM; }
|
||||
|
||||
// color modes
|
||||
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
|
||||
public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }
|
||||
public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }
|
||||
public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }
|
||||
public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }
|
||||
public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }
|
||||
public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }
|
||||
public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }
|
||||
|
||||
// colors
|
||||
public getFgColor(): number {
|
||||
switch (this.fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;
|
||||
case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;
|
||||
default: return -1; // CM_DEFAULT defaults to -1
|
||||
}
|
||||
}
|
||||
public getBgColor(): number {
|
||||
switch (this.bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;
|
||||
case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;
|
||||
default: return -1; // CM_DEFAULT defaults to -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CellData - represents a single Cell in the terminal buffer.
|
||||
*/
|
||||
export class CellData extends AttributeData implements ICellData {
|
||||
|
||||
/** Helper to create CellData from CharData. */
|
||||
public static fromCharData(value: CharData): CellData {
|
||||
const obj = new CellData();
|
||||
obj.setFromCharData(value);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** Primitives from terminal buffer. */
|
||||
public content: number = 0;
|
||||
public fg: number = 0;
|
||||
public bg: number = 0;
|
||||
public combinedData: string = '';
|
||||
|
||||
/** Whether cell contains a combined string. */
|
||||
public isCombined(): number {
|
||||
return this.content & Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
/** Width of the cell. */
|
||||
public getWidth(): number {
|
||||
return this.content >> Content.WIDTH_SHIFT;
|
||||
}
|
||||
|
||||
/** JS string of the content. */
|
||||
public getChars(): string {
|
||||
if (this.content & Content.IS_COMBINED_MASK) {
|
||||
return this.combinedData;
|
||||
}
|
||||
if (this.content & Content.CODEPOINT_MASK) {
|
||||
return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Codepoint of cell
|
||||
* Note this returns the UTF32 codepoint of single chars,
|
||||
* if content is a combined string it returns the codepoint
|
||||
* of the last char in string to be in line with code in CharData.
|
||||
* */
|
||||
public getCode(): number {
|
||||
return (this.isCombined())
|
||||
? this.combinedData.charCodeAt(this.combinedData.length - 1)
|
||||
: this.content & Content.CODEPOINT_MASK;
|
||||
}
|
||||
|
||||
/** Set data from CharData */
|
||||
public setFromCharData(value: CharData): void {
|
||||
this.fg = value[CHAR_DATA_ATTR_INDEX];
|
||||
this.bg = 0;
|
||||
let combined = false;
|
||||
|
||||
// surrogates and combined strings need special treatment
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
|
||||
combined = true;
|
||||
} else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
|
||||
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
|
||||
// if the 2-char string is a surrogate create single codepoint
|
||||
// everything else is combined
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
|
||||
if (0xDC00 <= second && second <= 0xDFFF) {
|
||||
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
} else {
|
||||
combined = true;
|
||||
}
|
||||
} else {
|
||||
combined = true;
|
||||
}
|
||||
} else {
|
||||
this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
if (combined) {
|
||||
this.combinedData = value[CHAR_DATA_CHAR_INDEX];
|
||||
this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get data as CharData. */
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
FLAGS = 0,
|
||||
STRING = 1,
|
||||
WIDTH = 2
|
||||
}
|
||||
|
||||
/** single vs. combined char distinction */
|
||||
const IS_COMBINED_BIT_MASK = 0x80000000;
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*
|
||||
* There are 2 ways to insert data into the cell buffer:
|
||||
* - `setCellFromCodepoint` + `addCodepointToCell`
|
||||
* Use these for data that is already UTF32.
|
||||
* Used during normal input in `InputHandler` for faster buffer access.
|
||||
* - `setCell`
|
||||
* This method takes a CellData object and stores the data in the buffer.
|
||||
* Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).
|
||||
*
|
||||
* To retrieve data from the buffer use either one of the primitive methods
|
||||
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
|
||||
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
|
||||
*/
|
||||
export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array | null = null;
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
|
||||
constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
|
||||
if (!fillCharData) {
|
||||
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
if (cols) {
|
||||
this._data = new Uint32Array(cols * CELL_SIZE);
|
||||
const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
for (let i = 0; i < cols; ++i) {
|
||||
this.setCell(i, cell);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
this.length = cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cell data CharData.
|
||||
* @deprecated
|
||||
*/
|
||||
public get(index: number): CharData {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
|
||||
return [
|
||||
this._data[index * CELL_SIZE + Cell.FG],
|
||||
(content & Content.IS_COMBINED_MASK)
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS],
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
? this._combined[index]
|
||||
: (cp) ? stringFromCodePoint(cp) : '',
|
||||
content >> Content.WIDTH_SHIFT,
|
||||
(content & Content.IS_COMBINED_MASK)
|
||||
: (stringData) ? String.fromCharCode(stringData) : '',
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH],
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
? this._combined[index].charCodeAt(this._combined[index].length - 1)
|
||||
: cp
|
||||
: stringData
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell data from CharData.
|
||||
* @deprecated
|
||||
*/
|
||||
public set(index: number, value: CharData): void {
|
||||
this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
} else {
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* primitive getters
|
||||
* use these when only one value is needed, otherwise use `loadCell`
|
||||
*/
|
||||
public getWidth(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
|
||||
return this._data[index * CELL_SIZE + Cell.WIDTH];
|
||||
}
|
||||
|
||||
/** Test whether content has width. */
|
||||
public hasWidth(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
|
||||
}
|
||||
|
||||
/** Get FG cell component. */
|
||||
public getFg(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.FG];
|
||||
}
|
||||
|
||||
/** Get BG cell component. */
|
||||
public getBg(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.BG];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether contains any chars.
|
||||
* Basically an empty has no content, but other cells might differ in FG/BG
|
||||
* from real empty cells.
|
||||
* */
|
||||
public hasContent(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get codepoint of the cell.
|
||||
* To be in line with `code` in CharData this either returns
|
||||
* a single UTF32 codepoint or the last codepoint of a combined string.
|
||||
*/
|
||||
public getCodePoint(index: number): number {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index].charCodeAt(this._combined[index].length - 1);
|
||||
}
|
||||
return content & Content.CODEPOINT_MASK;
|
||||
}
|
||||
|
||||
/** Test whether the cell contains a combined string. */
|
||||
public isCombined(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
/** Returns the string content of the cell. */
|
||||
public getString(index: number): string {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index];
|
||||
}
|
||||
if (content & Content.CODEPOINT_MASK) {
|
||||
return stringFromCodePoint(content & Content.CODEPOINT_MASK);
|
||||
}
|
||||
// return empty string for empty cells
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data at `index` into `cell`. This is used to access cells in a way that's more friendly
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
const startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[startIndex + Cell.FG];
|
||||
cell.bg = this._data[startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data at `index` to `cell`.
|
||||
*/
|
||||
public setCell(index: number, cell: ICellData): void {
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
this._combined[index] = cell.combinedData;
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
|
||||
this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
|
||||
this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell data from input handler.
|
||||
* Since the input handler see the incoming chars as UTF32 codepoints,
|
||||
* it gets an optimized access method.
|
||||
*/
|
||||
public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void {
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[index * CELL_SIZE + Cell.FG] = fg;
|
||||
this._data[index * CELL_SIZE + Cell.BG] = bg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a codepoint to a cell from input handler.
|
||||
* During input stage combining chars with a width of 0 follow and stack
|
||||
* onto a leading char. Since we already set the attrs
|
||||
* by the previous `setDataFromCodePoint` call, we can omit it here.
|
||||
*/
|
||||
public addCodepointToCell(index: number, codePoint: number): void {
|
||||
let content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
// we already have a combined string, simply add
|
||||
this._combined[index] += stringFromCodePoint(codePoint);
|
||||
public set(index: number, value: CharData): void {
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS] = value[0];
|
||||
if (value[1].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK;
|
||||
} else {
|
||||
if (content & Content.CODEPOINT_MASK) {
|
||||
// normal case for combining chars:
|
||||
// - move current leading char + new one into combined string
|
||||
// - set combined flag
|
||||
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
|
||||
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
|
||||
content |= Content.IS_COMBINED_MASK;
|
||||
} else {
|
||||
// should not happen - we actually have no data in the cell yet
|
||||
// simply set the data in the cell buffer with a width of 1
|
||||
content = codePoint | (1 << Content.WIDTH_SHIFT);
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = content;
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH] = value[2];
|
||||
}
|
||||
|
||||
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
public insertCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
const cell = new CellData();
|
||||
for (let i = this.length - pos - n - 1; i >= 0; --i) {
|
||||
this.setCell(pos + n + i, this.loadCell(pos + i, cell));
|
||||
this.set(pos + n + i, this.get(pos + i));
|
||||
}
|
||||
for (let i = 0; i < n; ++i) {
|
||||
this.setCell(pos + i, fillCellData);
|
||||
this.set(pos + i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
for (let i = pos; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
const cell = new CellData();
|
||||
for (let i = 0; i < this.length - pos - n; ++i) {
|
||||
this.setCell(pos + i, this.loadCell(pos + n + i, cell));
|
||||
this.set(pos + i, this.get(pos + n + i));
|
||||
}
|
||||
for (let i = this.length - n; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
for (let i = pos; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public replaceCells(start: number, end: number, fillCellData: ICellData): void {
|
||||
public replaceCells(start: number, end: number, fillCharData: CharData): void {
|
||||
while (start < end && start < this.length) {
|
||||
this.setCell(start++, fillCellData);
|
||||
this.set(start++, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(cols: number, fillCellData: ICellData): void {
|
||||
public resize(cols: number, fillCharData: CharData): void {
|
||||
if (cols === this.length) {
|
||||
return;
|
||||
}
|
||||
@@ -531,7 +122,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
this._data = data;
|
||||
for (let i = this.length; i < cols; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
if (cols) {
|
||||
@@ -555,10 +146,10 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
/** fill a line with fillCharData */
|
||||
public fill(fillCellData: ICellData): void {
|
||||
public fill(fillCharData: CharData): void {
|
||||
this._combined = {};
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +172,8 @@ export class BufferLine implements IBufferLine {
|
||||
/** create a new clone */
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLine(0);
|
||||
// creation of new typed array from another is actually pretty slow :(
|
||||
// still faster than copying values one by one
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine.length = this.length;
|
||||
for (const el in this._combined) {
|
||||
@@ -592,8 +185,8 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
public getTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0
|
||||
return i + this._data[i * CELL_SIZE + Cell.WIDTH];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -631,10 +224,9 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
let result = '';
|
||||
while (startCol < endCol) {
|
||||
const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
|
||||
startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1
|
||||
const stringData = this._data[startCol * CELL_SIZE + Cell.STRING];
|
||||
result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR;
|
||||
startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+10
-8
@@ -3,9 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FILL_CHAR_DATA, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { IBufferLine, ICellData } from './Types';
|
||||
import { CircularList, IDeleteEvent } from './common/CircularList';
|
||||
import { IBufferLine } from './Types';
|
||||
|
||||
export interface INewLayoutResult {
|
||||
layout: number[];
|
||||
@@ -18,7 +19,7 @@ export interface INewLayoutResult {
|
||||
* @param lines The buffer lines.
|
||||
* @param newCols The columns after resize.
|
||||
*/
|
||||
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
|
||||
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number): number[] {
|
||||
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
|
||||
// batched up and only committed once
|
||||
const toRemove: number[] = [];
|
||||
@@ -74,13 +75,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, o
|
||||
if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {
|
||||
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
|
||||
// Null out the end of the last row
|
||||
wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);
|
||||
wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out remaining cells or fragments could remain;
|
||||
wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);
|
||||
wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA);
|
||||
|
||||
// Work backwards and remove any rows at the end that only contain null cells
|
||||
let countToRemove = 0;
|
||||
@@ -118,10 +119,10 @@ export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, to
|
||||
const countToRemove = toRemove[++nextToRemoveIndex];
|
||||
|
||||
// Tell markers that there was a deletion
|
||||
lines.onDeleteEmitter.fire({
|
||||
lines.emit('delete', {
|
||||
index: i - countRemovedSoFar,
|
||||
amount: countToRemove
|
||||
});
|
||||
} as IDeleteEvent);
|
||||
|
||||
i += countToRemove - 1;
|
||||
countRemovedSoFar += countToRemove;
|
||||
@@ -211,7 +212,8 @@ export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols
|
||||
// Detect whether the following line starts with a wide character and the end of the current line
|
||||
// is null, if so then we can be pretty sure the null character should be excluded from the line
|
||||
// length]
|
||||
const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;
|
||||
const cell = lines[i].get(cols - 1);
|
||||
const endsInNull = cell[CHAR_DATA_CHAR_INDEX] === '' && cell[CHAR_DATA_WIDTH_INDEX] === 1;
|
||||
const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;
|
||||
if (endsInNull && followingLineStartsWithWide) {
|
||||
return cols - 1;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer } from './Buffer';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
|
||||
describe('BufferSet', () => {
|
||||
let terminal: ITerminal;
|
||||
|
||||
+7
-10
@@ -3,28 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBufferSet, IAttributeData, IBuffer } from './Types';
|
||||
import { ITerminal, IBufferSet } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
|
||||
/**
|
||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||
* provides also utilities for working with them.
|
||||
*/
|
||||
export class BufferSet implements IBufferSet {
|
||||
export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
private _normal: Buffer;
|
||||
private _alt: Buffer;
|
||||
private _activeBuffer: Buffer;
|
||||
|
||||
|
||||
private _onBufferActivate = new EventEmitter2<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>();
|
||||
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
* @param _terminal - The terminal the BufferSet will belong to
|
||||
*/
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._normal = new Buffer(this._terminal, true);
|
||||
this._normal.fillViewportRows();
|
||||
|
||||
@@ -71,7 +68,7 @@ export class BufferSet implements IBufferSet {
|
||||
// when activated.
|
||||
this._alt.clear();
|
||||
this._activeBuffer = this._normal;
|
||||
this._onBufferActivate.fire({
|
||||
this.emit('activate', {
|
||||
activeBuffer: this._normal,
|
||||
inactiveBuffer: this._alt
|
||||
});
|
||||
@@ -80,7 +77,7 @@ export class BufferSet implements IBufferSet {
|
||||
/**
|
||||
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
||||
*/
|
||||
public activateAltBuffer(fillAttr?: IAttributeData): void {
|
||||
public activateAltBuffer(fillAttr?: number): void {
|
||||
if (this._activeBuffer === this._alt) {
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +87,7 @@ export class BufferSet implements IBufferSet {
|
||||
this._alt.x = this._normal.x;
|
||||
this._alt.y = this._normal.y;
|
||||
this._activeBuffer = this._alt;
|
||||
this._onBufferActivate.fire({
|
||||
this.emit('activate', {
|
||||
activeBuffer: this._alt,
|
||||
inactiveBuffer: this._normal
|
||||
});
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TestTerminal } from './TestUtils.test';
|
||||
import { TestTerminal } from './ui/TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { getStringCellWidth, wcwidth } from './CharWidth';
|
||||
import { IBuffer } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CellData } from './BufferLine';
|
||||
|
||||
|
||||
describe('getStringCellWidth', function(): void {
|
||||
@@ -23,7 +22,7 @@ describe('getStringCellWidth', function(): void {
|
||||
for (let i = start; i < end; ++i) {
|
||||
const line = buffer.lines.get(i);
|
||||
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
|
||||
const ch = line.loadCell(j, new CellData()).getAsCharData();
|
||||
const ch = line.get(j);
|
||||
result += ch[CHAR_DATA_WIDTH_INDEX];
|
||||
// return on sentinel
|
||||
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
|
||||
|
||||
@@ -353,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
if (this._oscHandlers[ident] === undefined) {
|
||||
this._oscHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._oscHandlers[ident];
|
||||
const handlerList = this._oscHandlers[ident];
|
||||
handlerList.push(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
@@ -505,8 +505,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
const handlers = this._csiHandlers[code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](params, collect) !== false) {
|
||||
if (handlers[j](params, collect)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -591,8 +590,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
const handlers = this._oscHandlers[identifier];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](content) !== false) {
|
||||
if (handlers[j](content)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-119
@@ -5,33 +5,32 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { DEFAULT_ATTR_DATA } from './Buffer';
|
||||
import { MockInputHandlingTerminal } from './ui/TestUtils.test';
|
||||
import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { Terminal } from './Terminal';
|
||||
import { IBufferLine } from './Types';
|
||||
import { CellData, Attributes, AttributeData } from './BufferLine';
|
||||
|
||||
describe('InputHandler', () => {
|
||||
describe('save and restore cursor', () => {
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
terminal.buffer.x = 1;
|
||||
terminal.buffer.y = 2;
|
||||
terminal.curAttrData.fg = 3;
|
||||
terminal.curAttr = 3;
|
||||
const inputHandler = new InputHandler(terminal);
|
||||
// Save cursor position
|
||||
inputHandler.saveCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttrData.fg, 3);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
// Change cursor position
|
||||
terminal.buffer.x = 10;
|
||||
terminal.buffer.y = 20;
|
||||
terminal.curAttrData.fg = 30;
|
||||
terminal.curAttr = 30;
|
||||
// Restore cursor position
|
||||
inputHandler.restoreCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttrData.fg, 3);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
});
|
||||
describe('setCursorStyle', () => {
|
||||
it('should call Terminal.setOption with correct params', () => {
|
||||
@@ -357,149 +356,45 @@ describe('InputHandler', () => {
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => {
|
||||
handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST');
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => {
|
||||
handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST');
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
// Text color of 'JUNK' should be red
|
||||
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => {
|
||||
handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST');
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => {
|
||||
handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST');
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
handler.parse('\x1b[?1049h\x1b[uTEST');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => {
|
||||
handler.parse('\x1b[42m\x1b[?1049h');
|
||||
// Buffer should be filled with green background
|
||||
expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text attributes', () => {
|
||||
let term: TestTerminal;
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal();
|
||||
});
|
||||
it('bold', () => {
|
||||
term.writeSync('\x1b[1m');
|
||||
assert.equal(!!term.curAttrData.isBold(), true);
|
||||
term.writeSync('\x1b[22m');
|
||||
assert.equal(!!term.curAttrData.isBold(), false);
|
||||
});
|
||||
it('dim', () => {
|
||||
term.writeSync('\x1b[2m');
|
||||
assert.equal(!!term.curAttrData.isDim(), true);
|
||||
term.writeSync('\x1b[22m');
|
||||
assert.equal(!!term.curAttrData.isDim(), false);
|
||||
});
|
||||
it('italic', () => {
|
||||
term.writeSync('\x1b[3m');
|
||||
assert.equal(!!term.curAttrData.isItalic(), true);
|
||||
term.writeSync('\x1b[23m');
|
||||
assert.equal(!!term.curAttrData.isItalic(), false);
|
||||
});
|
||||
it('underline', () => {
|
||||
term.writeSync('\x1b[4m');
|
||||
assert.equal(!!term.curAttrData.isUnderline(), true);
|
||||
term.writeSync('\x1b[24m');
|
||||
assert.equal(!!term.curAttrData.isUnderline(), false);
|
||||
});
|
||||
it('blink', () => {
|
||||
term.writeSync('\x1b[5m');
|
||||
assert.equal(!!term.curAttrData.isBlink(), true);
|
||||
term.writeSync('\x1b[25m');
|
||||
assert.equal(!!term.curAttrData.isBlink(), false);
|
||||
});
|
||||
it('inverse', () => {
|
||||
term.writeSync('\x1b[7m');
|
||||
assert.equal(!!term.curAttrData.isInverse(), true);
|
||||
term.writeSync('\x1b[27m');
|
||||
assert.equal(!!term.curAttrData.isInverse(), false);
|
||||
});
|
||||
it('invisible', () => {
|
||||
term.writeSync('\x1b[8m');
|
||||
assert.equal(!!term.curAttrData.isInvisible(), true);
|
||||
term.writeSync('\x1b[28m');
|
||||
assert.equal(!!term.curAttrData.isInvisible(), false);
|
||||
});
|
||||
it('colormode palette 16', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
// lower 8 colors
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
term.writeSync(`\x1b[${i + 30};${i + 40}m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getFgColor(), i);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getBgColor(), i);
|
||||
}
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
});
|
||||
it('colormode palette 256', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
// lower 8 colors
|
||||
for (let i = 0; i < 256; ++i) {
|
||||
term.writeSync(`\x1b[38;5;${i};48;5;${i}m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getFgColor(), i);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getBgColor(), i);
|
||||
}
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getFgColor(), -1);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColor(), -1);
|
||||
});
|
||||
it('colormode RGB', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB);
|
||||
assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]);
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getFgColor(), -1);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColor(), -1);
|
||||
});
|
||||
it('should zero missing RGB values', () => {
|
||||
term.writeSync(`\x1b[38;2;1;2;3m`);
|
||||
term.writeSync(`\x1b[38;2;5m`);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]);
|
||||
expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+121
-126
@@ -4,18 +4,18 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types';
|
||||
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
||||
import { C0, C1 } from './common/data/EscapeSequences';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
||||
import { NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './Buffer';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||
import { FLAGS } from './renderer/Types';
|
||||
import { wcwidth } from './CharWidth';
|
||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||
import { ICharset } from './core/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { concat } from './common/TypedArrayUtils';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder';
|
||||
import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* Map collect to glevel. Used in `selectCharset`.
|
||||
@@ -105,16 +105,6 @@ class DECRQSS implements IDcsHandler {
|
||||
export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _parseBuffer: Uint32Array = new Uint32Array(4096);
|
||||
private _stringDecoder: StringToUtf32 = new StringToUtf32();
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _onCursorMove = new EventEmitter2<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onData = new EventEmitter2<string>();
|
||||
public get onData(): IEvent<string> { return this._onData.event; }
|
||||
private _onLineFeed = new EventEmitter2<void>();
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onScroll = new EventEmitter2<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
|
||||
constructor(
|
||||
protected _terminal: IInputHandlingTerminal,
|
||||
@@ -311,46 +301,52 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (this._parseBuffer.length < data.length) {
|
||||
this._parseBuffer = new Uint32Array(data.length);
|
||||
}
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
this._parseBuffer[i] = data.charCodeAt(i);
|
||||
}
|
||||
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
|
||||
|
||||
buffer = this._terminal.buffer;
|
||||
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
|
||||
this._onCursorMove.fire();
|
||||
this._terminal.emit('cursormove');
|
||||
}
|
||||
}
|
||||
|
||||
public print(data: Uint32Array, start: number, end: number): void {
|
||||
let code: number;
|
||||
let char: string;
|
||||
let chWidth: number;
|
||||
const buffer = this._terminal.buffer;
|
||||
const charset = this._terminal.charset;
|
||||
const screenReaderMode = this._terminal.options.screenReaderMode;
|
||||
const cols = this._terminal.cols;
|
||||
const wraparoundMode = this._terminal.wraparoundMode;
|
||||
const insertMode = this._terminal.insertMode;
|
||||
const curAttr = this._terminal.curAttrData;
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const charset: ICharset = this._terminal.charset;
|
||||
const screenReaderMode: boolean = this._terminal.options.screenReaderMode;
|
||||
const cols: number = this._terminal.cols;
|
||||
const wraparoundMode: boolean = this._terminal.wraparoundMode;
|
||||
const insertMode: boolean = this._terminal.insertMode;
|
||||
const curAttr: number = this._terminal.curAttr;
|
||||
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
||||
|
||||
this._terminal.updateRange(buffer.y);
|
||||
for (let pos = start; pos < end; ++pos) {
|
||||
code = data[pos];
|
||||
char = stringFromCodePoint(code);
|
||||
|
||||
// calculate print space
|
||||
// expensive call, therefore we save width in line buffer
|
||||
chWidth = wcwidth(code);
|
||||
|
||||
// get charset replacement character
|
||||
// charset is only defined for ASCII, therefore we only
|
||||
// charset are only defined for ASCII, therefore we only
|
||||
// search for an replacement char if code < 127
|
||||
if (code < 127 && charset) {
|
||||
const ch = charset[String.fromCharCode(code)];
|
||||
const ch = charset[char];
|
||||
if (ch) {
|
||||
code = ch.charCodeAt(0);
|
||||
char = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (screenReaderMode) {
|
||||
this._terminal.emit('a11y.char', stringFromCodePoint(code));
|
||||
this._terminal.emit('a11y.char', char);
|
||||
}
|
||||
|
||||
// insert combining char at last cursor position
|
||||
@@ -359,13 +355,23 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// since they always follow a cell consuming char
|
||||
// therefore we can test for buffer.x to avoid overflow left
|
||||
if (!chWidth && buffer.x) {
|
||||
if (!bufferRow.getWidth(buffer.x - 1)) {
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
// it is save to step 2 cells back here
|
||||
// since an empty cell is only set by fullwidth chars
|
||||
bufferRow.addCodepointToCell(buffer.x - 2, code);
|
||||
} else {
|
||||
bufferRow.addCodepointToCell(buffer.x - 1, code);
|
||||
const chMinusOne = bufferRow.get(buffer.x - 1);
|
||||
if (chMinusOne) {
|
||||
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
// it is save to step 2 cells back here
|
||||
// since an empty cell is only set by fullwidth chars
|
||||
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||
if (chMinusTwo) {
|
||||
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
|
||||
bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now
|
||||
}
|
||||
} else {
|
||||
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
|
||||
bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -404,42 +410,35 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// insert mode: move characters to right
|
||||
if (insertMode) {
|
||||
// right shift cells according to the width
|
||||
bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr));
|
||||
bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
// test last cell - since the last cell has only room for
|
||||
// a halfwidth char any fullwidth shifted there is lost
|
||||
// and will be set to empty cell
|
||||
if (bufferRow.getWidth(cols - 1) === 2) {
|
||||
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg);
|
||||
// and will be set to eraseChar
|
||||
const lastCell = bufferRow.get(cols - 1);
|
||||
if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
}
|
||||
|
||||
// write current char to buffer and advance cursor
|
||||
bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg);
|
||||
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
|
||||
|
||||
// fullwidth char - also set next cell to placeholder stub and advance cursor
|
||||
// for graphemes bigger than fullwidth we can simply loop to zero
|
||||
// we already made sure above, that buffer.x + chWidth will not overflow right
|
||||
if (chWidth > 0) {
|
||||
while (--chWidth) {
|
||||
// other than a regular empty cell a cell following a wide char has no width
|
||||
bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg);
|
||||
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward addCsiHandler from parser.
|
||||
*/
|
||||
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
|
||||
return this._parser.addCsiHandler(flag, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward addOscHandler from parser.
|
||||
*/
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._parser.addOscHandler(ident, callback);
|
||||
}
|
||||
|
||||
@@ -471,8 +470,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (buffer.x >= this._terminal.cols) {
|
||||
buffer.x--;
|
||||
}
|
||||
|
||||
this._onLineFeed.fire();
|
||||
/**
|
||||
* This event is emitted whenever the terminal outputs a LF or NL.
|
||||
*
|
||||
* @event linefeed
|
||||
*/
|
||||
this._terminal.emit('linefeed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -531,7 +534,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -705,7 +708,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.replaceCells(
|
||||
start,
|
||||
end,
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
if (clearWrap) {
|
||||
line.isWrapped = false;
|
||||
@@ -775,7 +778,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||
// Force a scroll event to refresh viewport
|
||||
this._onScroll.fire(0);
|
||||
this._terminal.emit('scroll', 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -828,7 +831,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1L\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
|
||||
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
|
||||
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
@@ -858,7 +861,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
buffer.lines.splice(row, 1);
|
||||
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
|
||||
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
@@ -874,7 +877,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -890,7 +893,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -909,7 +912,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -925,7 +928,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
|
||||
this._terminal.buffer.x,
|
||||
this._terminal.buffer.x + (params[0] || 1),
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -981,10 +984,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
||||
line.loadCell(buffer.x - 1, this._workCell);
|
||||
line.replaceCells(buffer.x,
|
||||
buffer.x + (params[0] || 1),
|
||||
(this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA)
|
||||
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
// FIXME: no updateRange here?
|
||||
}
|
||||
@@ -1322,7 +1324,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// FALL-THROUGH
|
||||
case 47: // alt screen buffer
|
||||
case 1047: // alt screen buffer
|
||||
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttrData());
|
||||
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr());
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
@@ -1586,124 +1588,127 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public charAttributes(params: number[]): void {
|
||||
// Optimize a single SGR0.
|
||||
if (params.length === 1 && params[0] === 0) {
|
||||
this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg;
|
||||
this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg;
|
||||
this._terminal.curAttr = DEFAULT_ATTR;
|
||||
return;
|
||||
}
|
||||
|
||||
const l = params.length;
|
||||
let flags = this._terminal.curAttr >> 18;
|
||||
let fg = (this._terminal.curAttr >> 9) & 0x1ff;
|
||||
let bg = this._terminal.curAttr & 0x1ff;
|
||||
let p;
|
||||
const attr = this._terminal.curAttrData;
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
p = params[i];
|
||||
if (p >= 30 && p <= 37) {
|
||||
// fg color 8
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 30);
|
||||
fg = p - 30;
|
||||
} else if (p >= 40 && p <= 47) {
|
||||
// bg color 8
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 40);
|
||||
bg = p - 40;
|
||||
} else if (p >= 90 && p <= 97) {
|
||||
// fg color 16
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 90) | 8;
|
||||
p += 8;
|
||||
fg = p - 90;
|
||||
} else if (p >= 100 && p <= 107) {
|
||||
// bg color 16
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 100) | 8;
|
||||
p += 8;
|
||||
bg = p - 100;
|
||||
} else if (p === 0) {
|
||||
// default
|
||||
attr.fg = DEFAULT_ATTR_DATA.fg;
|
||||
attr.bg = DEFAULT_ATTR_DATA.bg;
|
||||
flags = DEFAULT_ATTR >> 18;
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
// flags = 0;
|
||||
// fg = 0x1ff;
|
||||
// bg = 0x1ff;
|
||||
} else if (p === 1) {
|
||||
// bold text
|
||||
attr.fg |= FgFlags.BOLD;
|
||||
flags |= FLAGS.BOLD;
|
||||
} else if (p === 3) {
|
||||
// italic text
|
||||
attr.bg |= BgFlags.ITALIC;
|
||||
flags |= FLAGS.ITALIC;
|
||||
} else if (p === 4) {
|
||||
// underlined text
|
||||
attr.fg |= FgFlags.UNDERLINE;
|
||||
flags |= FLAGS.UNDERLINE;
|
||||
} else if (p === 5) {
|
||||
// blink
|
||||
attr.fg |= FgFlags.BLINK;
|
||||
flags |= FLAGS.BLINK;
|
||||
} else if (p === 7) {
|
||||
// inverse and positive
|
||||
// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
|
||||
attr.fg |= FgFlags.INVERSE;
|
||||
flags |= FLAGS.INVERSE;
|
||||
} else if (p === 8) {
|
||||
// invisible
|
||||
attr.fg |= FgFlags.INVISIBLE;
|
||||
flags |= FLAGS.INVISIBLE;
|
||||
} else if (p === 2) {
|
||||
// dimmed text
|
||||
attr.bg |= BgFlags.DIM;
|
||||
flags |= FLAGS.DIM;
|
||||
} else if (p === 22) {
|
||||
// not bold nor faint
|
||||
attr.fg &= ~FgFlags.BOLD;
|
||||
attr.bg &= ~BgFlags.DIM;
|
||||
flags &= ~FLAGS.BOLD;
|
||||
flags &= ~FLAGS.DIM;
|
||||
} else if (p === 23) {
|
||||
// not italic
|
||||
attr.bg &= ~BgFlags.ITALIC;
|
||||
flags &= ~FLAGS.ITALIC;
|
||||
} else if (p === 24) {
|
||||
// not underlined
|
||||
attr.fg &= ~FgFlags.UNDERLINE;
|
||||
flags &= ~FLAGS.UNDERLINE;
|
||||
} else if (p === 25) {
|
||||
// not blink
|
||||
attr.fg &= ~FgFlags.BLINK;
|
||||
flags &= ~FLAGS.BLINK;
|
||||
} else if (p === 27) {
|
||||
// not inverse
|
||||
attr.fg &= ~FgFlags.INVERSE;
|
||||
flags &= ~FLAGS.INVERSE;
|
||||
} else if (p === 28) {
|
||||
// not invisible
|
||||
attr.fg &= ~FgFlags.INVISIBLE;
|
||||
flags &= ~FLAGS.INVISIBLE;
|
||||
} else if (p === 39) {
|
||||
// reset fg
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
} else if (p === 49) {
|
||||
// reset bg
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
} else if (p === 38) {
|
||||
// fg color 256
|
||||
if (params[i + 1] === 2) {
|
||||
i += 2;
|
||||
attr.fg |= Attributes.CM_RGB;
|
||||
attr.fg &= ~Attributes.RGB_MASK;
|
||||
attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
|
||||
fg = this._terminal.matchColor(
|
||||
params[i] & 0xff,
|
||||
params[i + 1] & 0xff,
|
||||
params[i + 2] & 0xff);
|
||||
if (fg === -1) fg = 0x1ff;
|
||||
i += 2;
|
||||
} else if (params[i + 1] === 5) {
|
||||
i += 2;
|
||||
p = params[i] & 0xff;
|
||||
attr.fg &= ~Attributes.PCOLOR_MASK;
|
||||
attr.fg |= Attributes.CM_P256 | p;
|
||||
fg = p;
|
||||
}
|
||||
} else if (p === 48) {
|
||||
// bg color 256
|
||||
if (params[i + 1] === 2) {
|
||||
i += 2;
|
||||
attr.bg |= Attributes.CM_RGB;
|
||||
attr.bg &= ~Attributes.RGB_MASK;
|
||||
attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
|
||||
bg = this._terminal.matchColor(
|
||||
params[i] & 0xff,
|
||||
params[i + 1] & 0xff,
|
||||
params[i + 2] & 0xff);
|
||||
if (bg === -1) bg = 0x1ff;
|
||||
i += 2;
|
||||
} else if (params[i + 1] === 5) {
|
||||
i += 2;
|
||||
p = params[i] & 0xff;
|
||||
attr.bg &= ~Attributes.PCOLOR_MASK;
|
||||
attr.bg |= Attributes.CM_P256 | p;
|
||||
bg = p;
|
||||
}
|
||||
} else if (p === 100) {
|
||||
// reset fg/bg
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
} else {
|
||||
this._terminal.error('Unknown SGR attribute: %d.', p);
|
||||
}
|
||||
}
|
||||
|
||||
this._terminal.curAttr = (flags << 18) | (fg << 9) | bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1734,13 +1739,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
switch (params[0]) {
|
||||
case 5:
|
||||
// status report
|
||||
this._onData.fire(`${C0.ESC}[0n`);
|
||||
this._terminal.emit('data', `${C0.ESC}[0n`);
|
||||
break;
|
||||
case 6:
|
||||
// cursor position
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._onData.fire(`${C0.ESC}[${y};${x}R`);
|
||||
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
|
||||
break;
|
||||
}
|
||||
} else if (collect === '?') {
|
||||
@@ -1751,7 +1756,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// cursor position
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._onData.fire(`${C0.ESC}[?${y};${x}R`);
|
||||
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
|
||||
break;
|
||||
case 15:
|
||||
// no printer
|
||||
@@ -1790,7 +1795,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.applicationCursor = false;
|
||||
this._terminal.buffer.scrollTop = 0;
|
||||
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
|
||||
this._terminal.curAttrData = DEFAULT_ATTR_DATA;
|
||||
this._terminal.curAttr = DEFAULT_ATTR;
|
||||
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
|
||||
this._terminal.charset = null;
|
||||
this._terminal.glevel = 0; // ??
|
||||
@@ -1837,9 +1842,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* CSI ? Pm r
|
||||
*/
|
||||
public setScrollRegion(params: number[], collect?: string): void {
|
||||
if (collect) {
|
||||
return;
|
||||
}
|
||||
if (collect) return;
|
||||
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
|
||||
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
|
||||
this._terminal.buffer.x = 0;
|
||||
@@ -1855,8 +1858,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public saveCursor(params: number[]): void {
|
||||
this._terminal.buffer.savedX = this._terminal.buffer.x;
|
||||
this._terminal.buffer.savedY = this._terminal.buffer.y;
|
||||
this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg;
|
||||
this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg;
|
||||
this._terminal.buffer.savedCurAttr = this._terminal.curAttr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1868,8 +1870,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public restoreCursor(params: number[]): void {
|
||||
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
|
||||
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
|
||||
this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg;
|
||||
this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg;
|
||||
this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR;
|
||||
}
|
||||
|
||||
|
||||
@@ -1947,15 +1948,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?
|
||||
*/
|
||||
public selectCharset(collectAndFlag: string): void {
|
||||
if (collectAndFlag.length !== 2) {
|
||||
this.selectDefaultCharset();
|
||||
return;
|
||||
}
|
||||
if (collectAndFlag[0] === '/') {
|
||||
return; // TODO: Is this supported?
|
||||
}
|
||||
if (collectAndFlag.length !== 2) return this.selectDefaultCharset();
|
||||
if (collectAndFlag[0] === '/') return; // TODO: Is this supported?
|
||||
this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine, CellData } from './BufferLine';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(terminal: ITerminal) {
|
||||
@@ -52,7 +53,7 @@ describe('Linkifier', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+15
-18
@@ -3,15 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types';
|
||||
import { MouseZone } from './MouseZoneManager';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
|
||||
import { MouseZone } from './ui/MouseZoneManager';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
import { getStringCellWidth } from './CharWidth';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier implements ILinkifier {
|
||||
export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
@@ -33,16 +35,10 @@ export class Linkifier implements ILinkifier {
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
|
||||
private _onLinkHover = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
private _onLinkLeave = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
|
||||
private _onLinkTooltip = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal
|
||||
) {
|
||||
super();
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
@@ -236,9 +232,10 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||
const attr = line.getFg(bufferIndex[1]);
|
||||
const char = line.get(bufferIndex[1]);
|
||||
let fg: number | undefined;
|
||||
if (attr) {
|
||||
if (char) {
|
||||
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||
fg = (attr >> 9) & 0x1ff;
|
||||
}
|
||||
|
||||
@@ -288,18 +285,18 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
() => {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
@@ -314,7 +311,7 @@ export class Linkifier implements ILinkifier {
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent {
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
import { BufferLine, CellData } from './BufferLine';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
emit(event: string, data: any): void {}
|
||||
@@ -57,14 +57,14 @@ describe('SelectionManager', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(chars.length);
|
||||
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
|
||||
chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)]));
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('SelectionManager', () => {
|
||||
[null, 'o', 1, 'o'.charCodeAt(0)]
|
||||
];
|
||||
const line = new BufferLine(data.length);
|
||||
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
|
||||
for (let i = 0; i < data.length; ++i) line.set(i, data[i]);
|
||||
buffer.lines.set(0, line);
|
||||
// Ensure wide characters take up 2 columns
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
|
||||
+58
-66
@@ -3,15 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, IBufferLine, ISelectionRedrawRequestEvent } from './Types';
|
||||
import { MouseHelper } from './MouseHelper';
|
||||
import * as Browser from './common/Platform';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
|
||||
import { XtermListener } from './common/Types';
|
||||
import { MouseHelper } from './ui/MouseHelper';
|
||||
import * as Browser from './core/Platform';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
import { CellData } from './BufferLine';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -67,10 +67,10 @@ export const enum SelectionMode {
|
||||
* SelectionModel, SelectionManager handles with all logic associated with
|
||||
* dealing with the selection, including handling mouse interaction, wide
|
||||
* characters and fetching the actual text within the selection. Rendering is
|
||||
* not handled by the SelectionManager but the onRedrawRequest event is fired
|
||||
* when the selection is ready to be redrawn (on an animation frame).
|
||||
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
||||
* selection is ready to be redrawn.
|
||||
*/
|
||||
export class SelectionManager implements ISelectionManager {
|
||||
export class SelectionManager extends EventEmitter implements ISelectionManager {
|
||||
protected _model: SelectionModel;
|
||||
|
||||
/**
|
||||
@@ -102,22 +102,15 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: IDisposable;
|
||||
private _workCell: CellData = new CellData();
|
||||
private _trimListener: XtermListener;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
private _onLinuxMouseSelection = new EventEmitter2<string>();
|
||||
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
|
||||
private _onRedrawRequest = new EventEmitter2<ISelectionRedrawRequestEvent>();
|
||||
public get onRedrawRequest(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
|
||||
private _onSelectionChange = new EventEmitter2<void>();
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
this._initListeners();
|
||||
this.enable();
|
||||
|
||||
@@ -126,6 +119,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
@@ -139,13 +133,14 @@ export class SelectionManager implements ISelectionManager {
|
||||
private _initListeners(): void {
|
||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||
this._trimListener = (amount: number) => this._onTrim(amount);
|
||||
|
||||
this.initBuffersListeners();
|
||||
}
|
||||
|
||||
public initBuffersListeners(): void {
|
||||
this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e));
|
||||
this._terminal.buffer.lines.on('trim', this._trimListener);
|
||||
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,10 +244,10 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
/**
|
||||
* Queues a refresh, redrawing the selection on the next opportunity.
|
||||
* @param isLinuxMouseSelection Whether the selection should be registered as a new
|
||||
* @param isNewSelection Whether the selection should be registered as a new
|
||||
* selection on Linux.
|
||||
*/
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
public refresh(isNewSelection?: boolean): void {
|
||||
// Queue the refresh for the renderer
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||
@@ -260,10 +255,10 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||
// we need to update the selection for middle click to paste selection.
|
||||
if (Browser.isLinux && isLinuxMouseSelection) {
|
||||
if (Browser.isLinux && isNewSelection) {
|
||||
const selectionText = this.selectionText;
|
||||
if (selectionText.length) {
|
||||
this._onLinuxMouseSelection.fire(this.selectionText);
|
||||
this.emit('newselection', this.selectionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +269,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
*/
|
||||
private _refresh(): void {
|
||||
this._refreshAnimationFrame = null;
|
||||
this._onRedrawRequest.fire({
|
||||
this.emit('refresh', {
|
||||
start: this._model.finalSelectionStart,
|
||||
end: this._model.finalSelectionEnd,
|
||||
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
||||
@@ -323,7 +318,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
public selectAll(): void {
|
||||
this._model.isSelectAllActive = true;
|
||||
this.refresh();
|
||||
this._onSelectionChange.fire();
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
public selectLines(start: number, end: number): void {
|
||||
@@ -333,7 +328,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
this._model.selectionStart = [0, start];
|
||||
this._model.selectionEnd = [this._terminal.cols, end];
|
||||
this.refresh();
|
||||
this._onSelectionChange.fire();
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,7 +506,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// If the mouse is over the second half of a wide character, adjust the
|
||||
// selection to cover the whole character
|
||||
if (line.hasWidth(this._model.selectionStart[0]) === 0) {
|
||||
const char = line.get(this._model.selectionStart[0]);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
}
|
||||
@@ -600,7 +596,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
// selection. Note that selections at the very end of the line will never
|
||||
// have a character.
|
||||
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
||||
if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
|
||||
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
|
||||
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
}
|
||||
@@ -651,7 +648,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
||||
(new AltClickHandler(event, this._terminal)).move();
|
||||
} else if (this.hasSelection) {
|
||||
this._onSelectionChange.fire();
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,10 +658,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
// reverseIndex) and delete in a splice is only ever used when the same
|
||||
// number of elements was just added. Given this is could actually be
|
||||
// beneficial to leave the selection as is for these cases.
|
||||
if (this._trimListener) {
|
||||
this._trimListener.dispose();
|
||||
}
|
||||
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
e.inactiveBuffer.lines.off('trim', this._trimListener);
|
||||
e.activeBuffer.lines.on('trim', this._trimListener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,16 +670,16 @@ export class SelectionManager implements ISelectionManager {
|
||||
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
const char = bufferLine.get(i);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
charIndex--;
|
||||
} else if (length > 1 && coords[0] !== i) {
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) {
|
||||
// Emojis take up multiple characters, so adjust accordingly. For these
|
||||
// we don't want ot include the character at the column as we're
|
||||
// returning the start index in the string, not the end index.
|
||||
charIndex += length - 1;
|
||||
charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
}
|
||||
return charIndex;
|
||||
@@ -744,51 +739,48 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine.getWidth(startCol) === 0) {
|
||||
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine.getWidth(endCol) === 2) {
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
|
||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||
const length = bufferLine.getString(endCol).length;
|
||||
if (length > 1) {
|
||||
rightLongCharOffset += length - 1;
|
||||
endIndex += length - 1;
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
|
||||
bufferLine.loadCell(startCol - 1, this._workCell);
|
||||
const length = this._workCell.getChars().length;
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
|
||||
const char = bufferLine.get(startCol - 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
} else if (length > 1) {
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
leftLongCharOffset += length - 1;
|
||||
startIndex -= length - 1;
|
||||
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
|
||||
bufferLine.loadCell(endCol + 1, this._workCell);
|
||||
const length = this._workCell.getChars().length;
|
||||
if (this._workCell.getWidth() === 2) {
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
|
||||
const char = bufferLine.get(endCol + 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
} else if (length > 1) {
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
rightLongCharOffset += length - 1;
|
||||
endIndex += length - 1;
|
||||
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
endIndex++;
|
||||
endCol++;
|
||||
@@ -822,9 +814,9 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||
if (followWrappedLinesAbove) {
|
||||
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||
if (previousLineWordPosition) {
|
||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||
@@ -837,9 +829,9 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||
if (followWrappedLinesBelow) {
|
||||
if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||
if (nextLineWordPosition) {
|
||||
length += nextLineWordPosition.length;
|
||||
@@ -902,13 +894,13 @@ export class SelectionManager implements ISelectionManager {
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
*/
|
||||
private _isCharWordSeparator(cell: CellData): boolean {
|
||||
private _isCharWordSeparator(charData: CharData): boolean {
|
||||
// Zero width characters are never separators as they are always to the
|
||||
// right of wide characters
|
||||
if (cell.getWidth() === 0) {
|
||||
if (charData[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
return false;
|
||||
}
|
||||
return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0;
|
||||
return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
constructor(
|
||||
|
||||
@@ -13,9 +13,8 @@ import * as path from 'path';
|
||||
import * as pty from 'node-pty';
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
import { IViewport } from './Types';
|
||||
import { CellData } from './BufferLine';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
innerWrite(): void { this._innerWrite(); }
|
||||
@@ -68,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
for (let cell = 0; cell < term.cols; ++cell) {
|
||||
lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR;
|
||||
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
|
||||
+208
-329
@@ -5,9 +5,8 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
|
||||
import { DEFAULT_ATTR_DATA } from './Buffer';
|
||||
import { CellData } from './BufferLine';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -17,7 +16,7 @@ class TestTerminal extends Terminal {
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('xterm.js', () => {
|
||||
describe('term.js addons', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
@@ -68,81 +67,6 @@ describe('xterm.js', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('should fire the onData evnet', (done) => {
|
||||
term.onData(() => done());
|
||||
term.handler('fake');
|
||||
});
|
||||
it('should fire the onCursorMove event', (done) => {
|
||||
term.on('cursormove', () => done());
|
||||
term.write('foo');
|
||||
});
|
||||
it('should fire the onLineFeed event', (done) => {
|
||||
term.on('linefeed', () => done());
|
||||
term.write('\n');
|
||||
});
|
||||
it('should fire a scroll event when scrollback is created', (done) => {
|
||||
term.on('scroll', () => done());
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
});
|
||||
it('should fire a scroll event when scrollback is cleared', (done) => {
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
term.on('scroll', () => done());
|
||||
term.clear();
|
||||
});
|
||||
it('should fire a key event after a keypress DOM event', (done) => {
|
||||
term.onKey(e => {
|
||||
assert.equal(typeof e.key, 'string');
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
const evKeyPress = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
type: 'keypress',
|
||||
keyCode: 13
|
||||
};
|
||||
term.keyPress(evKeyPress);
|
||||
});
|
||||
it('should fire a key event after a keydown DOM event', (done) => {
|
||||
term.onKey(e => {
|
||||
assert.equal(typeof e.key, 'string');
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
const evKeyDown = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
type: 'keydown',
|
||||
keyCode: 13
|
||||
};
|
||||
term.keyDown(evKeyDown);
|
||||
});
|
||||
it('should fire the onResize event', (done) => {
|
||||
term.onResize(e => {
|
||||
expect(e).to.have.keys(['cols', 'rows']);
|
||||
assert.equal(typeof e.cols, 'number');
|
||||
assert.equal(typeof e.rows, 'number');
|
||||
done();
|
||||
});
|
||||
term.resize(1, 1);
|
||||
});
|
||||
it('should fire the onScroll event', (done) => {
|
||||
term.onScroll(e => {
|
||||
assert.equal(typeof e, 'number');
|
||||
done();
|
||||
});
|
||||
term.scroll();
|
||||
});
|
||||
it('should fire the onTitleChange event', (done) => {
|
||||
term.onTitleChange(e => {
|
||||
assert.equal(e, 'title');
|
||||
done();
|
||||
});
|
||||
term.handleTitle('title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on', () => {
|
||||
beforeEach(() => {
|
||||
term.on('key', () => { });
|
||||
@@ -160,40 +84,6 @@ describe('xterm.js', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursormove', () => {
|
||||
it('should emit a cursormove event', (done) => {
|
||||
term.on('cursormove', () => {
|
||||
done();
|
||||
});
|
||||
term.write('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('linefeed', () => {
|
||||
it('should emit a linefeed event', (done) => {
|
||||
term.on('linefeed', () => {
|
||||
done();
|
||||
});
|
||||
term.write('\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll', () => {
|
||||
it('should emit a scroll event when scrollback is created', (done) => {
|
||||
term.on('scroll', () => {
|
||||
done();
|
||||
});
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
});
|
||||
it('should emit a scroll event when scrollback is cleared', (done) => {
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
term.on('scroll', () => {
|
||||
done();
|
||||
});
|
||||
term.clear();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`keypress (including 'key' event)`, () => {
|
||||
it('should receive a string and event object', (done) => {
|
||||
let steps = 0;
|
||||
@@ -337,7 +227,7 @@ describe('xterm.js', () => {
|
||||
});
|
||||
|
||||
describe('setOption', () => {
|
||||
it('should set option correctly', () => {
|
||||
it('should set the option correctly', () => {
|
||||
term.setOption('cursorBlink', true);
|
||||
assert.equal(term.options.cursorBlink, true);
|
||||
term.setOption('cursorBlink', false);
|
||||
@@ -369,7 +259,7 @@ describe('xterm.js', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
it('should clear a buffer larger than rows', () => {
|
||||
@@ -386,7 +276,7 @@ describe('xterm.js', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
it('should not break the prompt when cleared twice', () => {
|
||||
@@ -399,7 +289,7 @@ describe('xterm.js', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -565,62 +455,62 @@ describe('xterm.js', () => {
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), '');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -631,65 +521,65 @@ describe('xterm.js', () => {
|
||||
});
|
||||
|
||||
it('should create a new line and shift everything up', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// 'a' gets pushed out of buffer
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), '');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -880,126 +770,116 @@ describe('xterm.js', () => {
|
||||
it('2 characters per cell', function (): void {
|
||||
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high + String.fromCharCode(i));
|
||||
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(tchar.getChars().length).eql(2);
|
||||
expect(tchar.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters at last cell', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.write(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end with autowrap', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = true;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
|
||||
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end without autowrap', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = false;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
|
||||
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('splitted surrogates', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high);
|
||||
term.write(String.fromCharCode(i));
|
||||
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(tchar.getChars().length).eql(2);
|
||||
expect(tchar.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('unicode - combining characters', () => {
|
||||
const cell = new CellData();
|
||||
it('café', () => {
|
||||
term.write('cafe\u0301');
|
||||
term.buffer.lines.get(0).loadCell(3, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('café - end of line', () => {
|
||||
term.buffer.x = term.cols - 1 - 3;
|
||||
term.write('cafe\u0301');
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('e\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
}
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple surrogate with combined', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
}
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unicode - fullwidth characters', () => {
|
||||
const cell = new CellData();
|
||||
it('cursor movement even', () => {
|
||||
expect(term.buffer.x).eql(0);
|
||||
term.write('¥');
|
||||
@@ -1015,141 +895,140 @@ describe('xterm.js', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
it('line of ¥ odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
it('line of ¥ with combining odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
it('line of ¥ with combining even', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining even', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
} else {
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert mode', () => {
|
||||
const cell = new CellData();
|
||||
it('halfwidth - all', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
term.buffer.x = 10;
|
||||
@@ -1157,10 +1036,10 @@ describe('xterm.js', () => {
|
||||
term.insertMode = true;
|
||||
term.write('abcde');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
|
||||
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
});
|
||||
it('fullwidth - insert', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
@@ -1169,11 +1048,11 @@ describe('xterm.js', () => {
|
||||
term.insertMode = true;
|
||||
term.write('¥¥¥');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||
});
|
||||
it('fullwidth - right border', () => {
|
||||
term.write(Array(41).join('¥'));
|
||||
@@ -1182,14 +1061,14 @@ describe('xterm.js', () => {
|
||||
term.insertMode = true;
|
||||
term.write('a');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
|
||||
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+128
-108
@@ -21,26 +21,28 @@
|
||||
* http://linux.die.net/man/7/urxvt
|
||||
*/
|
||||
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IBufferLine, IAttributeData, IMouseZoneManager } from './Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { IRenderer } from './renderer/Types';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR_DATA } from './Buffer';
|
||||
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
import { CompositionHelper } from './CompositionHelper';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { Viewport } from './Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './Clipboard';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/Clipboard';
|
||||
import { C0 } from './common/data/EscapeSequences';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { Renderer } from './renderer/Renderer';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import * as Browser from './common/Platform';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import * as Browser from './core/Platform';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import * as Strings from './Strings';
|
||||
import { MouseHelper } from './MouseHelper';
|
||||
import { MouseHelper } from './ui/MouseHelper';
|
||||
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
|
||||
import { MouseZoneManager } from './MouseZoneManager';
|
||||
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
|
||||
import { MouseZoneManager } from './ui/MouseZoneManager';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
|
||||
import { ITheme, IMarker, IDisposable } from 'xterm';
|
||||
@@ -50,9 +52,6 @@ import { IKeyboardEvent } from './common/Types';
|
||||
import { evaluateKeyboardEvent } from './core/input/Keyboard';
|
||||
import { KeyboardResultType, ICharset } from './core/Types';
|
||||
import { clone } from './common/Clone';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { Attributes } from './BufferLine';
|
||||
import { applyWindowsMode } from './WindowsMode';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -111,8 +110,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
tabStopWidth: 8,
|
||||
theme: null,
|
||||
rightClickSelectsWord: Browser.isMac,
|
||||
rendererType: 'canvas',
|
||||
windowsMode: false
|
||||
rendererType: 'canvas'
|
||||
};
|
||||
|
||||
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
|
||||
@@ -175,8 +173,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
private _refreshEnd: number;
|
||||
public savedCols: number;
|
||||
|
||||
public curAttrData: IAttributeData;
|
||||
private _eraseAttrData: IAttributeData;
|
||||
public curAttr: number;
|
||||
|
||||
public params: (string | number)[];
|
||||
public currentParam: string | number;
|
||||
@@ -213,7 +210,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
private _accessibilityManager: AccessibilityManager;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _theme: ITheme;
|
||||
private _windowsMode: IDisposable | undefined;
|
||||
|
||||
// bufferline to clone/copy from for new blank lines
|
||||
private _blankLine: IBufferLine = null;
|
||||
@@ -221,25 +217,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
public cols: number;
|
||||
public rows: number;
|
||||
|
||||
private _onCursorMove = new EventEmitter2<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onData = new EventEmitter2<string>();
|
||||
public get onData(): IEvent<string> { return this._onData.event; }
|
||||
private _onKey = new EventEmitter2<{ key: string, domEvent: KeyboardEvent }>();
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
|
||||
private _onLineFeed = new EventEmitter2<void>();
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onRender = new EventEmitter2<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onResize = new EventEmitter2<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
private _onScroll = new EventEmitter2<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
private _onSelectionChange = new EventEmitter2<void>();
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onTitleChange = new EventEmitter2<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
|
||||
/**
|
||||
* Creates a new `Terminal` object.
|
||||
*
|
||||
@@ -258,26 +235,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
super();
|
||||
this.options = clone(options);
|
||||
this._setup();
|
||||
|
||||
// TODO: Remove these in v4
|
||||
// Fire old style events from new emitters
|
||||
this.onCursorMove(() => this.emit('cursormove'));
|
||||
this.onData(e => this.emit('data', e));
|
||||
this.onKey(e => this.emit('key', e.key, e.domEvent));
|
||||
this.onLineFeed(() => this.emit('linefeed'));
|
||||
this.onRender(e => this.emit('refresh', e));
|
||||
this.onResize(e => this.emit('resize', e));
|
||||
this.onSelectionChange(() => this.emit('selection'));
|
||||
this.onScroll(e => this.emit('scroll', e));
|
||||
this.onTitleChange(e => this.emit('title', e));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
if (this._windowsMode) {
|
||||
this._windowsMode.dispose();
|
||||
this._windowsMode = undefined;
|
||||
}
|
||||
this._customKeyEventHandler = null;
|
||||
removeTerminalFromCache(this);
|
||||
this.handler = () => {};
|
||||
@@ -310,7 +271,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.rows = Math.max(this.options.rows, MINIMUM_ROWS);
|
||||
|
||||
if (this.options.handler) {
|
||||
this.onData(this.options.handler);
|
||||
this.on('data', this.options.handler);
|
||||
}
|
||||
|
||||
this.cursorState = 0;
|
||||
@@ -332,8 +293,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// TODO: Can this be just []?
|
||||
this.charsets = [null];
|
||||
|
||||
this.curAttrData = DEFAULT_ATTR_DATA.clone();
|
||||
this._eraseAttrData = DEFAULT_ATTR_DATA.clone();
|
||||
this.curAttr = DEFAULT_ATTR;
|
||||
|
||||
this.params = [];
|
||||
this.currentParam = 0;
|
||||
@@ -346,13 +306,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// this._writeStopped = false;
|
||||
this._userScrolling = false;
|
||||
|
||||
// Register input handler and refire/handle events
|
||||
this._inputHandler = new InputHandler(this);
|
||||
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
|
||||
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
|
||||
this._inputHandler.onData(e => this._onData.fire(e));
|
||||
this.register(this._inputHandler);
|
||||
|
||||
// Reuse renderer if the Terminal is being recreated via a reset call.
|
||||
this.renderer = this.renderer || null;
|
||||
this.selectionManager = this.selectionManager || null;
|
||||
@@ -366,10 +321,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.selectionManager.clearSelection();
|
||||
this.selectionManager.initBuffersListeners();
|
||||
}
|
||||
|
||||
if (this.options.windowsMode) {
|
||||
this._windowsMode = applyWindowsMode(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,10 +333,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* back_color_erase feature for xterm.
|
||||
*/
|
||||
public eraseAttrData(): IAttributeData {
|
||||
this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);
|
||||
this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000;
|
||||
return this._eraseAttrData;
|
||||
public eraseAttr(): number {
|
||||
// if (this.is('screen')) return DEFAULT_ATTR;
|
||||
return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +343,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
*/
|
||||
public focus(): void {
|
||||
if (this.textarea) {
|
||||
this.textarea.focus({ preventScroll: true });
|
||||
this.textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,18 +501,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
break;
|
||||
case 'tabStopWidth': this.buffers.setupTabStops(); break;
|
||||
case 'windowsMode':
|
||||
if (value) {
|
||||
if (!this._windowsMode) {
|
||||
this._windowsMode = applyWindowsMode(this);
|
||||
}
|
||||
} else {
|
||||
if (this._windowsMode) {
|
||||
this._windowsMode.dispose();
|
||||
this._windowsMode = undefined;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Inform renderer of changes
|
||||
if (this.renderer) {
|
||||
@@ -685,8 +623,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
|
||||
this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
|
||||
this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
|
||||
this.register(this.onRender(() => this._compositionHelper.updateCompositionElements()));
|
||||
this.register(this.onRender(e => this._queueLinkification(e.start, e.end)));
|
||||
this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements()));
|
||||
this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -738,7 +676,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.register(this._mouseZoneManager);
|
||||
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
|
||||
this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll()));
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.textarea = document.createElement('textarea');
|
||||
@@ -771,22 +709,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
||||
this.register(this.viewport);
|
||||
|
||||
this.register(this.onCursorMove(() => this.renderer.onCursorMove()));
|
||||
this.register(this.onResize(() => this.renderer.onResize(this.cols, this.rows)));
|
||||
this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove()));
|
||||
this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows)));
|
||||
this.register(this.addDisposableListener('blur', () => this.renderer.onBlur()));
|
||||
this.register(this.addDisposableListener('focus', () => this.renderer.onFocus()));
|
||||
this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)));
|
||||
// dprchange should handle this case, we need this as well for browsers that don't support the
|
||||
// matchMedia query.
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
|
||||
this.register(this.charMeasure.onCharSizeChanged(() => this.renderer.onCharSizeChanged()));
|
||||
this.register(this.renderer.onCanvasResize(() => this.viewport.syncScrollArea()));
|
||||
this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged()));
|
||||
this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea()));
|
||||
|
||||
this.selectionManager = new SelectionManager(this, this.charMeasure);
|
||||
this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
|
||||
this.register(this.selectionManager.onRedrawRequest(e => this.renderer.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
this.register(this.selectionManager.onLinuxMouseSelection(text => {
|
||||
this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode)));
|
||||
this.register(this.selectionManager.addDisposableListener('newselection', text => {
|
||||
// If there's a new selection, put it into the textarea, focus and select it
|
||||
// in order to register it as a selection on the OS. This event is fired
|
||||
// only on Linux to enable middle click to paste selection.
|
||||
@@ -794,7 +731,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.textarea.focus();
|
||||
this.textarea.select();
|
||||
}));
|
||||
this.register(this.onScroll(() => {
|
||||
this.register(this.addDisposableListener('scroll', () => {
|
||||
this.viewport.syncScrollArea();
|
||||
this.selectionManager.refresh();
|
||||
}));
|
||||
@@ -836,7 +773,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
|
||||
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
||||
}
|
||||
this.renderer.onRender(e => this._onRender.fire(e));
|
||||
this.register(this.renderer);
|
||||
}
|
||||
|
||||
@@ -925,11 +861,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
if (ch > 127) ch = 127;
|
||||
data.push(ch);
|
||||
} else {
|
||||
if (ch > 2047) {
|
||||
data.push(2047);
|
||||
if (ch === 2047) {
|
||||
data.push(0);
|
||||
return;
|
||||
}
|
||||
data.push(ch);
|
||||
if (ch < 127) {
|
||||
data.push(ch);
|
||||
} else {
|
||||
if (ch > 2047) ch = 2047;
|
||||
data.push(0xC0 | (ch >> 6));
|
||||
data.push(0x80 | (ch & 0x3F));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1245,9 +1187,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
public scroll(isWrapped: boolean = false): void {
|
||||
let newLine: IBufferLine;
|
||||
newLine = this._blankLine;
|
||||
const eraseAttr = this.eraseAttrData();
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = this.buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) {
|
||||
newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped);
|
||||
this._blankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
@@ -1302,7 +1243,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.updateRange(this.buffer.scrollTop);
|
||||
this.updateRange(this.buffer.scrollBottom);
|
||||
|
||||
this._onScroll.fire(this.buffer.ydisp);
|
||||
/**
|
||||
* This event is emitted whenever the terminal is scrolled.
|
||||
* The one parameter passed is the new y display position.
|
||||
*
|
||||
* @event scroll
|
||||
*/
|
||||
this.emit('scroll', this.buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1331,7 +1278,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this._onScroll.fire(this.buffer.ydisp);
|
||||
this.emit('scroll', this.buffer.ydisp);
|
||||
}
|
||||
|
||||
this.refresh(0, this.rows - 1);
|
||||
@@ -1620,7 +1567,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
|
||||
this.emit('keydown', event);
|
||||
this._onKey.fire({ key: result.key, domEvent: event });
|
||||
this.emit('key', result.key, event);
|
||||
this.showCursor();
|
||||
this.handler(result.key);
|
||||
|
||||
@@ -1699,7 +1646,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
key = String.fromCharCode(key);
|
||||
|
||||
this.emit('keypress', key, ev);
|
||||
this._onKey.fire({ key, domEvent: ev });
|
||||
this.emit('key', key, ev);
|
||||
this.showCursor();
|
||||
this.handler(key);
|
||||
|
||||
@@ -1776,7 +1723,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
|
||||
this.refresh(0, this.rows - 1);
|
||||
this._onResize.fire({ cols: x, rows: y });
|
||||
this.emit('resize', {cols: x, rows: y});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1816,10 +1763,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.buffer.ybase = 0;
|
||||
this.buffer.y = 0;
|
||||
for (let i = 1; i < this.rows; i++) {
|
||||
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
this.refresh(0, this.rows - 1);
|
||||
this._onScroll.fire(this.buffer.ydisp);
|
||||
this.emit('scroll', this.buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* If cur return the back color xterm feature attribute. Else return default attribute.
|
||||
* @param cur
|
||||
*/
|
||||
public ch(cur?: boolean): CharData {
|
||||
if (cur) {
|
||||
return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1831,7 +1789,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the data event and populate the given data.
|
||||
* Emit the 'data' event and populate the given data.
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
public handler(data: string): void {
|
||||
@@ -1849,7 +1807,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
this._onData.fire(data);
|
||||
this.emit('data', data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1857,7 +1815,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
* @param title The title to populate in the event.
|
||||
*/
|
||||
public handleTitle(title: string): void {
|
||||
this._onTitleChange.fire(title);
|
||||
/**
|
||||
* This event is emitted when the title of the terminal is changed
|
||||
* from inside the terminal. The parameter is the new title.
|
||||
*
|
||||
* @event title
|
||||
*/
|
||||
this.emit('title', title);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1891,7 +1855,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// blankLine(true) is xterm/linux behavior
|
||||
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
|
||||
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
|
||||
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData()));
|
||||
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr()));
|
||||
this.updateRange(this.buffer.scrollTop);
|
||||
this.updateRange(this.buffer.scrollBottom);
|
||||
} else {
|
||||
@@ -1936,6 +1900,46 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Remove when true color is implemented
|
||||
public matchColor(r1: number, g1: number, b1: number): number {
|
||||
const hash = (r1 << 16) | (g1 << 8) | b1;
|
||||
|
||||
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) {
|
||||
return matchColorCache[hash];
|
||||
}
|
||||
|
||||
let ldiff = Infinity;
|
||||
let li = -1;
|
||||
let i = 0;
|
||||
let c: number;
|
||||
let r2: number;
|
||||
let g2: number;
|
||||
let b2: number;
|
||||
let diff: number;
|
||||
|
||||
for (; i < DEFAULT_ANSI_COLORS.length; i++) {
|
||||
c = DEFAULT_ANSI_COLORS[i].rgba;
|
||||
r2 = c >>> 24;
|
||||
g2 = c >>> 16 & 0xFF;
|
||||
b2 = c >>> 8 & 0xFF;
|
||||
// assume that alpha is 0xFF
|
||||
|
||||
diff = matchColorDistance(r1, g1, b1, r2, g2, b2);
|
||||
|
||||
if (diff === 0) {
|
||||
li = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (diff < ldiff) {
|
||||
ldiff = diff;
|
||||
li = i;
|
||||
}
|
||||
}
|
||||
|
||||
return matchColorCache[hash] = li;
|
||||
}
|
||||
|
||||
private _visualBell(): boolean {
|
||||
return false;
|
||||
// return this.options.bellStyle === 'visual' ||
|
||||
@@ -1958,3 +1962,19 @@ function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
|
||||
ev.keyCode === 17 || // Ctrl
|
||||
ev.keyCode === 18; // Alt
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* The below color-related code can be removed when true color is implemented.
|
||||
* It's only purpose is to match true color requests with the closest matching
|
||||
* ANSI color code.
|
||||
*/
|
||||
|
||||
const matchColorCache: {[colorRGBHash: number]: number} = {};
|
||||
|
||||
// http://stackoverflow.com/questions/1633828
|
||||
function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
|
||||
return Math.pow(30 * (r1 - r2), 2)
|
||||
+ Math.pow(59 * (g1 - g2), 2)
|
||||
+ Math.pow(11 * (b1 - b2), 2);
|
||||
}
|
||||
|
||||
+25
-108
@@ -5,9 +5,9 @@
|
||||
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ICharset } from './core/Types';
|
||||
import { ICircularList } from './common/Types';
|
||||
import { IEvent } from './common/EventEmitter2';
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
@@ -22,6 +22,12 @@ export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
|
||||
export type BufferIndex = [number, number];
|
||||
|
||||
export const enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
LEAVE = 'linkleave'
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface encapsulates everything needed from the Terminal by the
|
||||
* InputHandler. This cleanly separates the large amount of methods needed by
|
||||
@@ -42,7 +48,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttrData: IAttributeData;
|
||||
curAttr: number;
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -64,7 +70,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
updateRange(y: number): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttrData(): IAttributeData;
|
||||
eraseAttr(): number;
|
||||
is(term: string): boolean;
|
||||
setgCharset(g: number, charset: ICharset): void;
|
||||
resize(x: number, y: number): void;
|
||||
@@ -72,6 +78,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
reset(): void;
|
||||
showCursor(): void;
|
||||
refresh(start: number, end: number): void;
|
||||
matchColor(r1: number, g1: number, b1: number): number;
|
||||
error(text: string, data?: any): void;
|
||||
setOption(key: string, value: any): void;
|
||||
tabSet(): void;
|
||||
@@ -187,7 +194,7 @@ export interface ILinkMatcher {
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifierEvent {
|
||||
export interface ILinkHoverEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
@@ -214,6 +221,10 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
bracketedPasteMode: boolean;
|
||||
applicationCursor: boolean;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
handler(data: string): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
@@ -241,9 +252,6 @@ export interface IMouseHelper {
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
onCharSizeChanged: IEvent<void>;
|
||||
|
||||
measure(options: ITerminalOptions): void;
|
||||
}
|
||||
|
||||
@@ -281,28 +289,24 @@ export interface IBuffer {
|
||||
hasScrollback: boolean;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
savedCurAttrData: IAttributeData;
|
||||
savedCurAttr: number;
|
||||
isCursorInViewport: boolean;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||
getWrappedRangeForLine(y: number): { first: number, last: number };
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
|
||||
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine;
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
|
||||
getNullCell(attr?: IAttributeData): ICellData;
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData;
|
||||
}
|
||||
|
||||
export interface IBufferSet {
|
||||
export interface IBufferSet extends IEventEmitter {
|
||||
alt: IBuffer;
|
||||
normal: IBuffer;
|
||||
active: IBuffer;
|
||||
|
||||
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
|
||||
|
||||
activateNormalBuffer(): void;
|
||||
activateAltBuffer(fillAttr?: IAttributeData): void;
|
||||
activateAltBuffer(fillAttr?: number): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
@@ -317,17 +321,7 @@ export interface ISelectionManager {
|
||||
selectWordAtCursor(event: MouseEvent): void;
|
||||
}
|
||||
|
||||
export interface ISelectionRedrawRequestEvent {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
columnSelectMode: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
@@ -525,52 +519,6 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
clearErrorHandler(): void;
|
||||
}
|
||||
|
||||
/** RGB color type */
|
||||
export type IColorRGB = [number, number, number];
|
||||
|
||||
/** Attribute data */
|
||||
export interface IAttributeData {
|
||||
fg: number;
|
||||
bg: number;
|
||||
|
||||
clone(): IAttributeData;
|
||||
|
||||
// flags
|
||||
isInverse(): number;
|
||||
isBold(): number;
|
||||
isUnderline(): number;
|
||||
isBlink(): number;
|
||||
isInvisible(): number;
|
||||
isItalic(): number;
|
||||
isDim(): number;
|
||||
|
||||
// color modes
|
||||
getFgColorMode(): number;
|
||||
getBgColorMode(): number;
|
||||
isFgRGB(): boolean;
|
||||
isBgRGB(): boolean;
|
||||
isFgPalette(): boolean;
|
||||
isBgPalette(): boolean;
|
||||
isFgDefault(): boolean;
|
||||
isBgDefault(): boolean;
|
||||
|
||||
// colors
|
||||
getFgColor(): number;
|
||||
getBgColor(): number;
|
||||
}
|
||||
|
||||
/** Cell data */
|
||||
export interface ICellData extends IAttributeData {
|
||||
content: number;
|
||||
combinedData: string;
|
||||
isCombined(): number;
|
||||
getWidth(): number;
|
||||
getChars(): string;
|
||||
getCode(): number;
|
||||
setFromCharData(value: CharData): void;
|
||||
getAsCharData(): CharData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a line in the terminal buffer.
|
||||
*/
|
||||
@@ -579,44 +527,13 @@ export interface IBufferLine {
|
||||
isWrapped: boolean;
|
||||
get(index: number): CharData;
|
||||
set(index: number, value: CharData): void;
|
||||
loadCell(index: number, cell: ICellData): ICellData;
|
||||
setCell(index: number, cell: ICellData): void;
|
||||
setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
|
||||
addCodepointToCell(index: number, codePoint: number): void;
|
||||
insertCells(pos: number, n: number, ch: ICellData): void;
|
||||
deleteCells(pos: number, n: number, fill: ICellData): void;
|
||||
replaceCells(start: number, end: number, fill: ICellData): void;
|
||||
resize(cols: number, fill: ICellData): void;
|
||||
fill(fillCellData: ICellData): void;
|
||||
insertCells(pos: number, n: number, ch: CharData): void;
|
||||
deleteCells(pos: number, n: number, fill: CharData): void;
|
||||
replaceCells(start: number, end: number, fill: CharData): void;
|
||||
resize(cols: number, fill: CharData): void;
|
||||
fill(fillCharData: CharData): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
|
||||
|
||||
/* direct access to cell attrs */
|
||||
getWidth(index: number): number;
|
||||
hasWidth(index: number): number;
|
||||
getFg(index: number): number;
|
||||
getBg(index: number): number;
|
||||
hasContent(index: number): number;
|
||||
getCodePoint(index: number): number;
|
||||
isCombined(index: number): number;
|
||||
getString(index: number): string;
|
||||
}
|
||||
|
||||
export interface IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
import { ITerminal } from './Types';
|
||||
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './Buffer';
|
||||
|
||||
export function applyWindowsMode(terminal: ITerminal): IDisposable {
|
||||
// Winpty does not support wraparound mode which means that lines will never
|
||||
// be marked as wrapped. This causes issues for things like copying a line
|
||||
// retaining the wrapped new line characters or if consumers are listening
|
||||
// in on the data stream.
|
||||
//
|
||||
// The workaround for this is to listen to every incoming line feed and mark
|
||||
// the line as wrapped if the last character in the previous line is not a
|
||||
// space. This is certainly not without its problems, but generally on
|
||||
// Windows when text reaches the end of the terminal it's likely going to be
|
||||
// wrapped.
|
||||
return terminal.onLineFeed(() => {
|
||||
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
|
||||
const lastChar = line.get(terminal.cols - 1);
|
||||
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
|
||||
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export interface IAttachAddonTerminal extends Terminal {
|
||||
|
||||
__socket?: WebSocket;
|
||||
__attachSocketBuffer?: string;
|
||||
__dataListener?: IDisposable;
|
||||
|
||||
__getMessage?(ev: MessageEvent): void;
|
||||
__flushBuffer?(): void;
|
||||
|
||||
@@ -90,8 +90,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData);
|
||||
addonTerminal._core.register(addonTerminal.__dataListener);
|
||||
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
|
||||
}
|
||||
|
||||
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
|
||||
@@ -120,8 +119,7 @@ function addSocketListener(socket: WebSocket, type: string, handler: (this: WebS
|
||||
*/
|
||||
export function detach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <IAttachAddonTerminal>term;
|
||||
addonTerminal.__dataListener.dispose();
|
||||
addonTerminal.__dataListener = undefined;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"outDir": "../../../lib/addons/attach/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
@@ -20,8 +19,6 @@ export class SearchHelper implements ISearchHelper {
|
||||
*/
|
||||
private _linesCache: string[] = null;
|
||||
private _linesCacheTimeoutId = 0;
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
constructor(private _terminal: ISearchAddonTerminal) {
|
||||
this._destroyLinesCache = this._destroyLinesCache.bind(this);
|
||||
@@ -58,27 +55,12 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
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.
|
||||
while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) {
|
||||
findingRow--;
|
||||
cumulativeCols += this._terminal.cols;
|
||||
}
|
||||
|
||||
// Search startRow
|
||||
result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
|
||||
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
||||
// Otherwise, reset beginning column index to zero with set new unwrapped line index
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
@@ -89,7 +71,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
// 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++) {
|
||||
for (let y = 0; y <= startRow; y++) {
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
@@ -118,8 +100,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
}
|
||||
|
||||
const isReverseSearch = true;
|
||||
let startRow = this._terminal._core.buffer.ydisp + this._terminal.rows - 1;
|
||||
let startCol = this._terminal.cols;
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
|
||||
|
||||
if (selectionManager.selectionStart) {
|
||||
// Start from the selection start if there is a selection
|
||||
@@ -136,24 +118,11 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
// 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._core.buffer.lines.get(startRow).isWrapped) {
|
||||
cumulativeCols += startCol;
|
||||
}
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
|
||||
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
// If the current line is wrapped line, increase scanning range,
|
||||
// preparing for scanning on unwrapped line
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
cumulativeCols += this._terminal.cols;
|
||||
} else {
|
||||
cumulativeCols = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,17 +130,11 @@ export class SearchHelper implements ISearchHelper {
|
||||
// case startCol > 0)
|
||||
if (!result) {
|
||||
const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
|
||||
let cumulativeCols = this._terminal.cols;
|
||||
for (let y = searchFrom; y >= startRow; y--) {
|
||||
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
|
||||
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
cumulativeCols += this._terminal.cols;
|
||||
} else {
|
||||
cumulativeCols = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +148,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
private _initLinesCache(): void {
|
||||
if (!this._linesCache) {
|
||||
this._linesCache = new Array(this._terminal._core.buffer.length);
|
||||
this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache());
|
||||
this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache());
|
||||
this._terminal.on('cursormove', this._destroyLinesCache);
|
||||
}
|
||||
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
@@ -195,14 +157,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
private _destroyLinesCache(): void {
|
||||
this._linesCache = null;
|
||||
if (this._cursorMoveListener) {
|
||||
this._cursorMoveListener.dispose();
|
||||
this._cursorMoveListener = undefined;
|
||||
}
|
||||
if (this._resizeListener) {
|
||||
this._resizeListener.dispose();
|
||||
this._resizeListener = undefined;
|
||||
}
|
||||
this._terminal.off('cursormove', this._destroyLinesCache);
|
||||
if (this._linesCacheTimeoutId) {
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = 0;
|
||||
@@ -232,11 +187,10 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
|
||||
|
||||
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||
if (this._terminal._core.buffer.lines.get(row).isWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||
if (stringLine === void 0) {
|
||||
stringLine = this.translateBufferLineToStringWithWrap(row, true);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -5,16 +5,11 @@
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface ITerminadoAddonTerminal extends Terminal {
|
||||
_core: {
|
||||
register<T extends IDisposable>(d: T): void;
|
||||
};
|
||||
|
||||
__socket?: WebSocket;
|
||||
__attachSocketBuffer?: string;
|
||||
__dataListener?: IDisposable;
|
||||
|
||||
__getMessage?(ev: MessageEvent): void;
|
||||
__flushBuffer?(): void;
|
||||
|
||||
@@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
|
||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData));
|
||||
addonTerminal.on('data', addonTerminal.__sendData);
|
||||
}
|
||||
addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize));
|
||||
addonTerminal.on('resize', addonTerminal.__setSize);
|
||||
|
||||
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
|
||||
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
|
||||
@@ -75,8 +75,7 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
|
||||
*/
|
||||
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <ITerminadoAddonTerminal>term;
|
||||
addonTerminal.__dataListener.dispose();
|
||||
addonTerminal.__dataListener = undefined;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -28,159 +28,63 @@ describe('webLinks addon', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should allow simple URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
it('should allow ~ character in URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com ';
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io');
|
||||
});
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
});
|
||||
|
||||
describe('should allow ~ character in URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
it('should allow : character in URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
const row = ' http://foo.com/colon:test ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/a~b#c~d?e~f ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/a~b#c~d?e~f');
|
||||
});
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
describe('should allow : character in URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
it('should not allow : character at the end of a URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/colon:test ';
|
||||
const row = ' http://foo.com/colon:test: ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/colon:test ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/colon:test');
|
||||
});
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
describe('should not allow : character at the end of a URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
it('should not allow " character at the end of a URI enclosed with ""', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/colon:test: ';
|
||||
const row = '"http://foo.com/"';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/colon:test: ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/colon:test');
|
||||
});
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
|
||||
describe('should not allow " character at the end of a URI enclosed with ""', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
it('should not allow \' character at the end of a URI enclosed with \'\'', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '"http://foo.com/"';
|
||||
const row = '\'http://foo.com/\'';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '"http://bar.io/"';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not allow \' character at the end of a URI enclosed with \'\'', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '\'http://foo.com/\'';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '\'http://bar.io/\'';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/');
|
||||
});
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
|
||||
const pathClause = '(' + pathCharacterSet + ')?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
}
|
||||
|
||||
export interface IWinptyCompatAddonTerminal extends Terminal {
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "xterm.winptycompat",
|
||||
"main": "winptyCompat.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/winptyCompat/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as winptyCompat from './winptyCompat';
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('winptyCompat addon', () => {
|
||||
describe('apply', () => {
|
||||
it('should do register the `winptyCompatInit` method', () => {
|
||||
winptyCompat.apply(<any>MockTerminal);
|
||||
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
const CHAR_DATA_CODE_INDEX = 3;
|
||||
const NULL_CELL_CODE = 0;
|
||||
const WHITESPACE_CELL_CODE = 32;
|
||||
|
||||
export function winptyCompatInit(terminal: Terminal): void {
|
||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
||||
|
||||
// Don't do anything when the platform is not Windows
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
(addonTerminal._core as any).isWinptyCompatEnabled = true;
|
||||
|
||||
// Winpty does not support wraparound mode which means that lines will never
|
||||
// be marked as wrapped. This causes issues for things like copying a line
|
||||
// retaining the wrapped new line characters or if consumers are listening
|
||||
// in on the data stream.
|
||||
//
|
||||
// The workaround for this is to listen to every incoming line feed and mark
|
||||
// the line as wrapped if the last character in the previous line is not a
|
||||
// space. This is certainly not without its problems, but generally on
|
||||
// Windows when text reaches the end of the terminal it's likely going to be
|
||||
// wrapped.
|
||||
addonTerminal.on('linefeed', () => {
|
||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||
const lastChar = line.get(addonTerminal.cols - 1);
|
||||
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
|
||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
|
||||
winptyCompatInit(this);
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ICircularList } from './Types';
|
||||
import { EventEmitter2, IEvent } from './EventEmitter2';
|
||||
|
||||
export interface IInsertEvent {
|
||||
index: number;
|
||||
@@ -20,21 +20,15 @@ export interface IDeleteEvent {
|
||||
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||
* overriding values at the start of the list.
|
||||
*/
|
||||
export class CircularList<T> implements ICircularList<T> {
|
||||
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
protected _array: (T | undefined)[];
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
public onDeleteEmitter = new EventEmitter2<IDeleteEvent>();
|
||||
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
|
||||
public onInsertEmitter = new EventEmitter2<IInsertEvent>();
|
||||
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
|
||||
public onTrimEmitter = new EventEmitter2<number>();
|
||||
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
|
||||
|
||||
constructor(
|
||||
private _maxLength: number
|
||||
) {
|
||||
super();
|
||||
this._array = new Array<T>(this._maxLength);
|
||||
this._startIndex = 0;
|
||||
this._length = 0;
|
||||
@@ -107,7 +101,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
this._array[this._getCyclicIndex(this._length)] = value;
|
||||
if (this._length === this._maxLength) {
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.onTrimEmitter.fire(1);
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
} else {
|
||||
this._length++;
|
||||
}
|
||||
@@ -123,7 +117,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
throw new Error('Can only recycle when the buffer is full');
|
||||
}
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.onTrimEmitter.fire(1);
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
return this._array[this._getCyclicIndex(this._length - 1)]!;
|
||||
}
|
||||
|
||||
@@ -173,7 +167,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
const countToTrim = (this._length + items.length) - this._maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this._maxLength;
|
||||
this.onTrimEmitter.fire(countToTrim);
|
||||
this.emitMayRemoveListeners('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
@@ -189,7 +183,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
}
|
||||
this._startIndex += count;
|
||||
this._length -= count;
|
||||
this.onTrimEmitter.fire(count);
|
||||
this.emitMayRemoveListeners('trim', count);
|
||||
}
|
||||
|
||||
public shiftElements(start: number, count: number, offset: number): void {
|
||||
@@ -213,7 +207,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
while (this._length > this._maxLength) {
|
||||
this._length--;
|
||||
this._startIndex++;
|
||||
this.onTrimEmitter.fire(1);
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IEventEmitter, XtermListener } from './Types';
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
import { Disposable } from './Lifecycle';
|
||||
|
||||
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { EventEmitter2 } from './EventEmitter2';
|
||||
|
||||
describe('EventEmitter2', () => {
|
||||
it('should fire listeners multiple times', () => {
|
||||
const order: string[] = [];
|
||||
const emitter = new EventEmitter2<number>();
|
||||
emitter.event(data => order.push(data + 'a'));
|
||||
emitter.event(data => order.push(data + 'b'));
|
||||
emitter.fire(1);
|
||||
emitter.fire(2);
|
||||
assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]);
|
||||
});
|
||||
|
||||
it('should not fire listeners once disposed', () => {
|
||||
const order: string[] = [];
|
||||
const emitter = new EventEmitter2<number>();
|
||||
emitter.event(data => order.push(data + 'a'));
|
||||
const disposeB = emitter.event(data => order.push(data + 'b'));
|
||||
emitter.event(data => order.push(data + 'c'));
|
||||
emitter.fire(1);
|
||||
disposeB.dispose();
|
||||
emitter.fire(2);
|
||||
assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]);
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from './Types';
|
||||
|
||||
interface IListener<T> {
|
||||
(e: T): void;
|
||||
}
|
||||
|
||||
export interface IEvent<T> {
|
||||
(listener: (e: T) => any): IDisposable;
|
||||
}
|
||||
|
||||
export class EventEmitter2<T> {
|
||||
private _listeners: IListener<T>[] = [];
|
||||
private _event?: IEvent<T>;
|
||||
|
||||
public get event(): IEvent<T> {
|
||||
if (!this._event) {
|
||||
this._event = (listener: (e: T) => any) => {
|
||||
this._listeners.push(listener);
|
||||
const disposable = {
|
||||
dispose: () => {
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
if (this._listeners[i] === listener) {
|
||||
this._listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return disposable;
|
||||
};
|
||||
}
|
||||
return this._event;
|
||||
}
|
||||
|
||||
public fire(data: T): void {
|
||||
const queue: IListener<T>[] = [];
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
queue.push(this._listeners[i]);
|
||||
}
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
queue[i].call(undefined, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from './Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
|
||||
|
||||
+2
-21
@@ -3,19 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEvent, EventEmitter2 } from './EventEmitter2';
|
||||
import { IDeleteEvent, IInsertEvent } from './CircularList';
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IEventEmitter {
|
||||
on(type: string, listener: (...args: any[]) => void): void;
|
||||
off(type: string, listener: (...args: any[]) => void): void;
|
||||
emit(type: string, data?: any): void;
|
||||
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
||||
}
|
||||
import { IEventEmitter } from 'xterm';
|
||||
|
||||
export type XtermListener = (...args: any[]) => void;
|
||||
|
||||
@@ -33,18 +21,11 @@ export interface IKeyboardEvent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ICircularList<T> {
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
isFull: boolean;
|
||||
|
||||
onDeleteEmitter: EventEmitter2<IDeleteEvent>;
|
||||
onDelete: IEvent<IDeleteEvent>;
|
||||
onInsertEmitter: EventEmitter2<IInsertEvent>;
|
||||
onInsert: IEvent<IInsertEvent>;
|
||||
onTrimEmitter: EventEmitter2<number>;
|
||||
onTrim: IEvent<number>;
|
||||
|
||||
get(index: number): T | undefined;
|
||||
set(index: number, value: T): void;
|
||||
push(value: T): void;
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig-library-base",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../lib",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
"../../node_modules/@types/mocha",
|
||||
"../../"
|
||||
]
|
||||
},
|
||||
"include": [ "./**/*" ]
|
||||
"include": [
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,16 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface INavigator {
|
||||
userAgent: string;
|
||||
language: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but
|
||||
// we want this module to live in common.
|
||||
declare const navigator: INavigator;
|
||||
|
||||
const isNode = (typeof navigator === 'undefined') ? true : false;
|
||||
const userAgent = (isNode) ? 'node' : navigator.userAgent;
|
||||
const platform = (isNode) ? 'node' : navigator.platform;
|
||||
@@ -352,10 +352,6 @@ export function evaluateKeyboardEvent(
|
||||
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) {
|
||||
// Include only keys that that result in a _single_ character; don't include num lock, volume up, etc.
|
||||
result.key = ev.key;
|
||||
} else if (ev.key && ev.ctrlKey) {
|
||||
if (ev.key === '_') { // ^_
|
||||
result.key = C0.US;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"extends": "../tsconfig-library-base",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../lib",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
"../../node_modules/@types/mocha",
|
||||
"../../"
|
||||
]
|
||||
},
|
||||
"include": [ "./**/*" ],
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../common/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
+1
-12
@@ -7,7 +7,6 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILink
|
||||
import { ITerminal } from '../Types';
|
||||
import { Terminal as TerminalCore } from '../Terminal';
|
||||
import * as Strings from '../Strings';
|
||||
import { IEvent } from '../common/EventEmitter2';
|
||||
|
||||
export class Terminal implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
@@ -16,21 +15,11 @@ export class Terminal implements ITerminalApi {
|
||||
this._core = new TerminalCore(options);
|
||||
}
|
||||
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
|
||||
public get element(): HTMLElement { return this._core.element; }
|
||||
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get markers(): ReadonlyArray<IMarker> { return this._core.markers; }
|
||||
public get markers(): IMarker[] { return this._core.markers; }
|
||||
public blur(): void {
|
||||
this._core.blur();
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
*/
|
||||
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
||||
import { ITerminal, ICellData } from '../Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier, DEFAULT_COLOR } from './atlas/Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
|
||||
import BaseCharAtlas from './atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { CellData, AttributeData } from '../BufferLine';
|
||||
import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
|
||||
import { JoinedCellData } from './CharacterJoinerRegistry';
|
||||
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -230,17 +229,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* 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 charData The char 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 {
|
||||
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
charData[CHAR_DATA_CHAR_INDEX],
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
}
|
||||
@@ -259,37 +258,17 @@ 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 {
|
||||
|
||||
// skip cache right away if we draw in RGB
|
||||
// Note: to avoid bad runtime JoinedCellData will be skipped
|
||||
// in the cache handler (atlasDidDraw == false) itself and
|
||||
// fall through to uncached later down below
|
||||
if (cell.isFgRGB() || cell.isBgRGB() || cell instanceof JoinedCellData) {
|
||||
this._drawUncachedChars(terminal, cell, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
let fg;
|
||||
let bg;
|
||||
if (cell.isInverse()) {
|
||||
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
|
||||
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
|
||||
} else {
|
||||
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
|
||||
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
|
||||
}
|
||||
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
|
||||
this._currentGlyphIdentifier.chars = chars;
|
||||
this._currentGlyphIdentifier.code = code;
|
||||
this._currentGlyphIdentifier.bg = bg;
|
||||
this._currentGlyphIdentifier.fg = fg;
|
||||
this._currentGlyphIdentifier.bold = cell.isBold() && terminal.options.enableBold;
|
||||
this._currentGlyphIdentifier.dim = !!cell.isDim();
|
||||
this._currentGlyphIdentifier.italic = !!cell.isItalic();
|
||||
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
|
||||
this._currentGlyphIdentifier.dim = dim;
|
||||
this._currentGlyphIdentifier.italic = italic;
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
this._currentGlyphIdentifier,
|
||||
@@ -298,7 +277,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(terminal, cell, x, y);
|
||||
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,42 +292,29 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @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(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, cell.isBold() && terminal.options.enableBold, !!cell.isItalic());
|
||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (is256Color(fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
} else {
|
||||
if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
this._clipRow(terminal, y);
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (cell.isDim()) {
|
||||
if (dim) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
chars,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
this._ctx.restore();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { MockTerminal, MockBuffer } from '../TestUtils.test';
|
||||
import { MockTerminal, MockBuffer } from '../ui/TestUtils.test';
|
||||
import { CircularList } from '../common/CircularList';
|
||||
|
||||
import { ICharacterJoinerRegistry } from './Types';
|
||||
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
||||
import { BufferLine, CellData } from '../BufferLine';
|
||||
import { BufferLine } from '../BufferLine';
|
||||
import { IBufferLine } from '../Types';
|
||||
|
||||
describe('CharacterJoinerRegistry', () => {
|
||||
@@ -24,18 +24,18 @@ describe('CharacterJoinerRegistry', () => {
|
||||
lines.set(4, new BufferLine(0));
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, null]));
|
||||
line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]);
|
||||
line6.resize(line6.length + 1, [0, '', 0, null]);
|
||||
let sub = lineData([['deemo']]);
|
||||
let oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)]));
|
||||
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
|
||||
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i));
|
||||
line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]);
|
||||
line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]);
|
||||
sub = lineData([['jiabc']]);
|
||||
oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
|
||||
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i));
|
||||
lines.set(6, line6);
|
||||
|
||||
(<MockBuffer>terminal.buffer).setLines(lines);
|
||||
@@ -273,8 +273,8 @@ function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const line = data[i][0];
|
||||
const attr = <number>(data[i][1] || 0);
|
||||
const offset = tline.length;
|
||||
tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)])));
|
||||
tline.resize(tline.length + line.split('').length, [0, '', 0, 0]);
|
||||
line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)]));
|
||||
}
|
||||
return tline;
|
||||
}
|
||||
|
||||
@@ -1,58 +1,11 @@
|
||||
import { ITerminal, IBufferLine, ICellData, CharData } from '../Types';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { ITerminal, IBufferLine } from '../Types';
|
||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||
import { CellData, Content, AttributeData } from '../BufferLine';
|
||||
import { WHITESPACE_CELL_CHAR } from '../Buffer';
|
||||
|
||||
export class JoinedCellData extends AttributeData implements ICellData {
|
||||
private _width: number;
|
||||
// .content carries no meaning for joined CellData, simply nullify it
|
||||
// thus we have to overload all other .content accessors
|
||||
public content: number = 0;
|
||||
public fg: number;
|
||||
public bg: number;
|
||||
public combinedData: string = '';
|
||||
|
||||
constructor(firstCell: ICellData, chars: string, width: number) {
|
||||
super();
|
||||
this.fg = firstCell.fg;
|
||||
this.bg = firstCell.bg;
|
||||
this.combinedData = chars;
|
||||
this._width = width;
|
||||
}
|
||||
|
||||
public isCombined(): number {
|
||||
// always mark joined cell data as combined
|
||||
return Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public getChars(): string {
|
||||
return this.combinedData;
|
||||
}
|
||||
|
||||
public getCode(): number {
|
||||
// code always gets the highest possible fake codepoint (read as -1)
|
||||
// this is needed as code is used by caches as identifier
|
||||
return 0x1FFFFF;
|
||||
}
|
||||
|
||||
public setFromCharData(value: CharData): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
}
|
||||
|
||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
|
||||
private _characterJoiners: ICharacterJoiner[] = [];
|
||||
private _nextCharacterJoinerId: number = 0;
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
}
|
||||
@@ -89,7 +42,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
}
|
||||
|
||||
const ranges: [number, number][] = [];
|
||||
const lineStr = line.translateToString(true);
|
||||
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
|
||||
|
||||
// Because some cells can be represented by multiple javascript characters,
|
||||
// we track the cell and the string indexes separately. This allows us to
|
||||
@@ -98,19 +51,21 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
let rangeStartColumn = 0;
|
||||
let currentStringIndex = 0;
|
||||
let rangeStartStringIndex = 0;
|
||||
let rangeAttrFG = line.getFg(0);
|
||||
let rangeAttrBG = line.getBg(0);
|
||||
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
for (let x = 0; x < line.getTrimmedLength(); x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
for (let x = 0; x < this._terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
if (width === 0) {
|
||||
// If this character is of width 0, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of range
|
||||
if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) {
|
||||
if (attr !== rangeAttr) {
|
||||
// If we ended up with a sequence of more than one character,
|
||||
// look for ranges to join.
|
||||
if (x - rangeStartColumn > 1) {
|
||||
@@ -129,11 +84,10 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
// Reset our markers for a new range.
|
||||
rangeStartColumn = x;
|
||||
rangeStartStringIndex = currentStringIndex;
|
||||
rangeAttrFG = this._workCell.fg;
|
||||
rangeAttrBG = this._workCell.bg;
|
||||
rangeAttr = attr;
|
||||
}
|
||||
|
||||
currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length;
|
||||
currentStringIndex += chars.length;
|
||||
}
|
||||
|
||||
// Process any trailing ranges.
|
||||
@@ -198,8 +152,9 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
}
|
||||
|
||||
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||
const width = line.getWidth(x);
|
||||
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
|
||||
const charData = line.get(x);
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||
|
||||
// We skip zero-width characters when creating the string to join the text
|
||||
// so we do the same here
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ITerminal, ICellData } from '../Types';
|
||||
import { CellData } from '../BufferLine';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -23,9 +23,8 @@ const BLINK_INTERVAL = 600;
|
||||
|
||||
export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _state: ICursorState;
|
||||
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, cell: ICellData) => void};
|
||||
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
|
||||
private _cursorBlinkStateManager: CursorBlinkStateManager;
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
@@ -128,8 +127,8 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.buffer.lines.get(cursorY).loadCell(terminal.buffer.x, this._cell);
|
||||
if (this._cell.content === undefined) {
|
||||
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
|
||||
if (!charData) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,13 +136,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell);
|
||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
this._state.x = terminal.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.options.cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,21 +158,21 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === terminal.isFocused &&
|
||||
this._state.style === terminal.options.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) {
|
||||
return;
|
||||
}
|
||||
this._clearCursor();
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell);
|
||||
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = terminal.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.options.cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
@@ -189,33 +188,33 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBarCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void {
|
||||
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this.fillLeftLineAtCell(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void {
|
||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this.fillCells(x, y, cell.getWidth(), 1);
|
||||
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this.fillCharTrueColor(terminal, cell, x, y);
|
||||
this.fillCharTrueColor(terminal, charData, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void {
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void {
|
||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this.strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ITerminal, ILinkifierAccessor } from '../Types';
|
||||
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent = null;
|
||||
private _state: ILinkHoverEvent = null;
|
||||
|
||||
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));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
@@ -40,7 +40,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (is256Color(e.fg)) {
|
||||
@@ -64,7 +64,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
private _onLinkLeave(e: ILinkHoverEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ import { ColorManager } from './ColorManager';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../common/EventEmitter';
|
||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||
import { ITheme } from 'xterm';
|
||||
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
|
||||
import { EventEmitter2, IEvent } from '../common/EventEmitter2';
|
||||
import { Disposable } from '../common/Lifecycle';
|
||||
|
||||
export class Renderer extends Disposable implements IRenderer {
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -30,11 +29,6 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>();
|
||||
public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; }
|
||||
private _onRender = new EventEmitter2<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
|
||||
constructor(private _terminal: ITerminal, theme: ITheme) {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
@@ -68,7 +62,7 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
this._updateDimensions();
|
||||
this.onOptionsChanged();
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
|
||||
this.register(this._screenDprMonitor);
|
||||
@@ -91,7 +85,6 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
this._isPaused = entry.intersectionRatio === 0;
|
||||
if (!this._isPaused && this._needsFullRefresh) {
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
this._needsFullRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +133,7 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this._onCanvasResize.fire({
|
||||
this.emit('resize', {
|
||||
width: this.dimensions.canvasWidth,
|
||||
height: this.dimensions.canvasHeight
|
||||
});
|
||||
@@ -194,7 +187,7 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
this._needsFullRefresh = true;
|
||||
return;
|
||||
}
|
||||
this._renderDebouncer.refresh(start, end, this._terminal.rows);
|
||||
this._renderDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +196,7 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
*/
|
||||
private _renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
this._onRender.fire({ start, end });
|
||||
this._terminal.emit('refresh', { start, end });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { NULL_CELL_CODE } from '../Buffer';
|
||||
import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal, ICellData } from '../Types';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { CellData, AttributeData, Content } from '../BufferLine';
|
||||
import { JoinedCellData } from './CharacterJoinerRegistry';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -24,7 +24,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
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);
|
||||
@@ -58,9 +57,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
cell: ICellData,
|
||||
code: number,
|
||||
chars: string,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
fg: number,
|
||||
bg: number,
|
||||
flags: number
|
||||
) => void
|
||||
): void {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
@@ -68,8 +72,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
let cell = this._workCell;
|
||||
const charData = line.get(x);
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE;
|
||||
|
||||
// Can either represent character(s) for a single cell or multiple cells
|
||||
// if indicated by a character joiner.
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
@@ -77,7 +87,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by
|
||||
// the char at x-1
|
||||
if (cell.getWidth() === 0) {
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -90,12 +100,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// 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]),
|
||||
range[1] - range[0]
|
||||
chars = terminal.buffer.translateBufferLineToString(
|
||||
row,
|
||||
true,
|
||||
range[0],
|
||||
range[1]
|
||||
);
|
||||
width = range[1] - range[0];
|
||||
code = Infinity;
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
@@ -105,7 +117,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// right is a space, take ownership of the cell to the right. We skip
|
||||
// this check for joined characters because their rendering likely won't
|
||||
// yield the same result as rendering the last character individually.
|
||||
if (!isJoined && this._isOverlapping(cell)) {
|
||||
if (!isJoined && this._isOverlapping(charData)) {
|
||||
// If the character is overlapping, we want to force a re-render on every
|
||||
// frame. This is specifically to work around the case where two
|
||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||
@@ -113,10 +125,8 @@ 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) {
|
||||
// patch width to 2
|
||||
cell.content &= ~Content.WIDTH_MASK;
|
||||
cell.content |= 2 << Content.WIDTH_SHIFT;
|
||||
if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
||||
width = 2;
|
||||
// this._clearChar(x + 1, y);
|
||||
// The overlapping char's char data will force a clear and render when the
|
||||
// overlapping char is no longer to the left of the character and also when
|
||||
@@ -125,10 +135,32 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
const flags = attr >> 18;
|
||||
let bg = attr & 0x1ff;
|
||||
let fg = (attr >> 9) & 0x1ff;
|
||||
|
||||
// If inverse flag is on, the foreground should become the background.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
const temp = bg;
|
||||
bg = fg;
|
||||
fg = temp;
|
||||
if (fg === DEFAULT_COLOR) {
|
||||
fg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
if (bg === DEFAULT_COLOR) {
|
||||
bg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
callback(
|
||||
cell,
|
||||
code,
|
||||
chars,
|
||||
width,
|
||||
x,
|
||||
y
|
||||
y,
|
||||
fg,
|
||||
bg,
|
||||
flags
|
||||
);
|
||||
|
||||
x = lastCharX;
|
||||
@@ -149,23 +181,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (cell, x, y) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
// 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
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isFgDefault()) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
|
||||
}
|
||||
} else if (cell.isBgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else if (cell.isBgPalette()) {
|
||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
if (bg === INVERTED_DEFAULT_COLOR) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
} else if (is256Color(bg)) {
|
||||
nextFillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
@@ -200,39 +223,29 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
if (flags & FLAGS.INVISIBLE) {
|
||||
return;
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
if (flags & FLAGS.UNDERLINE) {
|
||||
this._ctx.save();
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (is256Color(fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
} else {
|
||||
if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
this.fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
this.fillBottomLineAtCells(x, y, width);
|
||||
this._ctx.restore();
|
||||
}
|
||||
this.drawChars(terminal, cell, x, y);
|
||||
this.drawChars(
|
||||
terminal, chars, code,
|
||||
width, x, y,
|
||||
fg, bg,
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,23 +271,23 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
/**
|
||||
* Whether a character is overlapping to the next cell.
|
||||
*/
|
||||
private _isOverlapping(cell: ICellData): boolean {
|
||||
private _isOverlapping(charData: CharData): boolean {
|
||||
// Only single cell characters can be overlapping, rendering issues can
|
||||
// occur without this check
|
||||
if (cell.getWidth() !== 1) {
|
||||
if (charData[CHAR_DATA_WIDTH_INDEX] !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We assume that any ascii character will not overlap
|
||||
if (cell.getCode() < 256) {
|
||||
const code = charData[CHAR_DATA_CODE_INDEX];
|
||||
if (code < 256) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
|
||||
// Deliver from cache if available
|
||||
if (this._characterOverlapCache.hasOwnProperty(chars)) {
|
||||
return this._characterOverlapCache[chars];
|
||||
const char = charData[CHAR_DATA_CHAR_INDEX];
|
||||
if (this._characterOverlapCache.hasOwnProperty(char)) {
|
||||
return this._characterOverlapCache[char];
|
||||
}
|
||||
|
||||
// Setup the font
|
||||
@@ -284,13 +297,13 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// Measure the width of the character, but Math.floor it
|
||||
// because that is what the renderer does when it calculates
|
||||
// the character dimensions we are comparing against
|
||||
const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
|
||||
const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth;
|
||||
|
||||
// Restore the original context
|
||||
this._ctx.restore();
|
||||
|
||||
// Cache and return
|
||||
this._characterOverlapCache[chars] = overlaps;
|
||||
this._characterOverlapCache[char] = overlaps;
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { ITheme, IDisposable } from 'xterm';
|
||||
import { IEvent } from '../common/EventEmitter2';
|
||||
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Flags used to render terminal text properly.
|
||||
@@ -24,13 +23,10 @@ export const enum FLAGS {
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
export interface IRenderer extends IEventEmitter, IDisposable {
|
||||
dimensions: IRenderDimensions;
|
||||
colorManager: IColorManager;
|
||||
|
||||
onCanvasResize: IEvent<{ width: number, height: number }>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
|
||||
dispose(): void;
|
||||
setTheme(theme: ITheme): IColorSet;
|
||||
onWindowResize(devicePixelRatio: number): void;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'xterm';
|
||||
import { isFirefox, isSafari } from '../../common/Platform';
|
||||
import { isFirefox, isSafari } from '../../core/Platform';
|
||||
import { IColor } from '../Types';
|
||||
import { ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import BaseCharAtlas from './BaseCharAtlas';
|
||||
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
|
||||
import { clearColor } from './CharAtlasGenerator';
|
||||
import LRUMap from './LRUMap';
|
||||
import { isFirefox, isSafari } from '../../common/Platform';
|
||||
import { isFirefox, isSafari } from '../../core/Platform';
|
||||
import { IColor } from '../Types';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
||||
import { ILinkifierEvent, ITerminal, CharacterJoinerHandler } from '../../Types';
|
||||
import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types';
|
||||
import { ITheme } from 'xterm';
|
||||
import { EventEmitter } from '../../common/EventEmitter';
|
||||
import { ColorManager } from '../ColorManager';
|
||||
import { RenderDebouncer } from '../../ui/RenderDebouncer';
|
||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
|
||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
|
||||
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
|
||||
import { EventEmitter2, IEvent } from '../../common/EventEmitter2';
|
||||
import { Disposable } from '../../common/Lifecycle';
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||
@@ -30,7 +29,7 @@ let nextTerminalId = 1;
|
||||
* particularly fast or feature complete, more just stable and usable for when
|
||||
* canvas is not an option.
|
||||
*/
|
||||
export class DomRenderer extends Disposable implements IRenderer {
|
||||
export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
private _rowFactory: DomRendererRowFactory;
|
||||
private _terminalClass: number = nextTerminalId++;
|
||||
@@ -44,11 +43,6 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
public dimensions: IRenderDimensions;
|
||||
public colorManager: ColorManager;
|
||||
|
||||
private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>();
|
||||
public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; }
|
||||
private _onRender = new EventEmitter2<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
|
||||
constructor(private _terminal: ITerminal, theme: ITheme | undefined) {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
@@ -80,15 +74,15 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
};
|
||||
this._updateDimensions();
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._rowFactory = new DomRendererRowFactory(_terminal.options, document);
|
||||
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._rowFactory = new DomRendererRowFactory(document);
|
||||
|
||||
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._terminal.screenElement.appendChild(this._rowContainer);
|
||||
this._terminal.screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
this._terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
|
||||
this._terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -171,22 +165,12 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
|
||||
` font-style: italic;` +
|
||||
`}`;
|
||||
// Blink animation
|
||||
styles +=
|
||||
`@keyframes blink {` +
|
||||
` 0% { opacity: 1.0; }` +
|
||||
` 50% { opacity: 0.0; }` +
|
||||
` 100% { opacity: 1.0; }` +
|
||||
`}`;
|
||||
// Cursor
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
|
||||
` outline: 1px solid ${this.colorManager.colors.cursor.css};` +
|
||||
` outline-offset: -1px;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` +
|
||||
` animation: blink 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` background-color: ${this.colorManager.colors.cursor.css};` +
|
||||
` color: ${this.colorManager.colors.cursorAccent.css};` +
|
||||
@@ -244,10 +228,6 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
public onResize(cols: number, rows: number): void {
|
||||
this._refreshRowElements(cols, rows);
|
||||
this._updateDimensions();
|
||||
this._onCanvasResize.fire({
|
||||
width: this.dimensions.canvasWidth,
|
||||
height: this.dimensions.canvasHeight
|
||||
});
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
@@ -340,7 +320,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public refreshRows(start: number, end: number): void {
|
||||
this._renderDebouncer.refresh(start, end, this._terminal.rows);
|
||||
this._renderDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
@@ -348,7 +328,6 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
|
||||
const cursorX = this._terminal.buffer.x;
|
||||
const cursorBlink = this._terminal.options.cursorBlink;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const rowElement = this._rowElements[y];
|
||||
@@ -357,10 +336,10 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const lineData = terminal.buffer.lines.get(row);
|
||||
const cursorStyle = terminal.options.cursorStyle;
|
||||
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols));
|
||||
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols));
|
||||
}
|
||||
|
||||
this._onRender.fire({ start, end });
|
||||
this._terminal.emit('refresh', {start, end});
|
||||
}
|
||||
|
||||
private get _terminalSelector(): string {
|
||||
@@ -370,11 +349,11 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
private _onLinkLeave(e: ILinkHoverEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,39 +6,36 @@
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR_DATA } from '../../Buffer';
|
||||
import { BufferLine, CellData, FgFlags, BgFlags, Attributes } from '../../BufferLine';
|
||||
import { IBufferLine, ITerminalOptions } from '../../Types';
|
||||
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
|
||||
import { FLAGS } from '../Types';
|
||||
import { BufferLine } from '../../BufferLine';
|
||||
import { IBufferLine } from '../../Types';
|
||||
import { DEFAULT_COLOR } from '../atlas/Types';
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
const options: ITerminalOptions = {};
|
||||
let rowFactory: DomRendererRowFactory;
|
||||
let lineData: IBufferLine;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
|
||||
options.enableBold = true;
|
||||
options.drawBoldTextInBrightColors = true;
|
||||
|
||||
rowFactory = new DomRendererRowFactory(options, dom.window.document);
|
||||
rowFactory = new DomRendererRowFactory(dom.window.document);
|
||||
lineData = createEmptyLineData(2);
|
||||
});
|
||||
|
||||
describe('createRow', () => {
|
||||
it('should not create anything for an empty row', () => {
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('should set correct attributes for double width characters', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
|
||||
lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]);
|
||||
// There should be no element for the following "empty" cell
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, undefined]));
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="width: 10px;">語</span>'
|
||||
);
|
||||
@@ -46,24 +43,17 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should add class for cursor and cursor style', () => {
|
||||
for (const style of ['block', 'bar', 'underline']) {
|
||||
const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should add class for cursor blink', () => {
|
||||
const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render cells that go beyond the terminal\'s columns', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1);
|
||||
lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span>'
|
||||
);
|
||||
@@ -71,53 +61,26 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('attributes', () => {
|
||||
it('should add class for bold', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bold">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add class for italic', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-italic">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add class for dim', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-dim">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add class for underline', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add classes for 256 foreground colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_P256;
|
||||
const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-fg-${i}">a</span>`
|
||||
);
|
||||
@@ -125,13 +88,10 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add classes for 256 background colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg |= Attributes.CM_P256;
|
||||
const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
cell.bg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.bg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bg-${i}">a</span>`
|
||||
);
|
||||
@@ -139,72 +99,38 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
|
||||
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly invert default fg color', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
|
||||
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly invert default bg color', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
|
||||
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should turn bold fg text bright', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= FgFlags.BOLD | Attributes.CM_P16;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set style attribute for RBG', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="color:rgb(1,2,3);background-color:rgb(4,5,6);">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly invert RGB colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="background-color:rgb(1,2,3);color:rgb(4,5,6);">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,7 +143,7 @@ describe('DomRendererRowFactory', () => {
|
||||
function createEmptyLineData(cols: number): IBufferLine {
|
||||
const lineData = new BufferLine(cols);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]));
|
||||
lineData.set(i, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
return lineData;
|
||||
}
|
||||
|
||||
@@ -3,31 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
|
||||
import { IBufferLine, ITerminalOptions } from '../../Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
|
||||
import { CellData, AttributeData } from '../../BufferLine';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
|
||||
import { FLAGS } from '../Types';
|
||||
import { IBufferLine } from '../../Types';
|
||||
import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
export const ITALIC_CLASS = 'xterm-italic';
|
||||
export const UNDERLINE_CLASS = 'xterm-underline';
|
||||
export const CURSOR_CLASS = 'xterm-cursor';
|
||||
export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
|
||||
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
|
||||
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
|
||||
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||
|
||||
export class DomRendererRowFactory {
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
constructor(
|
||||
private _terminalOptions: ITerminalOptions,
|
||||
private _document: Document
|
||||
) {
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
|
||||
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
// Find the line length first, this prevents the need to output a bunch of
|
||||
@@ -37,15 +31,19 @@ export class DomRendererRowFactory {
|
||||
// the viewport).
|
||||
let lineLength = 0;
|
||||
for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) {
|
||||
if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
|
||||
const charData = lineData.get(x);
|
||||
const code = charData[CHAR_DATA_CODE_INDEX];
|
||||
if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
|
||||
lineLength = x + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
lineData.loadCell(x, this._workCell);
|
||||
const width = this._workCell.getWidth();
|
||||
const charData = lineData.get(x);
|
||||
const char = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by the char at x-1
|
||||
if (width === 0) {
|
||||
@@ -57,13 +55,13 @@ export class DomRendererRowFactory {
|
||||
charElement.style.width = `${cellWidth * width}px`;
|
||||
}
|
||||
|
||||
const flags = attr >> 18;
|
||||
let bg = attr & 0x1ff;
|
||||
let fg = (attr >> 9) & 0x1ff;
|
||||
|
||||
if (isCursorRow && x === cursorX) {
|
||||
charElement.classList.add(CURSOR_CLASS);
|
||||
|
||||
if (cursorBlink) {
|
||||
charElement.classList.add(CURSOR_BLINK_CLASS);
|
||||
}
|
||||
|
||||
switch (cursorStyle) {
|
||||
case 'bar':
|
||||
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
|
||||
@@ -77,53 +75,39 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._workCell.isBold() && this._terminalOptions.enableBold) {
|
||||
// If inverse flag is on, the foreground should become the background.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
const temp = bg;
|
||||
bg = fg;
|
||||
fg = temp;
|
||||
if (fg === DEFAULT_COLOR) {
|
||||
fg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
if (bg === DEFAULT_COLOR) {
|
||||
bg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & FLAGS.BOLD) {
|
||||
// Convert the FG color to the bold variant. This should not happen when
|
||||
// the fg is the inverse default color as there is no bold variant.
|
||||
if (fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
charElement.classList.add(BOLD_CLASS);
|
||||
}
|
||||
|
||||
if (this._workCell.isItalic()) {
|
||||
if (flags & FLAGS.ITALIC) {
|
||||
charElement.classList.add(ITALIC_CLASS);
|
||||
}
|
||||
|
||||
if (this._workCell.isDim()) {
|
||||
charElement.classList.add(DIM_CLASS);
|
||||
charElement.textContent = char;
|
||||
if (fg !== DEFAULT_COLOR) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
}
|
||||
|
||||
if (this._workCell.isUnderline()) {
|
||||
charElement.classList.add(UNDERLINE_CLASS);
|
||||
if (bg !== DEFAULT_COLOR) {
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
}
|
||||
|
||||
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
|
||||
const swapColor = this._workCell.isInverse();
|
||||
|
||||
// fg
|
||||
if (this._workCell.isFgRGB()) {
|
||||
let style = charElement.getAttribute('style') || '';
|
||||
style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`;
|
||||
charElement.setAttribute('style', style);
|
||||
} else if (this._workCell.isFgPalette()) {
|
||||
let fg = this._workCell.getFgColor();
|
||||
if (this._workCell.isBold() && fg < 8 && !swapColor &&
|
||||
this._terminalOptions.enableBold && this._terminalOptions.drawBoldTextInBrightColors) {
|
||||
fg += 8;
|
||||
}
|
||||
charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`);
|
||||
} else if (swapColor) {
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
|
||||
// bg
|
||||
if (this._workCell.isBgRGB()) {
|
||||
let style = charElement.getAttribute('style') || '';
|
||||
style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`;
|
||||
charElement.setAttribute('style', style);
|
||||
} else if (this._workCell.isBgPalette()) {
|
||||
charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`);
|
||||
} else if (swapColor) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
}
|
||||
return fragment;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [ "es5" ],
|
||||
"rootDir": ".",
|
||||
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"pretty": true,
|
||||
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "." },
|
||||
{ "path": "./addons/attach" },
|
||||
{ "path": "./addons/fit" },
|
||||
{ "path": "./addons/fullscreen" },
|
||||
{ "path": "./addons/search" },
|
||||
{ "path": "./addons/terminado" },
|
||||
{ "path": "./addons/webLinks" },
|
||||
{ "path": "./addons/zmodem" }
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { ICharMeasure } from './Types';
|
||||
import { ICharMeasure } from '../Types';
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
@@ -3,25 +3,23 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure, ITerminalOptions } from './Types';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { ICharMeasure, ITerminalOptions } from '../Types';
|
||||
import { EventEmitter } from '../common/EventEmitter';
|
||||
|
||||
/**
|
||||
* Utility class that measures the size of a character. Measurements are done in
|
||||
* the DOM rather than with a canvas context because support for extracting the
|
||||
* height of characters is patchy across browsers.
|
||||
*/
|
||||
export class CharMeasure implements ICharMeasure {
|
||||
export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
private _document: Document;
|
||||
private _parentElement: HTMLElement;
|
||||
private _measureElement: HTMLElement;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
private _onCharSizeChanged = new EventEmitter2<void>();
|
||||
public get onCharSizeChanged(): IEvent<void> { return this._onCharSizeChanged.event; }
|
||||
|
||||
constructor(document: Document, parentElement: HTMLElement) {
|
||||
super();
|
||||
this._document = document;
|
||||
this._parentElement = parentElement;
|
||||
this._measureElement = this._document.createElement('span');
|
||||
@@ -52,7 +50,7 @@ export class CharMeasure implements ICharMeasure {
|
||||
if (this._width !== geometry.width || this._height !== adjustedHeight) {
|
||||
this._width = geometry.width;
|
||||
this._height = adjustedHeight;
|
||||
this._onCharSizeChanged.fire();
|
||||
this.emit('charsizechanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager } from './Types';
|
||||
import { ITerminal, ISelectionManager } from '../Types';
|
||||
|
||||
interface IWindow extends Window {
|
||||
clipboardData?: {
|
||||
+3
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '../common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Adds a disposable listener to a node in the DOM, returning the disposable.
|
||||
@@ -24,6 +24,8 @@ export function addDisposableDomListener(
|
||||
return;
|
||||
}
|
||||
node.removeEventListener(type, handler, useCapture);
|
||||
node = null;
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure, IMouseHelper } from './Types';
|
||||
import { IRenderer } from './renderer/Types';
|
||||
import { ICharMeasure, IMouseHelper } from '../Types';
|
||||
import { IRenderer } from '../renderer/Types';
|
||||
|
||||
export class MouseHelper implements IMouseHelper {
|
||||
constructor(private _renderer: IRenderer) {}
|
||||
@@ -3,9 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { Disposable } from '../common/Lifecycle';
|
||||
import { addDisposableDomListener } from './Lifecycle';
|
||||
|
||||
const HOVER_DURATION = 500;
|
||||
|
||||
@@ -28,7 +29,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _tooltipTimeout: number = null;
|
||||
private _currentZone: IMouseZone = null;
|
||||
private _lastHoverCoords: [number, number] = [null, null];
|
||||
private _initialSelectionLength: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
@@ -157,10 +157,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
// Store current terminal selection length, to check if we're performing
|
||||
// a selection operation
|
||||
this._initialSelectionLength = this._terminal.getSelection().length;
|
||||
|
||||
// Ignore the event if there are no zones active
|
||||
if (!this._areZonesActive) {
|
||||
return;
|
||||
@@ -190,12 +186,9 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
private _onClick(e: MouseEvent): void {
|
||||
// Find the active zone and click it if found and no selection was
|
||||
// being performed
|
||||
// Find the active zone and click it if found
|
||||
const zone = this._findZoneEventAt(e);
|
||||
const currentSelectionLength = this._terminal.getSelection().length;
|
||||
|
||||
if (zone && currentSelectionLength === this._initialSelectionLength) {
|
||||
if (zone) {
|
||||
zone.clickCallback(e);
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
+21
-28
@@ -1,39 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '../common/Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IDisposable {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
private _animationFrame: number | undefined;
|
||||
private _rowStart: number;
|
||||
private _rowEnd: number;
|
||||
private _animationFrame: number = null;
|
||||
|
||||
constructor(
|
||||
private _renderCallback: (start: number, end: number) => void
|
||||
private _terminal: ITerminal,
|
||||
private _callback: (start: number, end: number) => void
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
this._animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart: number, rowEnd: number, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
public refresh(rowStart: number, rowEnd: number): void {
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||
rowStart = rowStart !== null && rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== null && rowEnd !== undefined ? rowEnd : this._terminal.rows - 1;
|
||||
// Check whether the row start/end values have already been set
|
||||
const isRowStartSet = this._rowStart !== undefined && this._rowStart !== null;
|
||||
const isRowEndSet = this._rowEnd !== undefined && this._rowEnd !== null;
|
||||
// Set the properties to the updated values
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
this._rowStart = isRowStartSet ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = isRowEndSet ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
if (this._animationFrame) {
|
||||
return;
|
||||
@@ -43,21 +41,16 @@ export class RenderDebouncer implements IDisposable {
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
// Make sure values are set
|
||||
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp values
|
||||
this._rowStart = Math.max(this._rowStart, 0);
|
||||
this._rowEnd = Math.min(this._rowEnd, this._rowCount - 1);
|
||||
this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1);
|
||||
|
||||
// Run render callback
|
||||
this._renderCallback(this._rowStart, this._rowEnd);
|
||||
this._callback(this._rowStart, this._rowEnd);
|
||||
|
||||
// Reset debouncer
|
||||
this._rowStart = undefined;
|
||||
this._rowEnd = undefined;
|
||||
this._animationFrame = undefined;
|
||||
this._rowStart = null;
|
||||
this._rowEnd = null;
|
||||
this._animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-17
@@ -18,10 +18,10 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat
|
||||
* monitor with a different DPI.
|
||||
*/
|
||||
export class ScreenDprMonitor extends Disposable {
|
||||
private _currentDevicePixelRatio: number = window.devicePixelRatio;
|
||||
private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined;
|
||||
private _listener: ScreenDprListener | undefined;
|
||||
private _resolutionMediaMatchList: MediaQueryList | undefined;
|
||||
private _currentDevicePixelRatio: number;
|
||||
private _outerListener: (this: MediaQueryList, ev: MediaQueryListEvent) => any;
|
||||
private _listener: ScreenDprListener;
|
||||
private _resolutionMediaMatchList: MediaQueryList;
|
||||
|
||||
public setListener(listener: ScreenDprListener): void {
|
||||
if (this._listener) {
|
||||
@@ -29,9 +29,6 @@ export class ScreenDprMonitor extends Disposable {
|
||||
}
|
||||
this._listener = listener;
|
||||
this._outerListener = () => {
|
||||
if (!this._listener) {
|
||||
return;
|
||||
}
|
||||
this._listener(window.devicePixelRatio, this._currentDevicePixelRatio);
|
||||
this._updateDpr();
|
||||
};
|
||||
@@ -44,13 +41,10 @@ export class ScreenDprMonitor extends Disposable {
|
||||
}
|
||||
|
||||
private _updateDpr(): void {
|
||||
if (!this._resolutionMediaMatchList || !this._outerListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear listeners for old DPR
|
||||
this._resolutionMediaMatchList.removeListener(this._outerListener);
|
||||
|
||||
if (this._resolutionMediaMatchList) {
|
||||
this._resolutionMediaMatchList.removeListener(this._outerListener);
|
||||
}
|
||||
// Add listeners for new DPR
|
||||
this._currentDevicePixelRatio = window.devicePixelRatio;
|
||||
this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`);
|
||||
@@ -58,12 +52,11 @@ export class ScreenDprMonitor extends Disposable {
|
||||
}
|
||||
|
||||
public clearListener(): void {
|
||||
if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) {
|
||||
if (!this._listener) {
|
||||
return;
|
||||
}
|
||||
this._resolutionMediaMatchList.removeListener(this._outerListener);
|
||||
this._resolutionMediaMatchList = undefined;
|
||||
this._listener = undefined;
|
||||
this._outerListener = undefined;
|
||||
this._listener = null;
|
||||
this._outerListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from './renderer/Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData, IAttributeData } from './Types';
|
||||
import { ICircularList, XtermListener } from './common/Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import * as Browser from './common/Platform';
|
||||
import { ITheme, IDisposable, IMarker, IEvent } from 'xterm';
|
||||
import { Terminal } from './Terminal';
|
||||
import { AttributeData } from './BufferLine';
|
||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
|
||||
import { ICircularList, XtermListener } from '../common/Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from '../core/Platform';
|
||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||
import { Terminal } from '../Terminal';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
writeSync(data: string): void {
|
||||
@@ -20,15 +19,6 @@ export class TestTerminal extends Terminal {
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
onCursorMove: IEvent<void>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onTitleChange: IEvent<string>;
|
||||
onScroll: IEvent<number>;
|
||||
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
|
||||
onRender: IEvent<{ start: number; end: number; }>;
|
||||
onResize: IEvent<{ cols: number; rows: number; }>;
|
||||
markers: IMarker[];
|
||||
addMarker(cursorYOffset: number): IMarker {
|
||||
throw new Error('Method not implemented.');
|
||||
@@ -175,7 +165,6 @@ export class MockTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
export class MockCharMeasure implements ICharMeasure {
|
||||
onCharSizeChanged: IEvent<void>;
|
||||
width: number;
|
||||
height: number;
|
||||
measure(options: ITerminalOptions): void {
|
||||
@@ -198,7 +187,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttrData = new AttributeData();
|
||||
curAttr: number;
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -233,7 +222,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
setgLevel(g: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
eraseAttrData(): IAttributeData {
|
||||
eraseAttr(): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
eraseRight(x: number, y: number): void {
|
||||
@@ -320,7 +309,7 @@ export class MockBuffer implements IBuffer {
|
||||
scrollTop: number;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
savedCurAttrData = new AttributeData();
|
||||
savedCurAttr: number;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
|
||||
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
||||
}
|
||||
@@ -336,7 +325,7 @@ export class MockBuffer implements IBuffer {
|
||||
setLines(lines: ICircularList<IBufferLine>): void {
|
||||
this.lines = lines;
|
||||
}
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
|
||||
return Buffer.prototype.getBlankLine.apply(this, arguments);
|
||||
}
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
|
||||
@@ -345,17 +334,9 @@ export class MockBuffer implements IBuffer {
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
|
||||
return Buffer.prototype.iterator.apply(this, arguments);
|
||||
}
|
||||
getNullCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
onCanvasResize: IEvent<{ width: number; height: number; }>;
|
||||
onRender: IEvent<{ start: number; end: number; }>;
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
export interface IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig-library-base",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
],
|
||||
"outDir": "../../lib",
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./Lifecycle.ts",
|
||||
"./RenderDebouncer.ts",
|
||||
"./ScreenDprMonitor.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
]
|
||||
}
|
||||
@@ -161,11 +161,3 @@
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user