Compare commits

..
3 Commits
Author SHA1 Message Date
Daniel Imms 894d067990 v3.10.1 2019-01-08 08:14:46 -08:00
Daniel Imms eeca95b68a Remove request term info handler
We're not supporting it anyway, no point having it.
2019-01-07 14:47:06 -08:00
Daniel Imms 1964e52cd9 Don't print keys that print more than a single char
Fixes #1880
2019-01-07 14:44:00 -08:00
316 changed files with 17332 additions and 29321 deletions
-22
View File
@@ -1,22 +0,0 @@
FROM node:10
# Configure apt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils 2>&1
# Verify git and process tools are installed
RUN apt-get install -y git procps
# Install yarn
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends yarn
# Clean up
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog
-9
View File
@@ -1,9 +0,0 @@
{
"name": "xterm.js",
"dockerFile": "Dockerfile",
"appPort": 3000,
"extensions": [
"editorconfig.editorconfig",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}
+5 -6
View File
@@ -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
View File
@@ -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/
+6 -28
View File
@@ -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
}
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"typescript.preferences.quoteStyle": "single"
}
-51
View File
@@ -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"
}
}
]
}
+6 -6
View File
@@ -33,21 +33,21 @@ opening an issue, read these pointers.
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make your changes.
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
- Make your changes
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you:
By contributing code to xterm.js you
- Agree to license the contributed code under xterm.js' [MIT
- agree to license the contributed code under xterm.js' [MIT
license](LICENSE).
- Confirm that you have the right to contribute and license the code
- confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
+54 -64
View File
@@ -1,22 +1,23 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](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.
- **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.
- **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
- **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
- **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
- Xterm.js is not a terminal application that you can download and use on your computer
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output)
## Getting Started
@@ -32,15 +33,15 @@ 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>
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
@@ -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.
@@ -100,17 +106,17 @@ Note that some APIs are marked *experimental*, these are added to enable experim
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
@@ -118,8 +124,9 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
@@ -130,30 +137,23 @@ 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.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS.
- [**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.
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users.
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
- [**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.
- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
@@ -163,17 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
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.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
## Contributing
@@ -183,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)
-2
View File
@@ -1,2 +0,0 @@
lib
node_modules
-5
View File
@@ -1,5 +0,0 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
-19
View File
@@ -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.
-22
View File
@@ -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.
-21
View File
@@ -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}`]
});
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'
};
-2
View File
@@ -1,2 +0,0 @@
lib
node_modules
-5
View File
@@ -1,5 +0,0 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
-19
View File
@@ -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.
-24
View File
@@ -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.
-21
View File
@@ -1,21 +0,0 @@
{
"name": "xterm-addon-fit",
"version": "0.2.1",
"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"
}
}
-79
View File
@@ -1,79 +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;
}
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.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.floor(availableWidth / core._renderService.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)
};
return geometry;
}
}
-55
View File
@@ -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;
}
}
-31
View File
@@ -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'
};
-2
View File
@@ -1,2 +0,0 @@
lib
node_modules
-5
View File
@@ -1,5 +0,0 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
-19
View File
@@ -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.
-23
View File
@@ -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.
-21
View File
@@ -1,21 +0,0 @@
{
"name": "xterm-addon-search",
"version": "0.2.1",
"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,124 +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(200000);
before(async function (): Promise<any> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal();
await page.evaluate(`window.search = new SearchAddon();`);
await page.evaluate(`window.term.loadAddon(window.search);`);
});
after(() => {
browser.close();
});
beforeEach(async () => {
await page.evaluate(`window.term.reset()`);
});
it('Simple Search', async () => {
await writeSync('dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
});
it('Scrolling Search', async () => {
let dataString = '';
for (let i = 0; i < 100; i++) {
if (i === 52) {
dataString += '$^1_3{}test$#';
}
dataString += makeData(50);
}
await writeSync(dataString);
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
});
it ('Incremental Find Previous', async () => {
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
await writeSync('package.json pack package.lock');
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
});
it ('Incremental Find Next', async () => {
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
await writeSync('package.jsonc');
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
});
it ('Simple Regex', async () => {
await writeSync('abc123defABCD');
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
async function writeSync(data: string): Promise<void> {
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'
};
-2
View File
@@ -1,2 +0,0 @@
lib
node_modules
-5
View File
@@ -1,5 +0,0 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
-19
View File
@@ -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.
-21
View File
@@ -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.
-21
View File
@@ -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}`]
});
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'
};
-2
View File
@@ -1,2 +0,0 @@
lib
node_modules
-5
View File
@@ -1,5 +0,0 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
-19
View File
@@ -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.
-23
View File
@@ -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.
-21
View File
@@ -1,21 +0,0 @@
{
"name": "xterm-addon-webgl",
"version": "0.2.1",
"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;
}
-15
View File
@@ -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,374 +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));
if (program === undefined) {
throw new Error('Could not create WebGL program');
}
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
View File
@@ -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}`]
});
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, CHAR_DATA_ATTR_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,358 +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;
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 = terminal.getOption('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" }
]
}
-26
View File
@@ -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
View File
@@ -20,8 +20,8 @@ jobs:
yarn
displayName: 'Install dependencies and build'
- script: |
yarn test-unit
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
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
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
displayName: 'Linux Integration tests'
- job: macOS_IntegrationTests
pool:
vmImage: 'xcode9-macos10.13'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
yarn test-api --headless
displayName: 'MacOS Integration tests'
- job: Release
dependsOn:
- Linux
- macOS
- Windows
- Linux_IntegrationTests
- macOS_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*')))
pool:
vmImage: 'ubuntu-16.04'
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'
+24
View File
@@ -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
-117
View File
@@ -1,117 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
// 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
if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
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);
}
});
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();
}
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;
}
Executable
+22
View File
@@ -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
-34
View File
@@ -1,34 +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'
];
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
if (process.argv.length > 2) {
testFiles = process.argv.slice(2);
}
const run = cp.spawnSync(
path.resolve(__dirname, '../node_modules/.bin/mocha'),
testFiles,
{
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'inherit'
}
);
process.exit(run.status);
-212
View File
@@ -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();
+50 -73
View File
@@ -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,12 +201,12 @@ function initOptions(term: TerminalType): void {
bellSound: null,
bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'],
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
experimentalBufferLineImpl: ['JsArray', 'TypedArray']
};
const options = Object.keys((<any>term)._core.options);
const booleanOptions = [];
@@ -282,11 +262,8 @@ function initOptions(term: TerminalType): void {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else if (o === 'lineHeight') {
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));
}
});
});
@@ -307,9 +284,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
View File
@@ -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>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

+9 -38
View File
@@ -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,36 +10,29 @@ function startServer() {
var terminals = {},
logs = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
app.use('/build', express.static(__dirname + '/../build'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/test', function(req, res){
res.sendFile(__dirname + '/test.html');
});
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', function (req, res) {
const env = Object.assign({}, process.env);
env['COLORTERM'] = 'truecolor';
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);
@@ -71,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;
@@ -86,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 {
@@ -128,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);
}
+4 -22
View File
@@ -5,22 +5,14 @@
* This file is the entry point for browserify.
*/
const cp = require('child_process');
const path = require('path');
const webpack = require('webpack');
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 +26,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',
-12
View File
@@ -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
View File
@@ -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",
+87
View File
@@ -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);
+128
View File
@@ -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>
&nbsp;
<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>
&nbsp;
<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 &lt;file&gt;</code> to send from the remote peer.</p>
<script src="main.js" defer ></script>
</body>
</html>
+388
View File
@@ -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 -1
View File
@@ -7,7 +7,7 @@ services:
volumes:
- ./:/usr/src/app
ports:
- ${XTERMJS_PORT:-3000}:3000
- ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"]
watch:
@@ -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
uvwxyz


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 @@
1D2D3D4D5D6D7D8D9ABCDEFaDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDuvwxyz


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
x12
@@ -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
x1
@@ -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:
[?6hAa
aBB b
C cC

DdEe
eFF f
G gG

HhIi
iJJ j
K kK

LlMm
mNN n
O oO

PpQq
qRR r
S sS

TtUu
uVV v
W wW

XxYy
yZZ z
[?6lPush <RETURN>

Some files were not shown because too many files have changed in this diff Show More