Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d04125ef | ||
|
|
d936a81c1f | ||
|
|
14a065e7c0 | ||
|
|
3ad45b72b6 | ||
|
|
925cdaef67 | ||
|
|
fa2a32e431 |
@@ -1,29 +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, puppeteer deps
|
||||
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 fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
|
||||
# https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352
|
||||
gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||||
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
|
||||
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
|
||||
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
|
||||
# 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"
|
||||
]
|
||||
}
|
||||
+5
-6
@@ -2,8 +2,6 @@ node_modules/
|
||||
*.swp
|
||||
.lock-wscript
|
||||
lib/
|
||||
out/
|
||||
out-test/
|
||||
Makefile.gyp
|
||||
*.Makefile
|
||||
*.target.gyp.mk
|
||||
@@ -15,12 +13,13 @@ npm-debug.log
|
||||
.env
|
||||
build/
|
||||
.DS_Store
|
||||
fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
# Directories needed for code coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
# dont commit benchmark folders
|
||||
.benchmark/
|
||||
timeline/
|
||||
|
||||
+1
-10
@@ -6,9 +6,6 @@
|
||||
!*.js
|
||||
!*.json
|
||||
|
||||
# Whitelist - css/
|
||||
!css/**/*.css
|
||||
|
||||
# Whitelist - dist/
|
||||
!dist/**/*.js
|
||||
!dist/**/*.js.map
|
||||
@@ -36,22 +33,16 @@
|
||||
!typings/*.d.ts
|
||||
|
||||
# Blacklist - (normal behavior) these will override any whitelist
|
||||
*.d.ts.map
|
||||
*.test.ts
|
||||
*.test.d.ts
|
||||
*.test.js
|
||||
*.test.js.map
|
||||
lib/test/
|
||||
tslint.json
|
||||
webpack.config.js
|
||||
|
||||
docs/
|
||||
/.idea/
|
||||
.vscode/
|
||||
bin/
|
||||
build/
|
||||
fixtures/
|
||||
coverage/
|
||||
demo/
|
||||
.devcontainer/
|
||||
out/
|
||||
addons/
|
||||
|
||||
Vendored
+6
-28
@@ -13,28 +13,7 @@
|
||||
"runtimeArgs": [
|
||||
"--colors",
|
||||
"--recursive",
|
||||
"${workspaceRoot}/out/**/*.test.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_PATH": "${workspaceRoot}/out"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Integration Tests",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--colors",
|
||||
"--recursive",
|
||||
"${workspaceRoot}/out/**/*.api.js"
|
||||
"${workspaceRoot}/lib"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
|
||||
@@ -44,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}/"
|
||||
},
|
||||
{
|
||||
@@ -56,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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
|
||||
## Features
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||
@@ -32,8 +33,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
|
||||
<script src="node_modules/xterm/lib/xterm.js"></script>
|
||||
<link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
|
||||
<script src="node_modules/xterm/dist/xterm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal"></div>
|
||||
@@ -56,38 +57,43 @@ import { Terminal } from 'xterm';
|
||||
|
||||
### Addons
|
||||
|
||||
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
|
||||
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts).
|
||||
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project:
|
||||
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
|
||||
|
||||
```bash
|
||||
npm i -S xterm-addon-web-links
|
||||
```
|
||||
|
||||
Then import the addon, instantiate it and call `Terminal.loadAddon`:
|
||||
|
||||
```ts
|
||||
```javascript
|
||||
import { Terminal } from 'xterm';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
const terminal = new Terminal();
|
||||
// Load WebLinksAddon on terminal, this is all that's needed to get web links
|
||||
// working in the terminal.
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
var xterm = new Terminal(); // Instantiate the terminal
|
||||
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
||||
```
|
||||
|
||||
The xterm.js team maintains the following addons but they can be built by anyone:
|
||||
You will also need to include the addon's CSS file if it has one in the folder.
|
||||
|
||||
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
|
||||
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
|
||||
- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality
|
||||
- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction
|
||||
#### Importing Addons in TypeScript
|
||||
|
||||
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking.
|
||||
|
||||
```typescript
|
||||
import { Terminal } from 'xterm';
|
||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||
const xterm = new Terminal();
|
||||
|
||||
fit(xterm); // Fit the terminal when necessary
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
|
||||
|
||||
We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
||||
- Chrome latest
|
||||
- Edge latest
|
||||
- Firefox latest
|
||||
- Safari latest
|
||||
- IE11
|
||||
|
||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
|
||||
|
||||
@@ -130,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.
|
||||
@@ -148,14 +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.
|
||||
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `<ng-terminal></ng-terminal>` into your component.
|
||||
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
|
||||
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
|
||||
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
@@ -167,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.
|
||||
@@ -185,6 +173,6 @@ You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Co
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,22 +0,0 @@
|
||||
## xterm-addon-attach
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-attach
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
|
||||
const terminal = new Terminal();
|
||||
const attachAddon = new AttachAddon(webSocket);
|
||||
terminal.loadAddon(attachAddon);
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts) for more advanced usage.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-attach",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-attach.js",
|
||||
"types": "typings/xterm-addon-attach.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
import WebSocket = require('ws');
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('AttachAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await page.goto(APP);
|
||||
});
|
||||
|
||||
it('string', async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await openTerminal({ rendererType: 'dom' });
|
||||
const port = 8080;
|
||||
const server = new WebSocket.Server({ port });
|
||||
server.on('connection', socket => socket.send('foo'));
|
||||
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
||||
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('utf8', async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await openTerminal({ rendererType: 'dom' });
|
||||
const port = 8080;
|
||||
const server = new WebSocket.Server({ port });
|
||||
const data = new Uint8Array([102, 111, 111]);
|
||||
server.on('connection', socket => socket.send(data));
|
||||
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
||||
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014, 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
|
||||
|
||||
interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon {
|
||||
private _socket: WebSocket;
|
||||
private _bidirectional: boolean;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
||||
this._socket = socket;
|
||||
// always set binary type to arraybuffer, we do not handle blobs
|
||||
this._socket.binaryType = 'arraybuffer';
|
||||
this._bidirectional = (options && options.bidirectional === false) ? false : true;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._disposables.push(
|
||||
addSocketListener(this._socket, 'message', ev => {
|
||||
const data: ArrayBuffer | string = ev.data;
|
||||
terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
|
||||
})
|
||||
);
|
||||
|
||||
if (this._bidirectional) {
|
||||
this._disposables.push(terminal.onData(data => this._sendData(data)));
|
||||
}
|
||||
|
||||
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
|
||||
this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
// TODO: do something better than just swallowing
|
||||
// the data if the socket is not in a working condition
|
||||
if (this._socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-attach' {
|
||||
export interface IAttachOptions {
|
||||
/**
|
||||
* Whether input should be written to the backend. Defaults to `true`.
|
||||
*/
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon {
|
||||
constructor(socket: WebSocket, options?: IAttachOptions);
|
||||
public activate(terminal: Terminal): void;
|
||||
public dispose(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'AttachAddon';
|
||||
const mainFile = 'xterm-addon-attach.js';
|
||||
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,24 +0,0 @@
|
||||
## xterm-addon-fit
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables fitting the terminal's dimensions to a containing element. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-fit
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
const terminal = new Terminal();
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(containerElement);
|
||||
fitAddon.fit();
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts) for more advanced usage.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-fit",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-fit.js",
|
||||
"types": "typings/xterm-addon-fit.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 1024;
|
||||
const height = 768;
|
||||
|
||||
describe('FitAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('no terminal', async function(): Promise<any> {
|
||||
await page.evaluate(`window.fit = new FitAddon();`);
|
||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||
});
|
||||
|
||||
describe('proposeDimensions', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
await loadFit();
|
||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
||||
cols: 87,
|
||||
rows: 26
|
||||
});
|
||||
});
|
||||
|
||||
it('width', async function(): Promise<any> {
|
||||
await loadFit(1008);
|
||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
||||
cols: 110,
|
||||
rows: 26
|
||||
});
|
||||
});
|
||||
|
||||
it('small', async function(): Promise<any> {
|
||||
await loadFit(1, 1);
|
||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
||||
cols: 2,
|
||||
rows: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fit', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
await loadFit();
|
||||
await page.evaluate(`window.fit.fit()`);
|
||||
assert.equal(await page.evaluate(`window.term.cols`), 87);
|
||||
assert.equal(await page.evaluate(`window.term.rows`), 26);
|
||||
});
|
||||
|
||||
it('width', async function(): Promise<any> {
|
||||
await loadFit(1008);
|
||||
await page.evaluate(`window.fit.fit()`);
|
||||
assert.equal(await page.evaluate(`window.term.cols`), 110);
|
||||
assert.equal(await page.evaluate(`window.term.rows`), 26);
|
||||
});
|
||||
|
||||
it('small', async function(): Promise<any> {
|
||||
await loadFit(1, 1);
|
||||
await page.evaluate(`window.fit.fit()`);
|
||||
assert.equal(await page.evaluate(`window.term.cols`), 2);
|
||||
assert.equal(await page.evaluate(`window.term.rows`), 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
|
||||
await page.evaluate(`
|
||||
window.fit = new FitAddon();
|
||||
window.term.loadAddon(window.fit);
|
||||
document.querySelector('#terminal-container').style.width='${width}px';
|
||||
document.querySelector('#terminal-container').style.height='${height}px';
|
||||
`);
|
||||
}
|
||||
|
||||
async function unloadFit(): Promise<void> {
|
||||
await page.evaluate(`window.fit.dispose();`);
|
||||
}
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
/**
|
||||
* The number of rows in the terminal.
|
||||
*/
|
||||
rows: number;
|
||||
|
||||
/**
|
||||
* The number of columns in the terminal.
|
||||
*/
|
||||
cols: number;
|
||||
}
|
||||
|
||||
const MINIMUM_COLS = 2;
|
||||
const MINIMUM_ROWS = 1;
|
||||
|
||||
export class FitAddon implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
public dispose(): void {}
|
||||
|
||||
public fit(): void {
|
||||
const dims = this.proposeDimensions();
|
||||
if (!dims || !this._terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (<any>this._terminal)._core;
|
||||
|
||||
// Force a full render
|
||||
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
|
||||
core._renderService.clear();
|
||||
this._terminal.resize(dims.cols, dims.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public proposeDimensions(): ITerminalDimensions | undefined {
|
||||
if (!this._terminal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this._terminal.element || !this._terminal.element.parentElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (<any>this._terminal)._core;
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(this._terminal.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-fit' {
|
||||
/**
|
||||
* An xterm.js addon that enables resizing the terminal to the dimensions of
|
||||
* its containing element.
|
||||
*/
|
||||
export class FitAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Creates a new fit addon.
|
||||
*/
|
||||
constructor();
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
|
||||
/**
|
||||
* Resizes the terminal to the dimensions of its containing element.
|
||||
*/
|
||||
public fit(): void;
|
||||
|
||||
/**
|
||||
* Gets the proposed dimensions that will be used for a fit.
|
||||
*/
|
||||
public proposeDimensions(): ITerminalDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprepresents the dimensions of a terminal.
|
||||
*/
|
||||
export interface ITerminalDimensions {
|
||||
/**
|
||||
* The number of rows in the terminal.
|
||||
*/
|
||||
rows: number;
|
||||
|
||||
/**
|
||||
* The number of columns in the terminal.
|
||||
*/
|
||||
cols: number;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'FitAddon';
|
||||
const mainFile = 'xterm-addon-fit.js';
|
||||
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,23 +0,0 @@
|
||||
## xterm-addon-search
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables searching the buffer. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-search
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
|
||||
const terminal = new Terminal();
|
||||
const searchAddon = new SearchAddon();
|
||||
terminal.loadAddon(searchAddon);
|
||||
searchAddon.findNext('foo');
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-search/typings/xterm-addon-search.d.ts) for more advanced usage.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-search",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-search.js",
|
||||
"types": "typings/xterm-addon-search.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('Search Tests', function (): void {
|
||||
this.timeout(20000);
|
||||
|
||||
before(async function (): Promise<any> {
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal();
|
||||
await page.evaluate(`window.search = new SearchAddon();`);
|
||||
await page.evaluate(`window.term.loadAddon(window.search);`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
});
|
||||
|
||||
it('Simple Search', async () => {
|
||||
await writeSync('dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
|
||||
});
|
||||
|
||||
it('Scrolling Search', async () => {
|
||||
let dataString = '';
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (i === 52) {
|
||||
dataString += '$^1_3{}test$#';
|
||||
}
|
||||
dataString += makeData(50);
|
||||
}
|
||||
await writeSync(dataString);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
|
||||
});
|
||||
it ('Incremental Find Previous', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
||||
await writeSync('package.json pack package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it ('Incremental Find Next', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
||||
await writeSync('package.jsonc');
|
||||
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it ('Simple Regex', async () => {
|
||||
await writeSync('abc123defABCD');
|
||||
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
|
||||
});
|
||||
|
||||
it('Search for single result twice should not unselect it', async () => {
|
||||
await writeSync('abc def');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
});
|
||||
});
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSync(data: string): Promise<void> {
|
||||
return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
|
||||
}
|
||||
|
||||
function makeData(length: number): string {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-search' {
|
||||
/**
|
||||
* Options for a search.
|
||||
*/
|
||||
export interface ISearchOptions {
|
||||
/**
|
||||
* Whether the search term is a regex.
|
||||
*/
|
||||
regex?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to search for a whole word, the result is only valid if it's
|
||||
* suppounded in "non-word" characters such as `_`, `(`, `)` or space.
|
||||
*/
|
||||
wholeWord?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the search is case sensitive.
|
||||
*/
|
||||
caseSensitive?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to do an indcremental search, this will expand the selection if it
|
||||
* still matches the term the user typed. Note that this only affects
|
||||
* `findNext`, not `findPrevious`.
|
||||
*/
|
||||
incremental?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An xterm.js addon that provides search functionality.
|
||||
*/
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
|
||||
/**
|
||||
* Search forwards for the next result that matches the search term and
|
||||
* options.
|
||||
* @param term The search term.
|
||||
* @param searchOptions The options for the search.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean;
|
||||
|
||||
/**
|
||||
* Search backwards for the previous result that matches the search term and
|
||||
* options.
|
||||
* @param term The search term.
|
||||
* @param searchOptions The options for the search.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'SearchAddon';
|
||||
const mainFile = 'xterm-addon-search.js';
|
||||
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,21 +0,0 @@
|
||||
## xterm-addon-web-links
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables web links. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-web-links
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
|
||||
const terminal = new Terminal();
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts) for more advanced usage.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-web-links",
|
||||
"version": "0.2.1",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-web-links.js",
|
||||
"types": "typings/xterm-addon-web-links.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('WebLinksAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async function(): Promise<any> {
|
||||
this.timeout(5000);
|
||||
await page.goto(APP);
|
||||
});
|
||||
|
||||
it('.com', async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await testHostName('foo.com');
|
||||
});
|
||||
|
||||
it('.com.au', async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await testHostName('foo.com.au');
|
||||
});
|
||||
|
||||
it('.io', async function(): Promise<any> {
|
||||
this.timeout(20000);
|
||||
await testHostName('foo.io');
|
||||
});
|
||||
});
|
||||
|
||||
async function testHostName(hostname: string): Promise<void> {
|
||||
await openTerminal({ rendererType: 'dom' });
|
||||
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
|
||||
await page.evaluate(`
|
||||
window.term.writeln(' http://${hostname} ');
|
||||
window.term.writeln(' http://${hostname}/a~b#c~d?e~f ');
|
||||
window.term.writeln(' http://${hostname}/colon:test ');
|
||||
window.term.writeln(' http://${hostname}/colon:test: ');
|
||||
window.term.writeln('"http://${hostname}/"');
|
||||
window.term.writeln('\\'http://${hostname}/\\'');
|
||||
window.term.writeln('http://${hostname}/subpath/+/id');
|
||||
`);
|
||||
assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`);
|
||||
assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`);
|
||||
assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`);
|
||||
assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`);
|
||||
assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`);
|
||||
assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`);
|
||||
assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`);
|
||||
}
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
|
||||
async function getLinkAtCell(col: number, row: number): Promise<string> {
|
||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||
await page.hover(`${rowSelector} > :nth-child(${col})`);
|
||||
return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-web-links' {
|
||||
/**
|
||||
* An xterm.js addon that enables web links.
|
||||
*/
|
||||
export class WebLinksAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Creates a new web links addon.
|
||||
* @param handler The callback when the link is called.
|
||||
* @param options Options for the link matcher.
|
||||
*/
|
||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'WebLinksAddon';
|
||||
const mainFile = 'xterm-addon-web-links.js';
|
||||
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -1,5 +0,0 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,23 +0,0 @@
|
||||
## xterm-addon-webgl
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+.
|
||||
|
||||
⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-webgl
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
||||
const terminal = new Terminal();
|
||||
terminal.loadAddon(new WebglAddon());
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-webgl.js",
|
||||
"types": "typings/xterm-addon-webgl.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { FLAGS } from './Constants';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
|
||||
export function getCompatAttr(bufferLine: IBufferLine, index: number): number {
|
||||
// TODO: Need to move WebGL over to the new system and remove this block
|
||||
const cell = new CellData();
|
||||
bufferLine.loadCell(index, cell);
|
||||
const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor();
|
||||
const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor();
|
||||
const oldAttr =
|
||||
(cell.isBold() ? FLAGS.BOLD : 0) |
|
||||
(cell.isUnderline() ? FLAGS.UNDERLINE : 0) |
|
||||
(cell.isBlink() ? FLAGS.BLINK : 0) |
|
||||
(cell.isInverse() ? FLAGS.INVERSE : 0) |
|
||||
(cell.isDim() ? FLAGS.DIM : 0) |
|
||||
(cell.isItalic() ? FLAGS.ITALIC : 0);
|
||||
const attrCompat =
|
||||
oldBg |
|
||||
(oldFg << 9) |
|
||||
(oldAttr << 18);
|
||||
return attrCompat;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* @license MIT
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
*/
|
||||
|
||||
import { IColor } from 'browser/Types';
|
||||
|
||||
export function getLuminance(color: IColor): number {
|
||||
// Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast
|
||||
const r = color.rgba >> 24 & 0xff;
|
||||
const g = color.rgba >> 16 & 0xff;
|
||||
const b = color.rgba >> 8 & 0xff;
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// TODO: Should be removed after chardata workaround is fixed
|
||||
export const enum FLAGS {
|
||||
BOLD = 1,
|
||||
UNDERLINE = 2,
|
||||
BLINK = 4,
|
||||
INVERSE = 8,
|
||||
INVISIBLE = 16,
|
||||
DIM = 32,
|
||||
ITALIC = 64
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { INDICIES_PER_CELL } from './WebglRenderer';
|
||||
import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { slice } from './TypedArray';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { getLuminance } from './ColorUtils';
|
||||
import { Terminal, IBufferLine } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
/**
|
||||
* These buffers are the ones used to bind to WebGL, the reason there are
|
||||
* multiple is to allow double buffering to work as you cannot modify the
|
||||
* buffer while it's being used by the GPU. Having multiple lets us start
|
||||
* working on the next frame.
|
||||
*/
|
||||
attributesBuffers: Float32Array[];
|
||||
selectionAttributes: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
UNIT_QUAD = 0,
|
||||
CELL_POSITION = 1,
|
||||
OFFSET = 2,
|
||||
SIZE = 3,
|
||||
TEXCOORD = 4,
|
||||
TEXSIZE = 5
|
||||
}
|
||||
|
||||
const vertexShaderSource = `#version 300 es
|
||||
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
|
||||
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
|
||||
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
|
||||
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
|
||||
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
|
||||
|
||||
uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec2 v_texcoord;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_texture, v_texcoord);
|
||||
}`;
|
||||
|
||||
const INDICES_PER_CELL = 10;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
export class GlyphRenderer {
|
||||
private _atlas: WebglCharAtlas | undefined;
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _textureLocation: WebGLUniformLocation;
|
||||
private _atlasTexture: WebGLTexture;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _activeBuffer: number = 0;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(0),
|
||||
attributesBuffers: [
|
||||
new Float32Array(0),
|
||||
new Float32Array(0)
|
||||
],
|
||||
selectionAttributes: new Float32Array(0)
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
const gl = this._gl;
|
||||
|
||||
const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
this._program = program;
|
||||
|
||||
// Uniform locations
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
|
||||
this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
this._vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
const unitQuadVerticesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup empty texture atlas
|
||||
this._atlasTexture = throwIfFalsy(gl.createTexture());
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
// Allow drawing of transparent texture
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Set viewport
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
return this._atlas ? this._atlas.beginFrame() : true;
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void {
|
||||
this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars);
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null/space character
|
||||
if (code === NULL_CELL_CODE || code === WHITESPACE_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (!this._atlas) {
|
||||
throw new Error('atlas must be set before updating cell');
|
||||
}
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg);
|
||||
} else {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg);
|
||||
}
|
||||
|
||||
// Fill empty if no glyph was found
|
||||
if (!rasterizedGlyph) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
// a_origin
|
||||
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
|
||||
// a_size
|
||||
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
|
||||
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
// a_texcoord
|
||||
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
|
||||
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
|
||||
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
public updateSelection(model: IRenderModel, columnSelectMode: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
||||
|
||||
// TODO: Make fg and bg configurable, currently since the buffer doesn't
|
||||
// support truecolor the char atlas cannot store it.
|
||||
const lumi = getLuminance(this._colors.background);
|
||||
const fg = lumi > 0.5 ? 7 : 0;
|
||||
const bg = lumi > 0.5 ? 0 : 7;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = model.selection.startCol;
|
||||
const width = model.selection.endCol - startCol;
|
||||
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
||||
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
|
||||
this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg);
|
||||
}
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
|
||||
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
|
||||
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
|
||||
this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg);
|
||||
}
|
||||
|
||||
// Draw final row
|
||||
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void {
|
||||
const terminal = this._terminal;
|
||||
const row = y + terminal.buffer.viewportY;
|
||||
let line: IBufferLine | undefined;
|
||||
for (let x = startCol; x < endCol; x++) {
|
||||
const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL;
|
||||
// Because the cache uses attr as a lookup key it needs to contain the selection colors as well
|
||||
let attr = model.cells[offset + 1];
|
||||
attr = attr & ~0x3ffff | bg << 9 | fg;
|
||||
const code = model.cells[offset];
|
||||
if (code & COMBINED_CHAR_BIT_MASK) {
|
||||
if (!line) {
|
||||
line = terminal.buffer.getLine(row);
|
||||
}
|
||||
const chars = line!.getCell(x)!.char;
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars);
|
||||
} else {
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
const terminal = this._terminal;
|
||||
const gl = this._gl;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Update vertices
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
}
|
||||
|
||||
public render(renderModel: IRenderModel, isSelectionVisible: boolean): void {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gl = this._gl;
|
||||
|
||||
gl.useProgram(this._program);
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
// Alternate buffers each frame as the active buffer gets locked while it's in use by the GPU
|
||||
this._activeBuffer = (this._activeBuffer + 1) % 2;
|
||||
const activeBuffer = this._vertices.attributesBuffers[this._activeBuffer];
|
||||
|
||||
// Copy data for each cell of each line up to its line length (the last non-whitespace cell)
|
||||
// from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
|
||||
// The reasons for this are as follows:
|
||||
// - So the active buffer can be alternated so we don't get blocked on rendering finishing
|
||||
// - To copy either the normal attributes buffer or the selection attributes buffer when there
|
||||
// is a selection
|
||||
// - So we don't send vertices for all the line-ending whitespace to the GPU
|
||||
let bufferLength = 0;
|
||||
for (let y = 0; y < renderModel.lineLengths.length; y++) {
|
||||
const si = y * this._terminal.cols * INDICES_PER_CELL;
|
||||
const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
activeBuffer.set(sub, bufferLength);
|
||||
bufferLength += sub.length;
|
||||
}
|
||||
|
||||
// Bind the attributes buffer
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the texture atlas if it's changed
|
||||
if (this._atlas.hasCanvasChanged) {
|
||||
this._atlas.hasCanvasChanged = false;
|
||||
gl.uniform1i(this._textureLocation, 0);
|
||||
gl.activeTexture(gl.TEXTURE0 + 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
|
||||
// Set uniforms
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: WebglCharAtlas): void {
|
||||
const gl = this._gl;
|
||||
this._atlas = atlas;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
import { DEFAULT_COLOR } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
POSITION = 0,
|
||||
SIZE = 1,
|
||||
COLOR = 2,
|
||||
UNIT_QUAD = 3
|
||||
}
|
||||
|
||||
const vertexShaderSource = `#version 300 es
|
||||
layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position;
|
||||
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
|
||||
layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color;
|
||||
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
|
||||
uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec3 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec3 v_color;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = vec4(v_color, 1);
|
||||
}`;
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
selection: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const INDICES_PER_RECTANGLE = 8;
|
||||
const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT;
|
||||
|
||||
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
|
||||
|
||||
export class RectangleRenderer {
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat!: Float32Array;
|
||||
private _selectionFloat!: Float32Array;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY),
|
||||
selection: new Float32Array(3 * INDICES_PER_RECTANGLE)
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
const gl = this._gl;
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
|
||||
// Uniform locations
|
||||
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
this._vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
const unitQuadVerticesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.POSITION, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.COLOR);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
|
||||
|
||||
this._updateCachedColors();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const gl = this._gl;
|
||||
|
||||
gl.useProgram(this._program);
|
||||
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Bind attributes buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
|
||||
// Bind selection buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
this._updateCachedColors();
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
this._selectionFloat = this._colorToFloat32Array(this._colors.selection);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
// Set first rectangle that clears the screen
|
||||
this._addRectangleFloat(
|
||||
this._vertices.attributes,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
this._terminal.cols * this._dimensions.scaledCellWidth,
|
||||
this._terminal.rows * this._dimensions.scaledCellHeight,
|
||||
this._bgFloat
|
||||
);
|
||||
}
|
||||
|
||||
public updateSelection(model: ISelectionRenderModel, columnSelectMode: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
if (!model.hasSelection) {
|
||||
fill(this._vertices.selection, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = model.startCol;
|
||||
const width = model.endCol - startCol;
|
||||
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
width * this._dimensions.scaledCellWidth,
|
||||
height * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
||||
const startRowEndCol = model.viewportCappedStartRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
(startRowEndCol - startCol) * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0);
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE,
|
||||
0,
|
||||
(model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight,
|
||||
terminal.cols * this._dimensions.scaledCellWidth,
|
||||
middleRowsCount * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw final row
|
||||
if (model.viewportCappedStartRow !== model.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE * 2,
|
||||
0,
|
||||
model.viewportCappedEndRow * this._dimensions.scaledCellHeight,
|
||||
endCol * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
} else {
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateBackgrounds(model: IRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
let rectangleCount = 1;
|
||||
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
let currentStartX = -1;
|
||||
let currentBg = DEFAULT_COLOR;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const modelIndex = ((y * terminal.cols) + x) * 4;
|
||||
const bg = model.cells[modelIndex + 2];
|
||||
if (bg !== currentBg) {
|
||||
// A rectangle needs to be drawn if going from non-default to another color
|
||||
if (currentBg !== DEFAULT_COLOR) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y);
|
||||
}
|
||||
currentStartX = x;
|
||||
currentBg = bg;
|
||||
}
|
||||
}
|
||||
// Finish rectangle if it's still going
|
||||
if (currentBg !== DEFAULT_COLOR) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y);
|
||||
}
|
||||
}
|
||||
vertices.count = rectangleCount;
|
||||
}
|
||||
|
||||
private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void {
|
||||
let color: IColor | null = null;
|
||||
if (bg === INVERTED_DEFAULT_COLOR) {
|
||||
color = this._colors.foreground;
|
||||
} else if (is256Color(bg)) {
|
||||
color = this._colors.ansi[bg];
|
||||
} else {
|
||||
// TODO: Add support for true color
|
||||
color = this._colors.foreground;
|
||||
}
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
const x1 = startX * this._dimensions.scaledCellWidth;
|
||||
const y1 = y * this._dimensions.scaledCellHeight;
|
||||
const r = ((color.rgba >> 24) & 0xFF) / 255;
|
||||
const g = ((color.rgba >> 16) & 0xFF) / 255;
|
||||
const b = ((color.rgba >> 8 ) & 0xFF) / 255;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
|
||||
}
|
||||
|
||||
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset + 4] = r;
|
||||
array[offset + 5] = g;
|
||||
array[offset + 6] = b;
|
||||
array[offset + 7] = a;
|
||||
}
|
||||
|
||||
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset + 4] = color[0];
|
||||
array[offset + 5] = color[1];
|
||||
array[offset + 6] = color[2];
|
||||
array[offset + 7] = color[3];
|
||||
}
|
||||
|
||||
private _colorToFloat32Array(color: IColor): Float32Array {
|
||||
return new Float32Array([
|
||||
((color.rgba >> 24) & 0xFF) / 255,
|
||||
((color.rgba >> 16) & 0xFF) / 255,
|
||||
((color.rgba >> 8 ) & 0xFF) / 255,
|
||||
((color.rgba ) & 0xFF) / 255
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderModel, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
|
||||
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
|
||||
|
||||
export class RenderModel implements IRenderModel {
|
||||
public cells: Uint32Array;
|
||||
public lineLengths: Uint32Array;
|
||||
public selection: ISelectionRenderModel;
|
||||
|
||||
constructor() {
|
||||
this.cells = new Uint32Array(0);
|
||||
this.lineLengths = new Uint32Array(0);
|
||||
this.selection = {
|
||||
hasSelection: false,
|
||||
viewportStartRow: 0,
|
||||
viewportEndRow: 0,
|
||||
viewportCappedStartRow: 0,
|
||||
viewportCappedEndRow: 0,
|
||||
startCol: 0,
|
||||
endCol: 0
|
||||
};
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
const indexCount = cols * rows * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
if (indexCount !== this.cells.length) {
|
||||
this.cells = new Uint32Array(indexCount);
|
||||
this.lineLengths = new Uint32Array(rows);
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
fill(this.cells, 0, 0);
|
||||
fill(this.lineLengths, 0, 0);
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selection.hasSelection = false;
|
||||
this.selection.viewportStartRow = 0;
|
||||
this.selection.viewportEndRow = 0;
|
||||
this.selection.viewportCappedStartRow = 0;
|
||||
this.selection.viewportCappedEndRow = 0;
|
||||
this.selection.startCol = 0;
|
||||
this.selection.endCol = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { sliceFallback } from './TypedArray';
|
||||
|
||||
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
function deepEquals(a: TypedArray, b: TypedArray): void {
|
||||
assert.equal(a.length, b.length);
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
assert.equal(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('polyfill conformance tests', function(): void {
|
||||
describe('TypedArray.slice', () => {
|
||||
describe('should work with all typed array types', () => {
|
||||
it('Uint8Array', () => {
|
||||
const a = new Uint8Array(5);
|
||||
deepEquals(sliceFallback(a, 2), a.slice(2));
|
||||
deepEquals(sliceFallback(a, 65535), a.slice(65535));
|
||||
deepEquals(sliceFallback(a, -1), a.slice(-1));
|
||||
});
|
||||
it('Uint16Array', () => {
|
||||
const u161 = new Uint16Array(5);
|
||||
const u162 = new Uint16Array(5);
|
||||
deepEquals(sliceFallback(u161, 2), u162.slice(2));
|
||||
deepEquals(sliceFallback(u161, 65535), u162.slice(65535));
|
||||
deepEquals(sliceFallback(u161, -1), u162.slice(-1));
|
||||
});
|
||||
it('Uint32Array', () => {
|
||||
const u321 = new Uint32Array(5);
|
||||
const u322 = new Uint32Array(5);
|
||||
deepEquals(sliceFallback(u321, 2), u322.slice(2));
|
||||
deepEquals(sliceFallback(u321, 65537), u322.slice(65537));
|
||||
deepEquals(sliceFallback(u321, -1), u322.slice(-1));
|
||||
});
|
||||
it('Int8Array', () => {
|
||||
const i81 = new Int8Array(5);
|
||||
const i82 = new Int8Array(5);
|
||||
deepEquals(sliceFallback(i81, 2), i82.slice(2));
|
||||
deepEquals(sliceFallback(i81, 65537), i82.slice(65537));
|
||||
deepEquals(sliceFallback(i81, -1), i82.slice(-1));
|
||||
});
|
||||
it('Int16Array', () => {
|
||||
const i161 = new Int16Array(5);
|
||||
const i162 = new Int16Array(5);
|
||||
deepEquals(sliceFallback(i161, 2), i162.slice(2));
|
||||
deepEquals(sliceFallback(i161, 65535), i162.slice(65535));
|
||||
deepEquals(sliceFallback(i161, -1), i162.slice(-1));
|
||||
});
|
||||
it('Int32Array', () => {
|
||||
const i321 = new Int32Array(5);
|
||||
const i322 = new Int32Array(5);
|
||||
deepEquals(sliceFallback(i321, 2), i322.slice(2));
|
||||
deepEquals(sliceFallback(i321, 65537), i322.slice(65537));
|
||||
deepEquals(sliceFallback(i321, -1), i322.slice(-1));
|
||||
});
|
||||
it('Float32Array', () => {
|
||||
const f321 = new Float32Array(5);
|
||||
const f322 = new Float32Array(5);
|
||||
deepEquals(sliceFallback(f321, 2), f322.slice(2));
|
||||
deepEquals(sliceFallback(f321, 65537), f322.slice(65537));
|
||||
deepEquals(sliceFallback(f321, -1), f322.slice(-1));
|
||||
});
|
||||
it('Float64Array', () => {
|
||||
const f641 = new Float64Array(5);
|
||||
const f642 = new Float64Array(5);
|
||||
deepEquals(sliceFallback(f641, 2), f642.slice(2));
|
||||
deepEquals(sliceFallback(f641, 65537), f642.slice(65537));
|
||||
deepEquals(sliceFallback(f641, -1), f642.slice(-1));
|
||||
});
|
||||
it('Uint8ClampedArray', () => {
|
||||
const u8Clamped1 = new Uint8ClampedArray(5);
|
||||
const u8Clamped2 = new Uint8ClampedArray(5);
|
||||
deepEquals(sliceFallback(u8Clamped1, 2), u8Clamped2.slice(2));
|
||||
deepEquals(sliceFallback(u8Clamped1, 65537), u8Clamped2.slice(65537));
|
||||
deepEquals(sliceFallback(u8Clamped1, -1), u8Clamped2.slice(-1));
|
||||
});
|
||||
});
|
||||
it('start', () => {
|
||||
const arr = new Uint32Array([1, 2, 3, 4, 5]);
|
||||
deepEquals(sliceFallback(arr, -1), arr.slice(-1));
|
||||
deepEquals(sliceFallback(arr, 0), arr.slice(0));
|
||||
deepEquals(sliceFallback(arr, 1), arr.slice(1));
|
||||
deepEquals(sliceFallback(arr, 2), arr.slice(2));
|
||||
deepEquals(sliceFallback(arr, 3), arr.slice(3));
|
||||
deepEquals(sliceFallback(arr, 4), arr.slice(4));
|
||||
deepEquals(sliceFallback(arr, 5), arr.slice(5));
|
||||
});
|
||||
it('end', () => {
|
||||
const arr = new Uint32Array([1, 2, 3, 4, 5]);
|
||||
deepEquals(sliceFallback(arr, -1, -2), arr.slice(-1, -2));
|
||||
deepEquals(sliceFallback(arr, 0, -2), arr.slice(0, -2));
|
||||
deepEquals(sliceFallback(arr, 1, -2), arr.slice(1, -2));
|
||||
deepEquals(sliceFallback(arr, 2, -2), arr.slice(2, -2));
|
||||
deepEquals(sliceFallback(arr, 3, -2), arr.slice(3, -2));
|
||||
deepEquals(sliceFallback(arr, 4, -2), arr.slice(4, -2));
|
||||
deepEquals(sliceFallback(arr, 5, -2), arr.slice(5, -2));
|
||||
|
||||
deepEquals(sliceFallback(arr, -1, 3), arr.slice(-1, 3));
|
||||
deepEquals(sliceFallback(arr, 0, 3), arr.slice(0, 3));
|
||||
deepEquals(sliceFallback(arr, 1, 3), arr.slice(1, 3));
|
||||
deepEquals(sliceFallback(arr, 2, 3), arr.slice(2, 3));
|
||||
deepEquals(sliceFallback(arr, 3, 3), arr.slice(3, 3));
|
||||
deepEquals(sliceFallback(arr, 4, 3), arr.slice(4, 3));
|
||||
deepEquals(sliceFallback(arr, 5, 3), arr.slice(5, 3));
|
||||
|
||||
deepEquals(sliceFallback(arr, -1, 8), arr.slice(-1, 8));
|
||||
deepEquals(sliceFallback(arr, 0, 8), arr.slice(0, 8));
|
||||
deepEquals(sliceFallback(arr, 1, 8), arr.slice(1, 8));
|
||||
deepEquals(sliceFallback(arr, 2, 8), arr.slice(2, 8));
|
||||
deepEquals(sliceFallback(arr, 3, 8), arr.slice(3, 8));
|
||||
deepEquals(sliceFallback(arr, 4, 8), arr.slice(4, 8));
|
||||
deepEquals(sliceFallback(arr, 5, 8), arr.slice(5, 8));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
export function slice<T extends TypedArray>(array: T, start?: number, end?: number): T {
|
||||
// all modern engines that support .slice
|
||||
if (array.slice) {
|
||||
return array.slice(start, end) as T;
|
||||
}
|
||||
return sliceFallback(array, start, end);
|
||||
}
|
||||
|
||||
export function sliceFallback<T extends TypedArray>(array: T, start: number = 0, end: number = array.length): T {
|
||||
if (start < 0) {
|
||||
start = (array.length + start) % array.length;
|
||||
}
|
||||
if (end >= array.length) {
|
||||
end = array.length;
|
||||
} else {
|
||||
end = (array.length + end) % array.length;
|
||||
}
|
||||
start = Math.min(start, end);
|
||||
|
||||
const result: T = new (array.constructor as any)(end - start);
|
||||
for (let i = 0; i < end - start; ++i) {
|
||||
result[i] = array[i + start];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface IRasterizedGlyphSet {
|
||||
[flags: number]: IRasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface IRenderModel {
|
||||
cells: Uint32Array;
|
||||
lineLengths: Uint32Array;
|
||||
selection: ISelectionRenderModel;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
hasSelection: boolean;
|
||||
viewportStartRow: number;
|
||||
viewportEndRow: number;
|
||||
viewportCappedStartRow: number;
|
||||
viewportCappedEndRow: number;
|
||||
startCol: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
createVertexArray(): IWebGLVertexArrayObject;
|
||||
bindVertexArray(vao: IWebGLVertexArrayObject): void;
|
||||
drawElementsInstanced(mode: number, count: number, type: number, offset: number, instanceCount: number): void;
|
||||
}
|
||||
|
||||
export interface IWebGLVertexArrayObject {
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate WebglRendererAddon before Terminal.open');
|
||||
}
|
||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
||||
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
throw new Error('WebglRendererAddon.dispose Not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from '../../../src/Types';
|
||||
import { ITheme } from 'xterm';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('WebGL Renderer Integration Tests', function(): void {
|
||||
this.timeout(20000);
|
||||
|
||||
before(async function(): Promise<any> {
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal();
|
||||
await page.evaluate(`window.term.loadAddon(new WebglAddon(true));`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
});
|
||||
|
||||
describe('WebGL Renderer', () => {
|
||||
it('foreground colors normal', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
black: '#010203',
|
||||
red: '#040506',
|
||||
green: '#070809',
|
||||
yellow: '#0a0b0c',
|
||||
blue: '#0d0e0f',
|
||||
magenta: '#101112',
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('foreground colors bright', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
brightBlack: '#010203',
|
||||
brightRed: '#040506',
|
||||
brightGreen: '#070809',
|
||||
brightYellow: '#0a0b0c',
|
||||
brightBlue: '#0d0e0f',
|
||||
brightMagenta: '#101112',
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('background colors normal', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
black: '#010203',
|
||||
red: '#040506',
|
||||
green: '#070809',
|
||||
yellow: '#0a0b0c',
|
||||
blue: '#0d0e0f',
|
||||
magenta: '#101112',
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('background colors bright', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
brightBlack: '#010203',
|
||||
brightRed: '#040506',
|
||||
brightGreen: '#070809',
|
||||
brightYellow: '#0a0b0c',
|
||||
brightBlue: '#0d0e0f',
|
||||
brightMagenta: '#101112',
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSync(data: string): Promise<void> {
|
||||
return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
|
||||
}
|
||||
|
||||
async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
await page.evaluate(`
|
||||
window.gl = window.term._core._renderService._renderer._gl;
|
||||
window.result = new Uint8Array(4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor((${col - 0.5}) * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight),
|
||||
1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../../../src/Types';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { getLuminance } from './ColorUtils';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { FLAGS } from './Constants';
|
||||
import { getCompatAttr } from './CharDataCompat';
|
||||
|
||||
export const INDICIES_PER_CELL = 4;
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlas: WebglCharAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
private _rectangleRenderer: RectangleRenderer;
|
||||
private _glyphRenderer: GlyphRenderer;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _core: ITerminal;
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this._core = (<any>this._terminal)._core;
|
||||
|
||||
this._applyBgLuminanceBasedSelection();
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
|
||||
new CursorRenderLayer(this._core.screenElement, 3, this._colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this._updateDimensions();
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
const contextAttributes = {
|
||||
antialias: false,
|
||||
depth: false,
|
||||
preserveDrawingBuffer
|
||||
};
|
||||
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
||||
if (!this._gl) {
|
||||
throw new Error('WebGL2 not supported');
|
||||
}
|
||||
this._core.screenElement.appendChild(this._canvas);
|
||||
|
||||
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.onCharSizeChanged();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
this._core.screenElement.removeChild(this._canvas);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _applyBgLuminanceBasedSelection(): void {
|
||||
// HACK: This is needed until webgl renderer adds support for selection colors
|
||||
if (getLuminance(this._colors.background) > 0.5) {
|
||||
this._colors.selection = { css: '#000', rgba: 255 };
|
||||
} else {
|
||||
this._colors.selection = { css: '#fff', rgba: 4294967295 };
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
|
||||
this._applyBgLuminanceBasedSelection();
|
||||
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
this._glyphRenderer.setColors();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== window.devicePixelRatio) {
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||
this._rectangleRenderer.onResize();
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
this._canvas.height = this.dimensions.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
// Resize the screen
|
||||
this._core.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._renderLayers.forEach(l => l.onFocus(this._terminal));
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||
|
||||
this._updateSelectionModel(start, end);
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
|
||||
this._glyphRenderer.updateSelection(this._model, columnSelectMode);
|
||||
|
||||
// TODO: #2102 Should this move to RenderCoordinator?
|
||||
this._core.refresh(0, this._terminal.rows - 1);
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(): void {
|
||||
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
||||
if (!('getRasterizedGlyph' in atlas)) {
|
||||
throw new Error('The webgl renderer only works with the webgl char atlas');
|
||||
}
|
||||
this._charAtlas = atlas as WebglCharAtlas;
|
||||
this._charAtlas.warmUp();
|
||||
this._glyphRenderer.setAtlas(this._charAtlas);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
// Update render layers
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
this._updateModel(start, end);
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.render();
|
||||
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
|
||||
}
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row)!;
|
||||
this._model.lineLengths[y] = 0;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
let code = charData[CHAR_DATA_CODE_INDEX];
|
||||
const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX];
|
||||
const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL;
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
}
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve bg and fg and cache in the model
|
||||
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;
|
||||
}
|
||||
}
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
|
||||
// Flag combined chars with a bit mask so they're easily identifiable
|
||||
if (chars.length > 1) {
|
||||
code = code | COMBINED_CHAR_BIT_MASK;
|
||||
}
|
||||
|
||||
this._model.cells[i ] = code;
|
||||
this._model.cells[i + 1] = attr;
|
||||
this._model.cells[i + 2] = bg;
|
||||
this._model.cells[i + 3] = fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars);
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number], end: [number, number]): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
this._model.selection.hasSelection = true;
|
||||
this._model.selection.viewportStartRow = viewportStartRow;
|
||||
this._model.selection.viewportEndRow = viewportEndRow;
|
||||
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
// TODO: Acquire CharSizeService properly
|
||||
|
||||
// Perform a new measure if the CharMeasure dimensions are not yet available
|
||||
if (!(<any>this._core)._charSizeService.width || !(<any>this._core)._charSizeService.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
|
||||
// NOTE: ceil fixes sometime, floor does others :s
|
||||
|
||||
this.dimensions.scaledCharWidth = Math.floor((<any>this._core)._charSizeService.width * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case
|
||||
// devicePixelRatio is a floating point number in order to ensure there is
|
||||
// enough space to draw the character to the cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil((<any>this._core)._charSizeService.height * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
||||
|
||||
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
|
||||
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. CharMeasure.width/height by itself is
|
||||
// insufficient when the page is not at 100% zoom level as CharMeasure is
|
||||
// measured in CSS pixels, but the actual char size on the canvas can
|
||||
// differ.
|
||||
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
// This fixes 110% and 125%, not 150% or 175% though
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
|
||||
* top to bottom) to clip space.
|
||||
*/
|
||||
export const PROJECTION_MATRIX = new Float32Array([
|
||||
2, 0, 0, 0,
|
||||
0, -2, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
-1, 1, 0, 1
|
||||
]);
|
||||
|
||||
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined {
|
||||
const program = throwIfFalsy(gl.createProgram());
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource)));
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)));
|
||||
gl.linkProgram(program);
|
||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (success) {
|
||||
return program;
|
||||
}
|
||||
|
||||
console.error(gl.getProgramInfoLog(program));
|
||||
gl.deleteProgram(program);
|
||||
}
|
||||
|
||||
export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
|
||||
const shader = throwIfFalsy(gl.createShader(type));
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
||||
if (success) {
|
||||
return shader;
|
||||
}
|
||||
|
||||
console.error(gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
}
|
||||
|
||||
export function expandFloat32Array(source: Float32Array, max: number): Float32Array {
|
||||
const newLength = Math.min(source.length * 2, max);
|
||||
const newArray = new Float32Array(newLength);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
newArray[i] = source[i];
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DEFAULT_COLOR } from 'common/buffer/Constants';
|
||||
import { Terminal, FontWeight } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
|
||||
const NULL_COLOR: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: NULL_COLOR,
|
||||
cursorAccent: NULL_COLOR,
|
||||
selection: NULL_COLOR,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice()
|
||||
};
|
||||
return {
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.getOption('fontFamily'),
|
||||
fontSize: terminal.getOption('fontSize'),
|
||||
fontWeight: terminal.getOption('fontWeight') as FontWeight,
|
||||
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
|
||||
allowTransparency: terminal.getOption('allowTransparency'),
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
}
|
||||
|
||||
export function is256Color(colorCode: number): boolean {
|
||||
return colorCode < DEFAULT_COLOR;
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
|
||||
import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants';
|
||||
import { is256Color } from './CharAtlasUtils';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'browser/Types';
|
||||
import { FLAGS } from '../Constants';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
/**
|
||||
* The amount of the texture to be filled before throwing it away and starting
|
||||
* again. Since the throw away and individual glyph draws don't cost too much,
|
||||
* this prevent juggling multiple textures in the GL context.
|
||||
*/
|
||||
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
*/
|
||||
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
|
||||
offset: { x: 0, y: 0 },
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: { x: 0, y: 0 },
|
||||
sizeClipSpace: { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
const TMP_CANVAS_GLYPH_PADDING = 2;
|
||||
|
||||
export class WebglCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
|
||||
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
public cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Since glyphs are expected to be around the same height, the packing
|
||||
// strategy used it to fill a row with glyphs while keeping track of the
|
||||
// tallest glyph in the row. Once the row is full a new row is started at
|
||||
// (0,lastRow+lastRowTallestGlyph).
|
||||
private _currentRowY: number = 0;
|
||||
private _currentRowX: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
|
||||
public hasCanvasChanged = false;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
|
||||
constructor(document: Document, private _config: ICharAtlasConfig) {
|
||||
this.cacheCanvas = document.createElement('canvas');
|
||||
this.cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this.cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', {alpha: true}));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
|
||||
|
||||
// This is useful for debugging
|
||||
document.body.appendChild(this.cacheCanvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.cacheCanvas.parentElement) {
|
||||
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126
|
||||
for (let i = 33; i < 126; i++) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, DEFAULT_COLOR, DEFAULT_COLOR);
|
||||
this._cacheMap[i] = {
|
||||
[DEFAULT_ATTR]: rasterizedGlyph
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
if (this._currentRowY > TEXTURE_CAPACITY) {
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
this._cacheMap = {};
|
||||
this._currentRowHeight = 0;
|
||||
this._currentRowX = 0;
|
||||
this._currentRowY = 0;
|
||||
this._doWarmUp();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = this._cacheMapCombined[chars];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMapCombined[chars] = rasterizedGlyphSet;
|
||||
}
|
||||
let rasterizedGlyph = rasterizedGlyphSet[attr];
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(chars, attr, bg, fg);
|
||||
rasterizedGlyphSet[attr] = rasterizedGlyph;
|
||||
}
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the glyphs texture coords, drawing the texture if it's not already
|
||||
*/
|
||||
public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = this._cacheMap[code];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMap[code] = rasterizedGlyphSet;
|
||||
}
|
||||
let rasterizedGlyph = rasterizedGlyphSet[attr];
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(code, attr, bg, fg);
|
||||
rasterizedGlyphSet[attr] = rasterizedGlyph;
|
||||
}
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx >= this._config.colors.ansi.length) {
|
||||
throw new Error('No color found for idx ' + idx);
|
||||
}
|
||||
return this._config.colors.ansi[idx];
|
||||
}
|
||||
|
||||
private _getBackgroundColor(bg: number): IColor {
|
||||
if (this._config.allowTransparency) {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return TRANSPARENT_COLOR;
|
||||
} else if (bg === INVERTED_DEFAULT_COLOR) {
|
||||
return this._config.colors.foreground;
|
||||
} else if (is256Color(bg)) {
|
||||
return this._getColorFromAnsiIndex(bg);
|
||||
}
|
||||
// TODO: Support true color
|
||||
return this._config.colors.background;
|
||||
}
|
||||
|
||||
private _getForegroundColor(fg: number): IColor {
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
return this._config.colors.background;
|
||||
} else if (is256Color(fg)) {
|
||||
return this._getColorFromAnsiIndex(fg);
|
||||
}
|
||||
// TODO: Support true color
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
|
||||
private _drawToCache(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
|
||||
const flags = attr >> 18;
|
||||
|
||||
const bold = !!(flags & FLAGS.BOLD);
|
||||
const dim = !!(flags & FLAGS.DIM);
|
||||
const italic = !!(flags & FLAGS.ITALIC);
|
||||
|
||||
this._tmpCtx.save();
|
||||
|
||||
// draw the background
|
||||
const backgroundColor = this._getBackgroundColor(bg);
|
||||
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
|
||||
// transparency in backgroundColor
|
||||
this._tmpCtx.globalCompositeOperation = 'copy';
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'top';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(fg).css;
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
const imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
|
||||
);
|
||||
|
||||
// TODO: Support transparency
|
||||
// let isEmpty = false;
|
||||
// if (!this._config.allowTransparency) {
|
||||
// isEmpty = clearColor(imageData, backgroundColor);
|
||||
// }
|
||||
|
||||
// Clear out the background color and determine if the glyph is empty.
|
||||
const isEmpty = clearColor(imageData, backgroundColor);
|
||||
|
||||
// Handle empty glyphs
|
||||
if (isEmpty) {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
if (this._currentRowX + this._config.scaledCharWidth > TEXTURE_WIDTH) {
|
||||
this._currentRowX = 0;
|
||||
this._currentRowY += this._currentRowHeight;
|
||||
this._currentRowHeight = 0;
|
||||
}
|
||||
|
||||
// Record texture position
|
||||
rasterizedGlyph.texturePosition.x = this._currentRowX;
|
||||
rasterizedGlyph.texturePosition.y = this._currentRowY;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = this._currentRowX / TEXTURE_WIDTH;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = this._currentRowY / TEXTURE_HEIGHT;
|
||||
|
||||
// Update atlas current row
|
||||
this._currentRowHeight = Math.max(this._currentRowHeight, rasterizedGlyph.size.y);
|
||||
this._currentRowX += rasterizedGlyph.size.x;
|
||||
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ImageData object, find the bounding box of the non-transparent
|
||||
* portion of the texture and return an IRasterizedGlyph with these
|
||||
* dimensions.
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
let found = false;
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.right = this._tmpCanvas.width;
|
||||
found = false;
|
||||
for (let x = this._tmpCanvas.width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.bottom = this._tmpCanvas.height;
|
||||
found = false;
|
||||
for (let y = this._tmpCanvas.height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: {
|
||||
x: boundingBox.right - boundingBox.left + 1,
|
||||
y: boundingBox.bottom - boundingBox.top + 1
|
||||
},
|
||||
sizeClipSpace: {
|
||||
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING,
|
||||
y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
|
||||
const width = boundingBox.right - boundingBox.left + 1;
|
||||
const height = boundingBox.bottom - boundingBox.top + 1;
|
||||
const clippedData = new Uint8ClampedArray(width * height * 4);
|
||||
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
|
||||
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
|
||||
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
|
||||
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
|
||||
clippedData[newOffset] = imageData.data[oldOffset];
|
||||
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
|
||||
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
|
||||
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
|
||||
}
|
||||
}
|
||||
return new ImageData(clippedData, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a partiicular rgb color in an ImageData completely transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, color: IColor): boolean {
|
||||
let isEmpty = true;
|
||||
const r = color.rgba >>> 24;
|
||||
const g = color.rgba >>> 16 & 0xFF;
|
||||
const b = color.rgba >>> 8 & 0xFF;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (imageData.data[offset] === r &&
|
||||
imageData.data[offset + 1] === g &&
|
||||
imageData.data[offset + 2] === b) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
return isEmpty;
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderLayer } from './Types';
|
||||
import { IGlyphIdentifier } from '../atlas/Types';
|
||||
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
private _scaledCellHeight: number = 0;
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
protected _charAtlas: WebglCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||
*/
|
||||
private _currentGlyphIdentifier: IGlyphIdentifier = {
|
||||
chars: '',
|
||||
code: 0,
|
||||
bg: 0,
|
||||
fg: 0,
|
||||
bold: false,
|
||||
dim: false,
|
||||
italic: false
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {}
|
||||
public onBlur(terminal: Terminal): void {}
|
||||
public onFocus(terminal: Terminal): void {}
|
||||
public onCursorMove(terminal: Terminal): void {}
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(terminal: Terminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||
|
||||
public setColors(terminal: Terminal, colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
}
|
||||
|
||||
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Cloning preserves properties
|
||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
}
|
||||
|
||||
public abstract reset(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at
|
||||
* @param width The number of columns to fill.
|
||||
* @param height The number of rows to fill.
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
window.devicePixelRatio,
|
||||
this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
|
||||
* strokeStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
||||
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
||||
width * this._scaledCellWidth - window.devicePixelRatio,
|
||||
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire canvas.
|
||||
*/
|
||||
protected _clearAll(): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears 1+ cells completely.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at.
|
||||
* @param width The number of columns to clear.
|
||||
* @param height The number of rows to clear.
|
||||
*/
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a truecolor character at the cell. The character will be clipped to
|
||||
* ensure that it fits with the cell, including the cell to the right if it's
|
||||
* a wide character. This uses the existing fillStyle on the context.
|
||||
* @param terminal The terminal.
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, 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(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param terminal The terminal.
|
||||
* @param y The row to clip.
|
||||
*/
|
||||
private _clipRow(terminal: Terminal, y: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
terminal.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current font.
|
||||
* @param terminal The terminal.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.getOption('fontWeightBold') : terminal.getOption('fontWeight');
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${terminal.getOption('fontSize') * window.devicePixelRatio}px ${terminal.getOption('fontFamily')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
y: number;
|
||||
isFocused: boolean;
|
||||
style: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time between cursor blinks.
|
||||
*/
|
||||
const BLINK_INTERVAL = 600;
|
||||
|
||||
export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _state: ICursorState;
|
||||
private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: ICellData) => void};
|
||||
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
this._cursorRenderers = {
|
||||
'bar': this._renderBarCursor.bind(this),
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
public reset(terminal: Terminal): void {
|
||||
this._clearCursor();
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this.onOptionsChanged(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onBlur(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.pause();
|
||||
}
|
||||
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
|
||||
}
|
||||
|
||||
public onFocus(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.resume(terminal);
|
||||
} else {
|
||||
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {
|
||||
if (terminal.getOption('cursorBlink')) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
|
||||
this._render(terminal, true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
}
|
||||
}
|
||||
// Request a refresh from the terminal as management of rendering is being
|
||||
// moved back to the terminal
|
||||
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
|
||||
}
|
||||
|
||||
public onCursorMove(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(terminal, false);
|
||||
} else {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
|
||||
// Don't draw the cursor if it's hidden
|
||||
// TODO: Need to expose API for this
|
||||
if (!(terminal as any)._core.cursorState || (terminal as any)._core.cursorHidden) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorY = terminal.buffer.baseY + terminal.buffer.cursorY;
|
||||
const viewportRelativeCursorY = cursorY - terminal.buffer.viewportY;
|
||||
|
||||
// Don't draw the cursor if it's off-screen
|
||||
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Need fast buffer API for loading cell
|
||||
(terminal as any)._core.buffer.lines.get(cursorY).loadCell(terminal.buffer.cursorX, this._cell);
|
||||
if (this._cell.content === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTerminalFocused(terminal)) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
const cursorStyle = terminal.getOption('cursorStyle');
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
||||
}
|
||||
this._ctx.restore();
|
||||
this._state.x = terminal.buffer.cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't draw the cursor if it's blinking
|
||||
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state) {
|
||||
// The cursor is already in the correct spot, don't redraw
|
||||
if (this._state.x === terminal.buffer.cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === isTerminalFocused(terminal) &&
|
||||
this._state.style === terminal.getOption('cursorStyle') &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
this._clearCursor();
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = terminal.buffer.cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.getOption('cursorStyle');
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(terminal, cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
class CursorBlinkStateManager {
|
||||
public isCursorVisible: boolean;
|
||||
|
||||
private _animationFrame: number | undefined;
|
||||
private _blinkStartTimeout: number | undefined;
|
||||
private _blinkInterval: number | undefined;
|
||||
|
||||
/**
|
||||
* The time at which the animation frame was restarted, this is used on the
|
||||
* next render to restart the timers so they don't need to restart the timers
|
||||
* multiple times over a short period.
|
||||
*/
|
||||
private _animationTimeRestarted: number | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
private _renderCallback: () => void
|
||||
) {
|
||||
this.isCursorVisible = true;
|
||||
if (isTerminalFocused(terminal)) {
|
||||
this._restartInterval();
|
||||
}
|
||||
}
|
||||
|
||||
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
|
||||
|
||||
public dispose(): void {
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public restartBlinkAnimation(terminal: Terminal): void {
|
||||
if (this.isPaused) {
|
||||
return;
|
||||
}
|
||||
// Save a timestamp so that the restart can be done on the next interval
|
||||
this._animationTimeRestarted = Date.now();
|
||||
// Force a cursor render to ensure it's visible and in the correct position
|
||||
this.isCursorVisible = true;
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
|
||||
// Clear any existing interval
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
}
|
||||
|
||||
// Setup the initial timeout which will hide the cursor, this is done before
|
||||
// the regular interval is setup in order to support restarting the blink
|
||||
// animation in a lightweight way (without thrashing clearInterval and
|
||||
// setInterval).
|
||||
this._blinkStartTimeout = <number><any>setTimeout(() => {
|
||||
// Check if another animation restart was requested while this was being
|
||||
// started
|
||||
if (this._animationTimeRestarted) {
|
||||
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
|
||||
this._animationTimeRestarted = undefined;
|
||||
if (time > 0) {
|
||||
this._restartInterval(time);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the cursor
|
||||
this.isCursorVisible = false;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
|
||||
// Setup the blink interval
|
||||
this._blinkInterval = <number><any>setInterval(() => {
|
||||
// Adjust the animation time if it was restarted
|
||||
if (this._animationTimeRestarted) {
|
||||
// calc time diff
|
||||
// Make restart interval do a setTimeout initially?
|
||||
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
|
||||
this._animationTimeRestarted = undefined;
|
||||
this._restartInterval(time);
|
||||
return;
|
||||
}
|
||||
|
||||
// Invert visibility and render
|
||||
this.isCursorVisible = !this.isCursorVisible;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}, BLINK_INTERVAL);
|
||||
}, timeToStart);
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.isCursorVisible = true;
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public resume(terminal: Terminal): void {
|
||||
this._animationTimeRestarted = undefined;
|
||||
this._restartInterval();
|
||||
this.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalFocused(terminal: Terminal): boolean {
|
||||
return document.activeElement === terminal.textarea && document.hasFocus();
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierAccessor } from '../../../../src/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from '../atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||
super(container, 'link', zIndex, true, colors);
|
||||
terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = undefined;
|
||||
}
|
||||
|
||||
public reset(terminal: Terminal): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
|
||||
private _clearCurrentLink(): void {
|
||||
if (this._state) {
|
||||
this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
|
||||
const middleRowCount = this._state.y2 - this._state.y1 - 1;
|
||||
if (middleRowCount > 0) {
|
||||
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
|
||||
}
|
||||
this._clearCells(0, this._state.y2, this._state.x2, 1);
|
||||
this._state = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
// Single line link
|
||||
this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||
} else {
|
||||
// Multi-line link
|
||||
this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
|
||||
for (let y = e.y1 + 1; y < e.y2; y++) {
|
||||
this._fillBottomLineAtCells(0, y, e.cols);
|
||||
}
|
||||
this._fillBottomLineAtCells(0, e.y2, e.x2);
|
||||
}
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(terminal: Terminal, colorSet: IColorSet): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(terminal: Terminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(handler: (text: string) => [number, number][]): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
resize(terminal: Terminal, dim: IRenderDimensions): void;
|
||||
|
||||
/**
|
||||
* Clear the state of the render layer.
|
||||
*/
|
||||
reset(terminal: Terminal): void;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [ "../../../src/common/*" ],
|
||||
"browser/*": [ "../../../src/browser/*" ]
|
||||
},
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/common" },
|
||||
{ "path": "../../../src/browser" }
|
||||
]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-webgl' {
|
||||
/**
|
||||
* An xterm.js addon that provides search functionality.
|
||||
*/
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
constructor(preserveDrawingBuffer?: boolean);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'WebglAddon';
|
||||
const mainFile = 'xterm-addon-webgl.js';
|
||||
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
+11
-70
@@ -20,8 +20,8 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn test-unit --forbid-only
|
||||
displayName: 'Unit tests'
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
@@ -38,11 +38,16 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn test-unit --forbid-only
|
||||
displayName: 'Unit tests'
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
- script: |
|
||||
yarn test-coverage
|
||||
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
|
||||
yarn coveralls
|
||||
displayName: 'Generate and publish coverage'
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
@@ -56,72 +61,8 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn test-unit --forbid-only
|
||||
displayName: 'Unit tests'
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
|
||||
- job: Linux_IntegrationTests
|
||||
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
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn start &
|
||||
sleep 10
|
||||
yarn test-api --headless --forbid-only
|
||||
displayName: 'Linux Integration tests'
|
||||
|
||||
- job: macOS_IntegrationTests
|
||||
pool:
|
||||
vmImage: 'xcode9-macos10.13'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: |
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn start &
|
||||
sleep 10
|
||||
yarn test-api --headless --forbid-only
|
||||
displayName: 'MacOS Integration tests'
|
||||
|
||||
- job: Release
|
||||
dependsOn:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux_IntegrationTests
|
||||
- macOS_IntegrationTests
|
||||
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
|
||||
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
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
|
||||
displayName: 'Package and publish to npm'
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#! /usr/bin/env sh
|
||||
|
||||
# Usage: ./bin/prepare-release x.y.z
|
||||
# x.y.z should be semver (e.g. 1.0.0)
|
||||
|
||||
set -e
|
||||
|
||||
NEW_VERSION=$1
|
||||
CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
|
||||
| grep version \
|
||||
| head -1 \
|
||||
| awk -F: '{ print $2 }' \
|
||||
| sed 's/[",]//g' \
|
||||
| tr -d '[[:space:]]')
|
||||
|
||||
# Build xterm.js into `dist`
|
||||
export BUILD_DIR=dist
|
||||
npm run build
|
||||
|
||||
# Update version in package.json
|
||||
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
|
||||
|
||||
git commit -S -s -a -m "Bump version to $NEW_VERSION"
|
||||
git tag $NEW_VERSION
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// Setup auth
|
||||
fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
|
||||
|
||||
const isDryRun = process.argv.indexOf('--dry') !== -1;
|
||||
if (isDryRun) {
|
||||
console.log('Publish dry run');
|
||||
}
|
||||
|
||||
const changedFiles = getChangedFilesInCommit('HEAD');
|
||||
|
||||
// Publish xterm if any files were changed outside of the addons directory
|
||||
let isStableRelease = false;
|
||||
if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
|
||||
isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..'));
|
||||
}
|
||||
|
||||
// Publish addons if any files were changed inside of the addon
|
||||
const addonPackageDirs = [
|
||||
path.resolve(__dirname, '../addons/xterm-addon-attach'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-fit'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-search'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-web-links'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-webgl')
|
||||
];
|
||||
console.log(`Checking if addons need to be published`);
|
||||
addonPackageDirs.forEach(p => {
|
||||
const addon = path.basename(p);
|
||||
if (changedFiles.some(e => e.indexOf(addon) !== -1)) {
|
||||
console.log(`Try publish ${addon}`);
|
||||
checkAndPublishPackage(p);
|
||||
}
|
||||
});
|
||||
|
||||
// Publish website if it's a stable release
|
||||
if (isStableRelease) {
|
||||
updateWebsite();
|
||||
}
|
||||
|
||||
function checkAndPublishPackage(packageDir) {
|
||||
const packageJson = require(path.join(packageDir, 'package.json'));
|
||||
|
||||
// Determine if this is a stable or beta release
|
||||
const publishedVersions = getPublishedVersions(packageJson);
|
||||
const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
|
||||
|
||||
// Get the next version
|
||||
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson);
|
||||
console.log(`Publishing version: ${nextVersion}`);
|
||||
|
||||
// Set the version in package.json
|
||||
const packageJsonFile = path.join(packageDir, 'package.json');
|
||||
packageJson.version = nextVersion;
|
||||
console.log(`Set version of ${packageJsonFile} to ${nextVersion}`);
|
||||
if (!isDryRun) {
|
||||
fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
|
||||
// Publish
|
||||
const args = ['publish'];
|
||||
if (!isStableRelease) {
|
||||
args.push('--tag', 'beta');
|
||||
}
|
||||
console.log(`Spawn: npm ${args.join(' ')}`);
|
||||
if (!isDryRun) {
|
||||
const result = cp.spawnSync('npm', args, {
|
||||
cwd: packageDir,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (result.status) {
|
||||
console.error(`Spawn exited with code ${result.status}`);
|
||||
process.exit(result.status);
|
||||
}
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
|
||||
return isStableRelease;
|
||||
}
|
||||
|
||||
function getNextBetaVersion(packageJson) {
|
||||
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}.0`;
|
||||
const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag);
|
||||
if (publishedVersions.length === 0) {
|
||||
return `${nextStableVersion}-${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(packageJson, version, tag) {
|
||||
const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
|
||||
const versionsJson = JSON.parse(versionsProcess.stdout);
|
||||
if (tag) {
|
||||
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
|
||||
}
|
||||
return versionsJson;
|
||||
}
|
||||
|
||||
function getChangedFilesInCommit(commit) {
|
||||
const args = ['log', '-m', '-1', '--name-only', `--pretty=format:`, commit];
|
||||
const result = cp.spawnSync('git', args);
|
||||
const output = result.stdout.toString();
|
||||
const changedFiles = output.split('\n').filter(e => e.length > 0);
|
||||
return changedFiles;
|
||||
}
|
||||
|
||||
function updateWebsite() {
|
||||
console.log('Updating website');
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'website-'));
|
||||
const packageJson = require(path.join(path.resolve(__dirname, '..'), 'package.json'));
|
||||
if (!isDryRun) {
|
||||
cp.spawnSync('sh', [path.join(__dirname, 'update-website.sh'), packageJson.version], { cwd, stdio: [process.stdin, process.stdout, process.stderr] });
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#! /usr/bin/env sh
|
||||
|
||||
# Usage: ./bin/release x.y.z
|
||||
# x.y.z should be semver (e.g. 1.0.0)
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ];
|
||||
then
|
||||
echo "No version supplied. Please a version argument\n"
|
||||
echo "Usage: $0 VERSION\n"
|
||||
echo "Example: $0 1.0.0"
|
||||
exit
|
||||
fi
|
||||
|
||||
NEW_VERSION=$1
|
||||
|
||||
./bin/prepare-release $NEW_VERSION
|
||||
|
||||
git push && \
|
||||
git push --tags && \
|
||||
npm publish
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
||||
const env = { ...process.env };
|
||||
env.NODE_PATH = path.resolve(__dirname, '../out');
|
||||
|
||||
let testFiles = [
|
||||
'./out/*test.js',
|
||||
'./out/**/*test.js'
|
||||
];
|
||||
|
||||
let flagArgs = [];
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
const args = process.argv.slice(2);
|
||||
flagArgs = args.filter(e => e.startsWith('--'));
|
||||
// ability to inject particular test files via
|
||||
// yarn test [testFileA testFileB ...]
|
||||
files = args.filter(e => !e.startsWith('--'));
|
||||
if (files.length) {
|
||||
testFiles = files;
|
||||
}
|
||||
}
|
||||
|
||||
const run = cp.spawnSync(
|
||||
path.resolve(__dirname, '../node_modules/.bin/mocha'),
|
||||
[...testFiles, ...flagArgs],
|
||||
{
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
env,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
|
||||
process.exit(run.status);
|
||||
@@ -1,212 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Script to test different mouse modes in terminal emulators.
|
||||
* Tests for protocols DECSET 9, 1000, 1002, 1003 with different
|
||||
* report encodings (default, UTF8, SGR, URXVT).
|
||||
*
|
||||
* VT200 Highlight mode (DECSET 1001) is not implemented.
|
||||
*
|
||||
* The test basically applies the report data to the cursor, thus
|
||||
* a mouse report should move the cursor to the cell under the mouse.
|
||||
* Furthermore the reports are printed in the left lower corner as
|
||||
* raw data and their meaning.
|
||||
*
|
||||
* A failing test might show:
|
||||
* - wrong coords: the cursor will jump to some other place
|
||||
* - wrong buttons: see meaning output and check whether it makes sense
|
||||
* - faulty reports: inspect the raw data and compare with other emulators
|
||||
* - missing events: compare with spec / other emulators
|
||||
*/
|
||||
|
||||
let activeProtocol = 0;
|
||||
let activeEnc = 0;
|
||||
|
||||
const stdin = process.openStdin();
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
// close handler - reset terminal on exit
|
||||
stdin.addListener('data', function(data) {
|
||||
if (data[0] === 0x03) {
|
||||
process.stdin.setRawMode(false);
|
||||
process.stdout.write('\x1bc');
|
||||
process.exit();
|
||||
}
|
||||
if (data[0] === 0x01) {
|
||||
switchActiveProtocol();
|
||||
printMenu();
|
||||
}
|
||||
if (data[0] === 0x02) {
|
||||
switchActiveEnc();
|
||||
printMenu();
|
||||
}
|
||||
console.log('\x1b[100;H\x1b[2A\x1b[2KReport:', data, [data.toString('binary')]);
|
||||
// filter mouse reports
|
||||
if (data[0] === 0x1b && data[1] === '['.charCodeAt(0)) {
|
||||
applyReportData(data);
|
||||
}
|
||||
});
|
||||
|
||||
// button definitions
|
||||
const buttons = {
|
||||
'<none>': -1,
|
||||
left: 0,
|
||||
middle: 1,
|
||||
right: 2,
|
||||
released: 3,
|
||||
wheelUp: 4,
|
||||
wheelDown: 5,
|
||||
wheelLeft: 6,
|
||||
wheelRight: 7,
|
||||
aux1: 8,
|
||||
aux2: 9,
|
||||
aux3: 10,
|
||||
aux4: 11,
|
||||
aux5: 12,
|
||||
aux6: 13,
|
||||
aux7: 14,
|
||||
aux8: 15
|
||||
};
|
||||
const reverseButtons = {};
|
||||
for (const el in buttons) {
|
||||
reverseButtons[buttons[el]] = el;
|
||||
}
|
||||
|
||||
// extract button data from buttonCode
|
||||
function evalButtonCode(code) {
|
||||
// more than 15 buttons are not supported
|
||||
if (code > 255) {
|
||||
return {button: 'invalid', action: 'invalid', modifier: {}};
|
||||
}
|
||||
const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)};
|
||||
const move = code & 32;
|
||||
let button = code & 3;
|
||||
if (code & 128) {
|
||||
button |= 8;
|
||||
}
|
||||
if (code & 64) {
|
||||
button |= 4
|
||||
}
|
||||
let actionS = 'press';
|
||||
let buttonS = reverseButtons[button];
|
||||
if (button === 3) {
|
||||
buttonS = '<none>';
|
||||
actionS = 'release';
|
||||
}
|
||||
if (move) {
|
||||
actionS = 'move';
|
||||
} else if (4 <= button && button <= 7) {
|
||||
buttonS = 'wheel';
|
||||
actionS = button === 4 ? 'up' : button === 5 ? 'down' : button === 6 ? 'left' : 'right';
|
||||
}
|
||||
return {button: buttonS, action: actionS, modifier};
|
||||
}
|
||||
|
||||
// protocols
|
||||
const PROTOCOLS = {
|
||||
'9 (X10: press only)': '\x1b[?9h',
|
||||
'1000 (VT200: press, release, wheel)': '\x1b[?1000h',
|
||||
// '1001 (VT200 highlight)': '\x1b[?1001h', // handle of backreport - not implemented
|
||||
'1002 (press, release, move on pressed, wheel)': '\x1b[?1002h',
|
||||
'1003 (press, relase, move, wheel)': '\x1b[?1003h'
|
||||
}
|
||||
|
||||
// encodings: ENCODING_NAME => [sequence, parse_report]
|
||||
const ENC = {
|
||||
'DEFAULT' : [
|
||||
'',
|
||||
// format: CSI M <button + 32> <row + 32> <col + 32>
|
||||
report => ({
|
||||
state: evalButtonCode(report[3] - 32),
|
||||
col: report[4] - 32,
|
||||
row: report[5] - 32
|
||||
})
|
||||
],
|
||||
'UTF8' : [
|
||||
'\x1b[?1005h',
|
||||
// format: CSI M <button + 32> <row + 32> <col + 32>
|
||||
// + utf8 encoding on row/col
|
||||
report => {
|
||||
const sReport = report.toString(); // decode with utf8
|
||||
return {
|
||||
state: evalButtonCode(sReport.charCodeAt(3) - 32),
|
||||
col: sReport.charCodeAt(4) - 32,
|
||||
row: sReport.charCodeAt(5) - 32
|
||||
};
|
||||
}
|
||||
],
|
||||
'SGR' : [
|
||||
'\x1b[?1006h',
|
||||
// format: CSI < Pbutton ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(3, -1);
|
||||
const [buttonCode, col, row] = sReport.split(';');
|
||||
const state = evalButtonCode(buttonCode);
|
||||
if (report[report.length - 1] === 'm'.charCodeAt(0)) {
|
||||
state.action = 'release';
|
||||
}
|
||||
return {state, row, col};
|
||||
}
|
||||
],
|
||||
'URXVT': [
|
||||
'\x1b[?1015h',
|
||||
// format: CSI <button + 32> ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(2, -1);
|
||||
const [button, col, row] = sReport.split(';');
|
||||
return {state: evalButtonCode(button - 32), row, col};
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function printMenu() {
|
||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log();
|
||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||
const protocols = Object.keys(PROTOCOLS);
|
||||
for (let i = 0; i < protocols.length; ++i) {
|
||||
console.log(` ${activeProtocol === i ? '->' : ' '} ${protocols[i]}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(' Selected encoding [Ctrl-B to switch]');
|
||||
const encs = Object.keys(ENC);
|
||||
for (let i = 0; i < encs.length; ++i) {
|
||||
console.log(` ${activeEnc === i ? '->' : ' '} ${encs[i]}`);
|
||||
}
|
||||
process.stdout.write('\x1b[100;H');
|
||||
}
|
||||
|
||||
function switchActiveProtocol() {
|
||||
activeProtocol++;
|
||||
activeProtocol %= Object.keys(PROTOCOLS).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function switchActiveEnc() {
|
||||
activeEnc++;
|
||||
activeEnc %= Object.keys(ENC).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function activate() {
|
||||
// clear all protocols and encodings
|
||||
process.stdout.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l');
|
||||
process.stdout.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l');
|
||||
// apply new protocol and encoding
|
||||
process.stdout.write(PROTOCOLS[Object.keys(PROTOCOLS)[activeProtocol]]);
|
||||
process.stdout.write(ENC[Object.keys(ENC)[activeEnc]][0]);
|
||||
console.log('\x1b[100;H\x1b[2A\x1b[2KReport:');
|
||||
}
|
||||
|
||||
function applyReportData(data) {
|
||||
let {state, row, col} = ENC[Object.keys(ENC)[activeEnc]][1](data);
|
||||
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
|
||||
// apply to cursor position
|
||||
process.stdout.write(`\x1b[${row};${col}H`);
|
||||
}
|
||||
|
||||
printMenu();
|
||||
activate();
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Name the arguments
|
||||
VERSION=$1
|
||||
|
||||
# Clone docs repo and update the documentation
|
||||
git clone https://github.com/xtermjs/xtermjs.org
|
||||
cd xtermjs.org
|
||||
yarn
|
||||
./bin/update-docs
|
||||
|
||||
# Add changes to index and only proceed if there are changes to commit
|
||||
touch test-file
|
||||
git add .
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
|
||||
# Delete the upstream branch if it exists for some reason
|
||||
export BRANCH_NAME=update-$VERSION
|
||||
git branch -D $BRANCH_NAME || true
|
||||
git push origin :$BRANCH_NAME || true
|
||||
|
||||
# Create commit and push it to update-x.y.z
|
||||
git checkout -b $BRANCH_NAME
|
||||
git config --global user.name Daniel Imms
|
||||
git config --global user.email tyriar@tyriar.com
|
||||
git commit -m 'Update docs for v$VERSION'
|
||||
git push --set-upstream origin update-4.2.0
|
||||
git push -f
|
||||
|
||||
# Create a PR in the GitHub repo
|
||||
curl \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-X POST \
|
||||
-d "{\"title\":\"Update docs for v$VERSION\",\"base\":\"master\",\"head\":\"xtermjs:$BRANCH_NAME\"}" \
|
||||
https://api.github.com/repos/xtermjs/xtermjs.org/pulls
|
||||
|
||||
else
|
||||
|
||||
echo "No changes to commit"
|
||||
|
||||
fi
|
||||
+51
-76
@@ -7,40 +7,33 @@
|
||||
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
// Use tsc version (yarn watch)
|
||||
import { Terminal } from '../out/public/Terminal';
|
||||
import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon';
|
||||
import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon';
|
||||
import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon';
|
||||
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
|
||||
import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
|
||||
|
||||
// Use webpacked version (yarn package)
|
||||
// import { Terminal } from '../lib/xterm';
|
||||
// import { AttachAddon } from 'xterm-addon-attach';
|
||||
// import { FitAddon } from 'xterm-addon-fit';
|
||||
// import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
|
||||
// import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
// import { WebglAddon } from 'xterm-addon-webgl';
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
import * as attach from '../lib/addons/attach/attach';
|
||||
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;
|
||||
Terminal?: typeof TerminalType;
|
||||
AttachAddon?: typeof AttachAddon;
|
||||
FitAddon?: typeof FitAddon;
|
||||
SearchAddon?: typeof SearchAddon;
|
||||
WebLinksAddon?: typeof WebLinksAddon;
|
||||
WebglAddon?: typeof WebglAddon;
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
let term;
|
||||
let fitAddon: FitAddon;
|
||||
let searchAddon: SearchAddon;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
@@ -58,15 +51,16 @@ function setPadding(): void {
|
||||
term.fit();
|
||||
}
|
||||
|
||||
function getSearchOptions(e: KeyboardEvent): ISearchOptions {
|
||||
function getSearchOptions(): ISearchOptions {
|
||||
return {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
|
||||
incremental: e.key !== `Enter`
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
}
|
||||
|
||||
createTerminal();
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
// If the terminal exists dispose of it, otherwise recreate it
|
||||
if (term) {
|
||||
@@ -82,40 +76,16 @@ const disposeRecreateButtonHandler = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (document.location.pathname === '/test') {
|
||||
window.Terminal = Terminal;
|
||||
window.AttachAddon = AttachAddon;
|
||||
window.FitAddon = FitAddon;
|
||||
window.SearchAddon = SearchAddon;
|
||||
window.WebLinksAddon = WebLinksAddon;
|
||||
window.WebglAddon = WebglAddon;
|
||||
} else {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
document.getElementById('webgl').addEventListener('click', () => term.loadAddon(new WebglAddon()));
|
||||
}
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
|
||||
function createTerminal(): void {
|
||||
// Clean terminal
|
||||
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);
|
||||
|
||||
// Load addons
|
||||
const typedTerm = term as TerminalType;
|
||||
typedTerm.loadAddon(new WebLinksAddon());
|
||||
searchAddon = new SearchAddon();
|
||||
typedTerm.loadAddon(searchAddon);
|
||||
fitAddon = new FitAddon();
|
||||
typedTerm.loadAddon(fitAddon);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -129,17 +99,23 @@ function createTerminal(): void {
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
fitAddon.fit();
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
searchAddon.findNext(actionElements.findNext.value, getSearchOptions(e));
|
||||
const searchOptions = getSearchOptions();
|
||||
searchOptions.incremental = e.key !== `Enter`;
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
});
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
|
||||
if (e.key === `Enter`) {
|
||||
term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
}
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
@@ -167,7 +143,7 @@ function createTerminal(): void {
|
||||
}
|
||||
|
||||
function runRealTerminal(): void {
|
||||
term.loadAddon(new AttachAddon(socket));
|
||||
term.attach(socket);
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
@@ -188,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 {
|
||||
@@ -210,6 +189,7 @@ function initOptions(term: TerminalType): void {
|
||||
// Internal only options
|
||||
'cancelEvents',
|
||||
'convertEol',
|
||||
'debug',
|
||||
'handler',
|
||||
'screenKeys',
|
||||
'termName',
|
||||
@@ -221,13 +201,11 @@ function initOptions(term: TerminalType): void {
|
||||
bellSound: null,
|
||||
bellStyle: ['none', 'sound'],
|
||||
cursorStyle: ['block', 'underline', 'bar'],
|
||||
fastScrollModifier: ['alt', 'ctrl', 'shift', undefined],
|
||||
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
||||
fontFamily: null,
|
||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
logLevel: ['debug', 'info', 'warn', 'error', 'off'],
|
||||
rendererType: ['dom', 'canvas'],
|
||||
wordSeparator: null
|
||||
rendererType: ['dom', 'canvas']
|
||||
};
|
||||
const options = Object.keys((<any>term)._core.options);
|
||||
const booleanOptions = [];
|
||||
@@ -254,7 +232,7 @@ function initOptions(term: TerminalType): void {
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
@@ -283,11 +261,8 @@ function initOptions(term: TerminalType): void {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'cols' || o === 'rows') {
|
||||
updateTerminalSize();
|
||||
} else if (o === 'lineHeight' || o === 'scrollSensitivity') {
|
||||
term.setOption(o, parseFloat(input.value));
|
||||
updateTerminalSize();
|
||||
} else {
|
||||
term.setOption(o, parseInt(input.value));
|
||||
term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -308,9 +283,9 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
|
||||
function updateTerminalSize(): void {
|
||||
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
|
||||
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
|
||||
const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
|
||||
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
fitAddon.fit();
|
||||
term.fit();
|
||||
}
|
||||
|
||||
+4
-5
@@ -2,9 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||
<link rel="stylesheet" href="/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.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>
|
||||
</head>
|
||||
@@ -16,7 +16,7 @@
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||
</p>
|
||||
@@ -36,7 +36,6 @@
|
||||
<hr/>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<button id="webgl">Enable WebGL Renderer</button>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
+13
-44
@@ -3,9 +3,6 @@ var expressWs = require('express-ws');
|
||||
var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
|
||||
// Whether to use binary transport.
|
||||
const USE_BINARY = os.platform() !== "win32";
|
||||
|
||||
function startServer() {
|
||||
var app = express();
|
||||
expressWs(app);
|
||||
@@ -13,38 +10,29 @@ function startServer() {
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
|
||||
app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
res.sendFile(__dirname + '/logo.png');
|
||||
});
|
||||
app.use('/build', express.static(__dirname + '/../build'));
|
||||
|
||||
app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
res.sendFile(__dirname + '/test.html');
|
||||
});
|
||||
|
||||
app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
|
||||
app.use('/dist', express.static(__dirname + '/dist'));
|
||||
app.use('/src', express.static(__dirname + '/src'));
|
||||
app.get('/dist/client-bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||
});
|
||||
|
||||
app.post('/terminals', (req, res) => {
|
||||
const env = Object.assign({}, process.env);
|
||||
env['COLORTERM'] = 'truecolor';
|
||||
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', [], {
|
||||
name: 'xterm-256color',
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd: env.PWD,
|
||||
env: env,
|
||||
encoding: USE_BINARY ? null : 'utf8'
|
||||
cwd: process.env.PWD,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
@@ -57,7 +45,7 @@ function startServer() {
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.post('/terminals/:pid/size', (req, res) => {
|
||||
app.post('/terminals/:pid/size', function (req, res) {
|
||||
var pid = parseInt(req.params.pid),
|
||||
cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
@@ -73,7 +61,6 @@ function startServer() {
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
|
||||
// string message buffering
|
||||
function buffer(socket, timeout) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
@@ -88,25 +75,7 @@ function startServer() {
|
||||
}
|
||||
};
|
||||
}
|
||||
// binary message buffering
|
||||
function bufferUtf8(socket, timeout) {
|
||||
let buffer = [];
|
||||
let sender = null;
|
||||
let length = 0;
|
||||
return (data) => {
|
||||
buffer.push(data);
|
||||
length += data.length;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(buffer, length));
|
||||
buffer = [];
|
||||
sender = null;
|
||||
length = 0;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||
const send = buffer(ws, 5);
|
||||
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
@@ -130,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);
|
||||
}
|
||||
|
||||
|
||||
+3
-22
@@ -11,16 +11,7 @@ const startServer = require('./server.js');
|
||||
|
||||
startServer();
|
||||
|
||||
/**
|
||||
* This webpack config builds and watches the demo project. It works by taking the output from tsc
|
||||
* (via `yarn watch`) which is put into `out/` and then webpacks it into `demo/dist/`. The aliases
|
||||
* are used fix up the absolute paths output by tsc (because of `baseUrl` and `paths` in
|
||||
* `tsconfig.json`.
|
||||
*
|
||||
* For production builds see `webpack.config.js` in the root directory. If that is built the demo
|
||||
* can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for
|
||||
* validating that the packaged version works correctly.
|
||||
*/
|
||||
// Build/watch client source
|
||||
const clientConfig = {
|
||||
entry: path.resolve(__dirname, 'client.ts'),
|
||||
devtool: 'inline-source-map',
|
||||
@@ -34,22 +25,12 @@ const clientConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: [
|
||||
'node_modules',
|
||||
path.resolve(__dirname, '..'),
|
||||
path.resolve(__dirname, '../addons')
|
||||
],
|
||||
extensions: [ '.tsx', '.ts', '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('./out/common'),
|
||||
browser: path.resolve('./out/browser')
|
||||
}
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'client-bundle.js',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js integration test fixture</title>
|
||||
<link rel="stylesheet" href="/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal-container"></div>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-10
@@ -1,17 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"target": "es5",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"xterm-addon-attach": ["../addons/xterm-addon-attach"],
|
||||
"xterm-addon-fit": ["../addons/xterm-addon-fit"],
|
||||
"xterm-addon-search": ["../addons/xterm-addon-search"],
|
||||
"xterm-addon-web-links": ["../addons/xterm-addon-web-links"],
|
||||
"xterm-addon-webgl": ["../addons/xterm-addon-webgl"]
|
||||
}
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"client.ts",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
from glob import glob
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import atexit
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def enable_echo(fd, enabled):
|
||||
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(fd)
|
||||
if enabled:
|
||||
lflag |= termios.ECHO
|
||||
else:
|
||||
lflag &= ~termios.ECHO
|
||||
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
|
||||
termios.tcsetattr(fd, termios.TCSANOW, new_attr)
|
||||
|
||||
atexit.register(enable_echo, sys.stdin.fileno(), True)
|
||||
|
||||
output = []
|
||||
|
||||
|
||||
def log(append=False, *s):
|
||||
if append:
|
||||
output[-1] += ' ' + ' '.join(str(part) for part in s)
|
||||
else:
|
||||
output.append(' '.join(str(part) for part in s))
|
||||
|
||||
|
||||
def reset_terminal():
|
||||
sys.stdout.write('\x1bc\x1b[H')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def test():
|
||||
count = 0
|
||||
passed = 0
|
||||
for i, testfile in enumerate(sorted(glob(os.path.join(BASE_DIR, '*.in')))):
|
||||
count += 1
|
||||
log(False, os.path.basename(testfile))
|
||||
reset_terminal()
|
||||
with open(testfile) as test:
|
||||
sys.stdout.write('\x1b]0;%s\x07' % os.path.basename(testfile))
|
||||
sys.stdout.write(test.read()+'\x1bt')
|
||||
sys.stdout.flush()
|
||||
with open(os.path.join(os.path.dirname(testfile),
|
||||
os.path.basename(testfile).split('.')[0]+'.text')) as expected:
|
||||
terminal_output = sys.stdin.read()
|
||||
if not terminal_output:
|
||||
# we are in xterm
|
||||
continue
|
||||
if terminal_output != expected.read():
|
||||
log(True, '\x1b[31merror\x1b[0m')
|
||||
with open(os.path.join(os.path.dirname(testfile), 'output',
|
||||
os.path.basename(testfile)), 'w') as t_out:
|
||||
t_out.write(terminal_output)
|
||||
else:
|
||||
passed += 1
|
||||
log(True, '\x1b[32mpass\x1b[0m')
|
||||
return count, passed
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
enable_echo(sys.stdin.fileno(), False)
|
||||
count, passed = test()
|
||||
enable_echo(sys.stdin.fileno(), True)
|
||||
reset_terminal()
|
||||
for i in range(len(output)/2+1):
|
||||
if not (i+1) % 25:
|
||||
sys.stdin.read()
|
||||
print ''.join(i.ljust(40) for i in output[i*2:i*2+2])
|
||||
print '\x1b[33mcoverage: %s/%s (%d%%) tests passed.\x1b[0m' % (passed, count, passed*100/count)
|
||||
@@ -27,5 +27,5 @@ r
|
||||
s
|
||||
t
|
||||
u[78Gvwxyz
|
||||
[r[25d
|
||||
[r[24d
|
||||
The end.
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
6
|
||||
7
|
||||
8
|
||||
9 ABC
|
||||
1
|
||||
2
|
||||
6
|
||||
7
|
||||
8
|
||||
9 ABC
|
||||
DEF
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
yz vwx
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
yz qrstu vwx
|
||||
The end.
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[3;7r1D2D3D4D5D6D7D8D9[78GABCDEF[8daDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDu[78Gvwxyz
|
||||
[r[25d
|
||||
[r[24d
|
||||
The end.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
2
|
||||
6
|
||||
7
|
||||
8
|
||||
@@ -20,6 +21,5 @@ DEF
|
||||
o
|
||||
p
|
||||
q
|
||||
yz rstu vwx
|
||||
The end.
|
||||
The end. rstu vwx
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
b
|
||||
c
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
|
||||
|
||||
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
w
|
||||
x
|
||||
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
w
|
||||
x
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x[10;19r[25E1[24d[25F2[r[24d
|
||||
@@ -1,25 +0,0 @@
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
2
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
1
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x[10;19r[H[25e1[r[24d
|
||||
@@ -1,25 +0,0 @@
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
h
|
||||
i
|
||||
j
|
||||
k
|
||||
l
|
||||
m
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x
|
||||
1
|
||||
@@ -1,25 +1,25 @@
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x
|
||||
y
|
||||
z
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
|
||||
11
|
||||
n
|
||||
o
|
||||
p
|
||||
q
|
||||
r
|
||||
s
|
||||
t
|
||||
u
|
||||
v
|
||||
w
|
||||
x
|
||||
y
|
||||
z
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
|
||||
11
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
efgh
|
||||
-------- set: wraparound ----------------------------------------------abcd
|
||||
efgh
|
||||
-------- unset: no wraparound -------------------------------------------abch
|
||||
-------- unset: no wraparound -------------------------------------------abcd
|
||||
this should be immediately below "no wraparound"
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
Test of autowrap, mixing control and print characters.
|
||||
|
||||
|
||||
The left/right margins should have letters in order:
|
||||
|
||||
|
||||
[3;21r[?6h[19;1HA[19;80Ha
|
||||
[18;80HaB[19;80HB b
|
||||
[19;80HC c[19;2HC
|
||||
[19;80H
|
||||
[18;1HD[18;80Hd[19;1HE[19;80He
|
||||
[18;80HeF[19;80HF f
|
||||
[19;80HG g[19;2HG
|
||||
[19;80H
|
||||
[18;1HH[18;80Hh[19;1HI[19;80Hi
|
||||
[18;80HiJ[19;80HJ j
|
||||
[19;80HK k[19;2HK
|
||||
[19;80H
|
||||
[18;1HL[18;80Hl[19;1HM[19;80Hm
|
||||
[18;80HmN[19;80HN n
|
||||
[19;80HO o[19;2HO
|
||||
[19;80H
|
||||
[18;1HP[18;80Hp[19;1HQ[19;80Hq
|
||||
[18;80HqR[19;80HR r
|
||||
[19;80HS s[19;2HS
|
||||
[19;80H
|
||||
[18;1HT[18;80Ht[19;1HU[19;80Hu
|
||||
[18;80HuV[19;80HV v
|
||||
[19;80HW w[19;2HW
|
||||
[19;80H
|
||||
[18;1HX[18;80Hx[19;1HY[19;80Hy
|
||||
[18;80HyZ[19;80HZ z
|
||||
[?6l[r[22;1HPush <RETURN>
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
Test of autowrap, mixing control and print characters.
|
||||
|
||||
I i
|
||||
J j
|
||||
K k
|
||||
L l
|
||||
M m
|
||||
N n
|
||||
O o
|
||||
P p
|
||||
Q q
|
||||
R r
|
||||
S s
|
||||
T t
|
||||
U u
|
||||
V v
|
||||
W w
|
||||
X x
|
||||
Y y
|
||||
Z z
|
||||
|
||||
Push <RETURN>
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user