diff --git a/.vscode/launch.json b/.vscode/launch.json index c7bf7381..e5bad7b3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,10 +23,7 @@ "type": "chrome", "request": "launch", "name": "Demo Client", - "url": "http://0.0.0.0:3000", - "windows": { - "url": "http://127.0.0.1:3000" - }, + "url": "http://127.0.0.1:3000", "webRoot": "${workspaceFolder}/" }, { @@ -38,7 +35,11 @@ "run", "start-debug" ], - "port": 9229 + "port": 9229, + "serverReadyAction": { + "action": "openExternally", + "pattern": "App listening to (http://.*?:[0-9]+)" + } } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7924027..e8102a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/LICENSE b/LICENSE index 28adbdad..4472336c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) +Copyright (c) 2017-2019, 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/) diff --git a/README.md b/README.md index 35ccc9b6..1fa8ad41 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## 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 -- **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 +- **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 @@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t @@ -106,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. @@ -124,9 +124,8 @@ 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. @@ -137,23 +136,27 @@ computational environment for Jupyter, supporting interactive data science and s - [**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): 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 +- [**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. - [**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. +- [**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. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) @@ -163,7 +166,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), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). +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). ## Contributing diff --git a/demo/client.ts b/demo/client.ts index a38b8a5c..17cb4857 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,12 +13,11 @@ 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 above, it's looks a // little weird here as we're importing "this" module -import { Terminal as TerminalType } from 'xterm'; +import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; @@ -30,7 +29,6 @@ Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); -Terminal.applyAddon(winptyCompat); let term; @@ -83,7 +81,10 @@ function createTerminal(): void { while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); } - term = new Terminal({}); + const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; + term = new Terminal({ + windowsMode: isWindows + } as ITerminalOptions); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', (size: { cols: number, rows: number }) => { if (!pid) { @@ -99,7 +100,7 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.winptyCompatInit(); + term.webLinksInit(); term.fit(); term.focus(); @@ -172,7 +173,7 @@ function runFakeTerminal(): void { term.prompt(); } else if (ev.keyCode === 8) { // Do not delete the prompt - if (term.x > 2) { + if (term._core.buffer.x > 2) { term.write('\b \b'); } } else if (printable) { diff --git a/demo/index.html b/demo/index.html index 12f7cb98..370a51ed 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,8 +2,8 @@ xterm.js demo - - + + diff --git a/demo/server.js b/demo/server.js index 86bc9069..513d8b27 100644 --- a/demo/server.js +++ b/demo/server.js @@ -10,7 +10,7 @@ function startServer() { var terminals = {}, logs = {}; - app.use('/build', express.static(__dirname + '/../build')); + app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); @@ -100,7 +100,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://' + host + ':' + port); + console.log('App listening to http://127.0.0.1:' + port); app.listen(port, host); } diff --git a/demo/zmodem/app.js b/demo/zmodem/app.js deleted file mode 100644 index 7124c222..00000000 --- a/demo/zmodem/app.js +++ /dev/null @@ -1,87 +0,0 @@ -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); diff --git a/demo/zmodem/index.html b/demo/zmodem/index.html deleted file mode 100644 index aee7742a..00000000 --- a/demo/zmodem/index.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - xterm.js demo - - - - - - - - - - - - - - - - -

xterm.js: xterm, in the browser

- -
- -
- - - - - - - - - -
- -
-

Actions

-

- - -

-
-
-

Options

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-
-

Size

-
-
- - -
-
- - -
-
-
-
-

Attention: 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.

-

* ZMODEM file transfers are supported via an addon. To try it out, install lrzsz onto the remote peer, then run rz to send from your browser or sz <file> to send from the remote peer.

- - - diff --git a/demo/zmodem/main.js b/demo/zmodem/main.js deleted file mode 100644 index 619ef2b8..00000000 --- a/demo/zmodem/main.js +++ /dev/null @@ -1,388 +0,0 @@ -"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); - }); -} diff --git a/docker-compose.yml b/docker-compose.yml index 8e6a2f46..6eefed89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./:/usr/src/app ports: - - ${XTERMJS_PORT:3000}:3000 + - ${XTERMJS_PORT:-3000}:3000 command: ["npm", "start"] watch: diff --git a/gulpfile.js b/gulpfile.js index 9af8d6e4..bbb4d6e1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,9 +16,9 @@ const ts = require('gulp-typescript'); const util = require('gulp-util'); const buildDir = process.env.BUILD_DIR || 'build'; -const tsProject = ts.createProject('tsconfig.json'); -let srcDir = tsProject.config.compilerOptions.rootDir; -let outDir = tsProject.config.compilerOptions.outDir; +const tsProject = ts.createProject('src/tsconfig.json'); +let srcDir = './src'; +let outDir = './lib'; const addons = fs.readdirSync(`${__dirname}/src/addons`); @@ -61,7 +61,7 @@ gulp.task('browserify', function() { }; let bundleStream = browserify(browserifyOptions) .bundle() - .pipe(source('xterm.js')) + .pipe(source(`xterm.js`)) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) .pipe(sourcemaps.write('./')) @@ -136,6 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () { }) }); -gulp.task('build', ['sorcery', 'sorcery-addons']); +gulp.task('build', ['css', 'sorcery', 'sorcery-addons']); gulp.task('test', ['mocha']); gulp.task('default', ['build']); diff --git a/package.json b/package.json index b223c55f..4f2f511f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.10.0", + "version": "3.12.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", @@ -16,7 +16,6 @@ "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", - "concurrently": "^3.5.1", "coveralls": "^3.0.1", "express": "4.13.4", "express-ws": "2.0.0-rc.1", @@ -39,8 +38,8 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.1", "utf8": "^3.0.0", + "typescript": "3.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", @@ -52,20 +51,16 @@ "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", - "pretest": "npm run layering", "test": "npm run mocha", "posttest": "npm run lint", "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "mocha": "gulp test", - "tsc": "tsc", - "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", + "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", - "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", - "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" + "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" } } diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index fa0121ad..877676c9 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -5,7 +5,7 @@ import * as Strings from './Strings'; import { ITerminal, IBuffer } from './Types'; -import { isMac } from './core/Platform'; +import { isMac } from './common/Platform'; import { RenderDebouncer } from './ui/RenderDebouncer'; import { addDisposableDomListener } from './ui/Lifecycle'; import { Disposable } from './common/Lifecycle'; diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 59475adb..6cd6c413 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,7 +5,7 @@ import { assert, expect } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR } from './Buffer'; +import { Buffer, DEFAULT_ATTR_DATA } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { BufferLine, CellData } from './BufferLine'; @@ -37,13 +37,13 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).getAsCharData; + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData(); buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { assert.equal(buffer.lines.get(y).length, INIT_COLS); for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData, blankLineChar); + assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData(), blankLineChar); } } }); @@ -184,7 +184,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -204,7 +204,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -518,6 +518,29 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(5, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), 'ab c'); + assert.equal(buffer.lines.get(1).translateToString(false), 'd '); + buffer.resize(6, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), 'ab cd'); + assert.equal(buffer.lines.get(1).translateToString(false), ' '); + }); it('should wrap wide characters correctly when reflowing larger', () => { buffer.fillViewportRows(); buffer.resize(12, 10); @@ -553,6 +576,32 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); }); + it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(3, 10); + assert.equal(buffer.y, 2); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(false), 'ab '); + assert.equal(buffer.lines.get(1).translateToString(false), ' cd'); + buffer.resize(2, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(false), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(false), ' '); + assert.equal(buffer.lines.get(2).translateToString(false), 'cd'); + }); it('should wrap wide characters correctly when reflowing smaller', () => { buffer.fillViewportRows(); buffer.resize(12, 10); @@ -683,7 +732,7 @@ describe('Buffer', () => { beforeEach(() => { // Add 10 empty rows to start for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } buffer.ybase = 10; }); @@ -742,7 +791,7 @@ describe('Buffer', () => { terminal.options.scrollback = 10; // Add 10 empty rows to start for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } buffer.y = 9; buffer.ybase = 10; @@ -877,7 +926,7 @@ describe('Buffer', () => { beforeEach(() => { // Add 10 empty rows to start for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } buffer.ybase = 10; }); @@ -940,7 +989,7 @@ describe('Buffer', () => { terminal.options.scrollback = 10; // Add 10 empty rows to start for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } buffer.ybase = 10; }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 04622cdf..2dbe20d6 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,15 +4,18 @@ */ import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; -import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; +import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine, CellData } from './BufferLine'; -import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { BufferLine, CellData, AttributeData } from './BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); + +export const DEFAULT_ATTR_DATA = new AttributeData(); + export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -29,7 +32,7 @@ export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; /** - * Whilespace cell. + * Whitespace cell. * This is meant as a replacement for empty cells when needed * during rendering lines to preserve correct aligment. */ @@ -55,7 +58,7 @@ export class Buffer implements IBuffer { public tabs: any; public savedY: number; public savedX: number; - public savedCurAttr: number; + public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); @@ -77,19 +80,29 @@ export class Buffer implements IBuffer { this.clear(); } - public getNullCell(fg: number = 0, bg: number = 0): ICellData { - this._nullCell.fg = fg; - this._nullCell.bg = bg; + public getNullCell(attr?: IAttributeData): ICellData { + if (attr) { + this._nullCell.fg = attr.fg; + this._nullCell.bg = attr.bg; + } else { + this._nullCell.fg = 0; + this._nullCell.bg = 0; + } return this._nullCell; } - public getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { - this._whitespaceCell.fg = fg; - this._whitespaceCell.bg = bg; + public getWhitespaceCell(attr?: IAttributeData): ICellData { + if (attr) { + this._whitespaceCell.fg = attr.fg; + this._whitespaceCell.bg = attr.bg; + } else { + this._whitespaceCell.fg = 0; + this._whitespaceCell.bg = 0; + } return this._whitespaceCell; } - public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { + public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); } @@ -121,10 +134,10 @@ export class Buffer implements IBuffer { /** * Fills the buffer's viewport with blank lines. */ - public fillViewportRows(fillAttr?: number): void { + public fillViewportRows(fillAttr?: IAttributeData): void { if (this.lines.length === 0) { if (fillAttr === undefined) { - fillAttr = DEFAULT_ATTR; + fillAttr = DEFAULT_ATTR_DATA; } let i = this._rows; while (i--) { @@ -154,7 +167,7 @@ export class Buffer implements IBuffer { */ public resize(newCols: number, newRows: number): void { // store reference to null cell with default attrs - const nullCell = this.getNullCell(DEFAULT_ATTR); + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Increase max length if needed before adjustments to allow space to fill // as required. @@ -236,7 +249,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback) { + if (this._isReflowEnabled) { this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk @@ -251,6 +264,10 @@ export class Buffer implements IBuffer { this._rows = newRows; } + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !this._terminal.options.windowsMode; + } + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; @@ -258,23 +275,23 @@ export class Buffer implements IBuffer { // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - this._reflowLarger(newCols); + this._reflowLarger(newCols, newRows); } else { this._reflowSmaller(newCols, newRows); } } - private _reflowLarger(newCols: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); + private _reflowLarger(newCols: number, newRows: number): void { + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA)); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); - this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); + this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); } } - private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { - const nullCell = this.getNullCell(DEFAULT_ATTR); + private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Adjust viewport based on number of items removed let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { @@ -282,7 +299,7 @@ export class Buffer implements IBuffer { if (this.y > 0) { this.y--; } - if (this.lines.length < this._rows) { + if (this.lines.length < newRows) { // Add an extra row at the bottom of the viewport this.lines.push(new BufferLine(newCols, nullCell)); } @@ -296,7 +313,7 @@ export class Buffer implements IBuffer { } private _reflowSmaller(newCols: number, newRows: number): void { - const nullCell = this.getNullCell(DEFAULT_ATTR); + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Gather all BufferLines that need to be inserted into the Buffer here so that they can be // batched up and only committed once const toInsert = []; @@ -337,7 +354,7 @@ export class Buffer implements IBuffer { // Add the new lines const newLines: BufferLine[] = []; for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; + const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine; newLines.push(newLine); } if (newLines.length > 0) { @@ -371,8 +388,8 @@ export class Buffer implements IBuffer { srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - // TODO: srcCol shoudl take trimmed length into account - srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols; + const wrappedLinesIndex = Math.max(srcLineIndex, 0); + srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols); } } @@ -387,7 +404,7 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._rows - 1) { + if (this.y < newRows - 1) { this.y++; this.lines.pop(); } else { diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 7dfcbd2c..5b029cb8 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -32,7 +32,7 @@ describe('CellData', () => { // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); @@ -40,7 +40,7 @@ describe('CellData', () => { // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); @@ -350,7 +350,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -363,7 +363,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 14518454..d2f7af40 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,7 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine, ICellData } from './Types'; +import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { stringFromCodePoint } from './core/input/TextDecoder'; @@ -34,7 +34,7 @@ const enum Cell { } /** - * Bitmasks and helper for accessing data in `content`. + * Bitmasks for accessing data in `content`. */ export const enum Content { /** @@ -52,15 +52,15 @@ export const enum Content { * set: `content |= Content.isCombined;` * clear: `content &= ~Content.isCombined;` */ - IS_COMBINED = 0x200000, // 1 << 21 + IS_COMBINED_MASK = 0x200000, // 1 << 21 /** * bit 1..22 mask to check whether a cell contains any string data * we need to check for codepoint and isCombined bits to see * whether a cell contains anything - * read: `isEmtpy = !(content & Content.hasContent)` + * read: `isEmpty = !(content & Content.hasContent)` */ - HAS_CONTENT = 0x3FFFFF, + HAS_CONTENT_MASK = 0x3FFFFF, /** * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) @@ -76,10 +76,127 @@ export const enum Content { WIDTH_SHIFT = 22 } + +export const enum Attributes { + /** + * bit 1..8 blue in RGB, color in P256 and P16 + */ + BLUE_MASK = 0xFF, + BLUE_SHIFT = 0, + PCOLOR_MASK = 0xFF, + PCOLOR_SHIFT = 0, + + /** + * bit 9..16 green in RGB + */ + GREEN_MASK = 0xFF00, + GREEN_SHIFT = 8, + + /** + * bit 17..24 red in RGB + */ + RED_MASK = 0xFF0000, + RED_SHIFT = 16, + + /** + * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) + */ + CM_MASK = 0x3000000, + CM_DEFAULT = 0, + CM_P16 = 0x1000000, + CM_P256 = 0x2000000, + CM_RGB = 0x3000000, + + /** + * bit 1..24 RGB room + */ + RGB_MASK = 0xFFFFFF +} + +export const enum FgFlags { + /** + * bit 27..31 (32th bit unused) + */ + INVERSE = 0x4000000, + BOLD = 0x8000000, + UNDERLINE = 0x10000000, + BLINK = 0x20000000, + INVISIBLE = 0x40000000 +} + +export const enum BgFlags { + /** + * bit 27..32 (upper 4 unused) + */ + ITALIC = 0x4000000, + DIM = 0x8000000 +} + +export class AttributeData implements IAttributeData { + static toColorRGB(value: number): IColorRGB { + return [ + value >>> Attributes.RED_SHIFT & 255, + value >>> Attributes.GREEN_SHIFT & 255, + value & 255 + ]; + } + static fromColorRGB(value: IColorRGB): number { + return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; + } + + public clone(): IAttributeData { + const newObj = new AttributeData(); + newObj.fg = this.fg; + newObj.bg = this.bg; + return newObj; + } + + // data + public fg: number = 0; + public bg: number = 0; + + // flags + public isInverse(): number { return this.fg & FgFlags.INVERSE; } + public isBold(): number { return this.fg & FgFlags.BOLD; } + public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isBlink(): number { return this.fg & FgFlags.BLINK; } + public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } + public isItalic(): number { return this.bg & BgFlags.ITALIC; } + public isDim(): number { return this.bg & BgFlags.DIM; } + + // color modes + public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } + public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } + public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } + public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } + + // colors + public getFgColor(): number { + switch (this.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + public getBgColor(): number { + switch (this.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } +} + /** * CellData - represents a single Cell in the terminal buffer. */ -export class CellData implements ICellData { +export class CellData extends AttributeData implements ICellData { /** Helper to create CellData from CharData. */ public static fromCharData(value: CharData): CellData { @@ -96,7 +213,7 @@ export class CellData implements ICellData { /** Whether cell contains a combined string. */ public isCombined(): number { - return this.content & Content.IS_COMBINED; + return this.content & Content.IS_COMBINED_MASK; } /** Width of the cell. */ @@ -106,7 +223,7 @@ export class CellData implements ICellData { /** JS string of the content. */ public getChars(): string { - if (this.content & Content.IS_COMBINED) { + if (this.content & Content.IS_COMBINED_MASK) { return this.combinedData; } if (this.content & Content.CODEPOINT_MASK) { @@ -155,7 +272,7 @@ export class CellData implements ICellData { } if (combined) { this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } @@ -206,11 +323,11 @@ export class BufferLine implements IBufferLine { const cp = content & Content.CODEPOINT_MASK; return [ this._data[index * CELL_SIZE + Cell.FG], - (content & Content.IS_COMBINED) + (content & Content.IS_COMBINED_MASK) ? this._combined[index] : (cp) ? stringFromCodePoint(cp) : '', content >> Content.WIDTH_SHIFT, - (content & Content.IS_COMBINED) + (content & Content.IS_COMBINED_MASK) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : cp ]; @@ -224,7 +341,7 @@ export class BufferLine implements IBufferLine { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } @@ -244,12 +361,12 @@ export class BufferLine implements IBufferLine { } /** Get FG cell component. */ - public getFG(index: number): number { + public getFg(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } /** Get BG cell component. */ - public getBG(index: number): number { + public getBg(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } @@ -259,7 +376,7 @@ export class BufferLine implements IBufferLine { * from real empty cells. * */ public hasContent(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; } /** @@ -269,7 +386,7 @@ export class BufferLine implements IBufferLine { */ public getCodePoint(index: number): number { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { return this._combined[index].charCodeAt(this._combined[index].length - 1); } return content & Content.CODEPOINT_MASK; @@ -277,13 +394,13 @@ export class BufferLine implements IBufferLine { /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK; } /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { return this._combined[index]; } if (content & Content.CODEPOINT_MASK) { @@ -294,13 +411,15 @@ export class BufferLine implements IBufferLine { } /** - * Load data at `index` into `cell`. + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. */ public loadCell(index: number, cell: ICellData): ICellData { - cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; - cell.fg = this._data[index * CELL_SIZE + Cell.FG]; - cell.bg = this._data[index * CELL_SIZE + Cell.BG]; - if (cell.content & Content.IS_COMBINED) { + const startIndex = index * CELL_SIZE; + cell.content = this._data[startIndex + Cell.CONTENT]; + cell.fg = this._data[startIndex + Cell.FG]; + cell.bg = this._data[startIndex + Cell.BG]; + if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; } return cell; @@ -310,7 +429,7 @@ export class BufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - if (cell.content & Content.IS_COMBINED) { + if (cell.content & Content.IS_COMBINED_MASK) { this._combined[index] = cell.combinedData; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; @@ -337,7 +456,7 @@ export class BufferLine implements IBufferLine { */ public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { // we already have a combined string, simply add this._combined[index] += stringFromCodePoint(codePoint); } else { @@ -347,7 +466,7 @@ export class BufferLine implements IBufferLine { // - set combined flag this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 - content |= Content.IS_COMBINED; + content |= Content.IS_COMBINED_MASK; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 @@ -473,7 +592,7 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT)) { + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } @@ -514,7 +633,7 @@ export class BufferLine implements IBufferLine { while (startCol < endCol) { const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; const cp = content & Content.CODEPOINT_MASK; - result += (content & Content.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 } return result; diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index d27d7c48..9247ed66 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -3,10 +3,9 @@ * @license MIT */ -import { BufferLine, CellData } from './BufferLine'; +import { BufferLine } from './BufferLine'; import { CircularList, IDeleteEvent } from './common/CircularList'; -import { IBufferLine } from './Types'; -import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; +import { IBufferLine, ICellData } from './Types'; export interface INewLayoutResult { layout: number[]; @@ -19,8 +18,7 @@ export interface INewLayoutResult { * @param lines The buffer lines. * @param newCols The columns after resize. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { - const nullCell = CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); +export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -49,11 +47,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n // Copy buffer data to new locations let destLineIndex = 0; - let destCol = wrappedLines[destLineIndex].getTrimmedLength(); + let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols); let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols); const srcRemainingCells = srcTrimmedTineLength - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); @@ -174,7 +172,7 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new */ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and // linesNeeded @@ -188,7 +186,7 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo break; } srcCol += newCols; - const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength(); + const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols); if (srcCol > oldTrimmedLength) { srcCol -= oldTrimmedLength; srcLine++; @@ -204,3 +202,19 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo return newLineLengths; } + +export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number { + // If this is the last row in the wrapped line, get the actual trimmed length + if (i === lines.length - 1) { + return lines[i].getTrimmedLength(); + } + // Detect whether the following line starts with a wide character and the end of the current line + // is null, if so then we can be pretty sure the null character should be excluded from the line + // length] + const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1; + const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2; + if (endsInNull && followingLineStartsWithWide) { + return cols - 1; + } + return cols; +} diff --git a/src/BufferSet.ts b/src/BufferSet.ts index f84757d1..268c2d88 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, IBufferSet } from './Types'; +import { ITerminal, IBufferSet, IAttributeData } from './Types'; import { Buffer } from './Buffer'; import { EventEmitter } from './common/EventEmitter'; @@ -77,7 +77,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Sets the alt Buffer of the BufferSet as its currently active Buffer */ - public activateAltBuffer(fillAttr?: number): void { + public activateAltBuffer(fillAttr?: IAttributeData): void { if (this._activeBuffer === this._alt) { return; } diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 31ad866b..840bef55 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -203,6 +203,8 @@ export class CompositionHelper { this._compositionView.style.top = cursorTop + 'px'; this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._terminal.options.fontFamily; + this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 24eb0884..01596f80 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,33 +5,33 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; -import { MockInputHandlingTerminal } from './ui/TestUtils.test'; -import { DEFAULT_ATTR } from './Buffer'; +import { MockInputHandlingTerminal, TestTerminal } from './ui/TestUtils.test'; +import { DEFAULT_ATTR_DATA } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; -import { CellData } from './BufferLine'; +import { CellData, Attributes, AttributeData } from './BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); terminal.buffer.x = 1; terminal.buffer.y = 2; - terminal.curAttr = 3; + terminal.curAttrData.fg = 3; const inputHandler = new InputHandler(terminal); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); - assert.equal(terminal.curAttr, 3); + assert.equal(terminal.curAttrData.fg, 3); // Change cursor position terminal.buffer.x = 10; terminal.buffer.y = 20; - terminal.curAttr = 30; + terminal.curAttrData.fg = 30; // Restore cursor position inputHandler.restoreCursor([]); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); - assert.equal(terminal.curAttr, 3); + assert.equal(terminal.curAttrData.fg, 3); }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { @@ -357,45 +357,149 @@ describe('InputHandler', () => { expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); // Text color of 'JUNK' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); }); it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); handler.parse('\x1b[?1049h\x1b[uTEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { handler.parse('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background - expect(term.buffer.lines.get(20).loadCell(10, new CellData()).fg & 0x1ff).to.equal(2); + expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2); + }); + }); + + describe('text attributes', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal(); + }); + it('bold', () => { + term.writeSync('\x1b[1m'); + assert.equal(!!term.curAttrData.isBold(), true); + term.writeSync('\x1b[22m'); + assert.equal(!!term.curAttrData.isBold(), false); + }); + it('dim', () => { + term.writeSync('\x1b[2m'); + assert.equal(!!term.curAttrData.isDim(), true); + term.writeSync('\x1b[22m'); + assert.equal(!!term.curAttrData.isDim(), false); + }); + it('italic', () => { + term.writeSync('\x1b[3m'); + assert.equal(!!term.curAttrData.isItalic(), true); + term.writeSync('\x1b[23m'); + assert.equal(!!term.curAttrData.isItalic(), false); + }); + it('underline', () => { + term.writeSync('\x1b[4m'); + assert.equal(!!term.curAttrData.isUnderline(), true); + term.writeSync('\x1b[24m'); + assert.equal(!!term.curAttrData.isUnderline(), false); + }); + it('blink', () => { + term.writeSync('\x1b[5m'); + assert.equal(!!term.curAttrData.isBlink(), true); + term.writeSync('\x1b[25m'); + assert.equal(!!term.curAttrData.isBlink(), false); + }); + it('inverse', () => { + term.writeSync('\x1b[7m'); + assert.equal(!!term.curAttrData.isInverse(), true); + term.writeSync('\x1b[27m'); + assert.equal(!!term.curAttrData.isInverse(), false); + }); + it('invisible', () => { + term.writeSync('\x1b[8m'); + assert.equal(!!term.curAttrData.isInvisible(), true); + term.writeSync('\x1b[28m'); + assert.equal(!!term.curAttrData.isInvisible(), false); + }); + it('colormode palette 16', () => { + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 8; ++i) { + term.writeSync(`\x1b[${i + 30};${i + 40}m`); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16); + assert.equal(term.curAttrData.getFgColor(), i); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16); + assert.equal(term.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getFgColorMode(), 0); + assert.equal(term.curAttrData.getBgColorMode(), 0); + }); + it('colormode palette 256', () => { + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 256; ++i) { + term.writeSync(`\x1b[38;5;${i};48;5;${i}m`); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256); + assert.equal(term.curAttrData.getFgColor(), i); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256); + assert.equal(term.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getFgColorMode(), 0); + assert.equal(term.curAttrData.getFgColor(), -1); + assert.equal(term.curAttrData.getBgColorMode(), 0); + assert.equal(term.curAttrData.getBgColor(), -1); + }); + it('colormode RGB', () => { + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT + term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]); + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getFgColorMode(), 0); + assert.equal(term.curAttrData.getFgColor(), -1); + assert.equal(term.curAttrData.getBgColorMode(), 0); + assert.equal(term.curAttrData.getBgColor(), -1); + }); + it('should zero missing RGB values', () => { + term.writeSync(`\x1b[38;2;1;2;3m`); + term.writeSync(`\x1b[38;2;5m`); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]); }); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index d51839f7..0200bc2b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,19 +4,17 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { DEFAULT_ATTR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; -import { FLAGS } from './renderer/Types'; +import { NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './Buffer'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; -import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from './core/input/TextDecoder'; -import { CellData } from './BufferLine'; +import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -104,10 +102,10 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _parseBuffer = new Uint32Array(4096); - private _stringDecoder = new StringToUtf32(); + private _parseBuffer: Uint32Array = new Uint32Array(4096); + private _stringDecoder: StringToUtf32 = new StringToUtf32(); private _utf8Decoder = new Utf8ToUtf32(); - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor( protected _terminal: IInputHandlingTerminal, @@ -341,13 +339,13 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; - const buffer: IBuffer = this._terminal.buffer; - const charset: ICharset = this._terminal.charset; - const screenReaderMode: boolean = this._terminal.options.screenReaderMode; - const cols: number = this._terminal.cols; - const wraparoundMode: boolean = this._terminal.wraparoundMode; - const insertMode: boolean = this._terminal.insertMode; - const curAttr: number = this._terminal.curAttr; + const buffer = this._terminal.buffer; + const charset = this._terminal.charset; + const screenReaderMode = this._terminal.options.screenReaderMode; + const cols = this._terminal.cols; + const wraparoundMode = this._terminal.wraparoundMode; + const insertMode = this._terminal.insertMode; + const curAttr = this._terminal.curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._terminal.updateRange(buffer.y); @@ -378,7 +376,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - if (!bufferRow.loadCell(buffer.x - 1, this._cell).getWidth()) { + if (!bufferRow.getWidth(buffer.x - 1)) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars @@ -427,13 +425,13 @@ export class InputHandler extends Disposable implements IInputHandler { // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell - if (bufferRow.loadCell(cols - 1, this._cell).getWidth() === 2) { - bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); + if (bufferRow.getWidth(cols - 1) === 2) { + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg); } } // write current char to buffer and advance cursor - bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero @@ -441,7 +439,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg); } } } @@ -547,7 +545,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -721,7 +719,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); if (clearWrap) { line.isWrapped = false; @@ -844,7 +842,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr())); + buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } // this.maxRange(); @@ -874,7 +872,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr())); + buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } // this.maxRange(); @@ -890,7 +888,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -906,7 +904,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -925,7 +923,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -941,7 +939,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); } @@ -997,10 +995,10 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - line.loadCell(buffer.x - 1, this._cell); + line.loadCell(buffer.x - 1, this._workCell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR) + (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA) ); // FIXME: no updateRange here? } @@ -1298,7 +1296,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); } - this._terminal.selectionManager.disable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.disable(); + } this._terminal.log('Binding to mouse events.'); break; case 1004: // send focusin/focusout events @@ -1336,7 +1336,7 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer - this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr()); + this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttrData()); this._terminal.refresh(0, this._terminal.rows - 1); if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); @@ -1488,7 +1488,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events'); } - this._terminal.selectionManager.enable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.enable(); + } break; case 1004: // send focusin/focusout events this._terminal.sendFocus = false; @@ -1598,127 +1600,124 @@ export class InputHandler extends Disposable implements IInputHandler { public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { - this._terminal.curAttr = DEFAULT_ATTR; + this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg; + this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg; return; } const l = params.length; - let flags = this._terminal.curAttr >> 18; - let fg = (this._terminal.curAttr >> 9) & 0x1ff; - let bg = this._terminal.curAttr & 0x1ff; let p; + const attr = this._terminal.curAttrData; for (let i = 0; i < l; i++) { p = params[i]; if (p >= 30 && p <= 37) { // fg color 8 - fg = p - 30; + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 30); } else if (p >= 40 && p <= 47) { // bg color 8 - bg = p - 40; + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 40); } else if (p >= 90 && p <= 97) { // fg color 16 - p += 8; - fg = p - 90; + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 90) | 8; } else if (p >= 100 && p <= 107) { // bg color 16 - p += 8; - bg = p - 100; + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 100) | 8; } else if (p === 0) { // default - flags = DEFAULT_ATTR >> 18; - fg = (DEFAULT_ATTR >> 9) & 0x1ff; - bg = DEFAULT_ATTR & 0x1ff; - // flags = 0; - // fg = 0x1ff; - // bg = 0x1ff; + attr.fg = DEFAULT_ATTR_DATA.fg; + attr.bg = DEFAULT_ATTR_DATA.bg; } else if (p === 1) { // bold text - flags |= FLAGS.BOLD; + attr.fg |= FgFlags.BOLD; } else if (p === 3) { // italic text - flags |= FLAGS.ITALIC; + attr.bg |= BgFlags.ITALIC; } else if (p === 4) { // underlined text - flags |= FLAGS.UNDERLINE; + attr.fg |= FgFlags.UNDERLINE; } else if (p === 5) { // blink - flags |= FLAGS.BLINK; + attr.fg |= FgFlags.BLINK; } else if (p === 7) { // inverse and positive // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' - flags |= FLAGS.INVERSE; + attr.fg |= FgFlags.INVERSE; } else if (p === 8) { // invisible - flags |= FLAGS.INVISIBLE; + attr.fg |= FgFlags.INVISIBLE; } else if (p === 2) { // dimmed text - flags |= FLAGS.DIM; + attr.bg |= BgFlags.DIM; } else if (p === 22) { // not bold nor faint - flags &= ~FLAGS.BOLD; - flags &= ~FLAGS.DIM; + attr.fg &= ~FgFlags.BOLD; + attr.bg &= ~BgFlags.DIM; } else if (p === 23) { // not italic - flags &= ~FLAGS.ITALIC; + attr.bg &= ~BgFlags.ITALIC; } else if (p === 24) { // not underlined - flags &= ~FLAGS.UNDERLINE; + attr.fg &= ~FgFlags.UNDERLINE; } else if (p === 25) { // not blink - flags &= ~FLAGS.BLINK; + attr.fg &= ~FgFlags.BLINK; } else if (p === 27) { // not inverse - flags &= ~FLAGS.INVERSE; + attr.fg &= ~FgFlags.INVERSE; } else if (p === 28) { // not invisible - flags &= ~FLAGS.INVISIBLE; + attr.fg &= ~FgFlags.INVISIBLE; } else if (p === 39) { // reset fg - fg = (DEFAULT_ATTR >> 9) & 0x1ff; + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else if (p === 49) { // reset bg - bg = DEFAULT_ATTR & 0x1ff; + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else if (p === 38) { // fg color 256 if (params[i + 1] === 2) { i += 2; - fg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (fg === -1) fg = 0x1ff; + attr.fg |= Attributes.CM_RGB; + attr.fg &= ~Attributes.RGB_MASK; + attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); i += 2; } else if (params[i + 1] === 5) { i += 2; p = params[i] & 0xff; - fg = p; + attr.fg &= ~Attributes.PCOLOR_MASK; + attr.fg |= Attributes.CM_P256 | p; } } else if (p === 48) { // bg color 256 if (params[i + 1] === 2) { i += 2; - bg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (bg === -1) bg = 0x1ff; + attr.bg |= Attributes.CM_RGB; + attr.bg &= ~Attributes.RGB_MASK; + attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); i += 2; } else if (params[i + 1] === 5) { i += 2; p = params[i] & 0xff; - bg = p; + attr.bg &= ~Attributes.PCOLOR_MASK; + attr.bg |= Attributes.CM_P256 | p; } } else if (p === 100) { // reset fg/bg - fg = (DEFAULT_ATTR >> 9) & 0x1ff; - bg = DEFAULT_ATTR & 0x1ff; + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else { this._terminal.error('Unknown SGR attribute: %d.', p); } } - - this._terminal.curAttr = (flags << 18) | (fg << 9) | bg; } /** @@ -1805,7 +1804,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = DEFAULT_ATTR; + this._terminal.curAttrData = DEFAULT_ATTR_DATA; this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? this._terminal.charset = null; this._terminal.glevel = 0; // ?? @@ -1868,7 +1867,8 @@ export class InputHandler extends Disposable implements IInputHandler { public saveCursor(params: number[]): void { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.y; - this._terminal.buffer.savedCurAttr = this._terminal.curAttr; + this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; + this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; } @@ -1880,7 +1880,8 @@ export class InputHandler extends Disposable implements IInputHandler { public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; - this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; + this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg; + this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg; } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a2b9045b..80399904 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -231,7 +231,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - const attr = line.getFG(bufferIndex[1]); + const attr = line.getFg(bufferIndex[1]); let fg: number | undefined; if (attr) { fg = (attr >> 9) & 0x1ff; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 9d49a860..394808f6 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -6,7 +6,7 @@ import { ITerminal, ISelectionManager, IBuffer, IBufferLine } from './Types'; import { XtermListener } from './common/Types'; import { MouseHelper } from './ui/MouseHelper'; -import * as Browser from './core/Platform'; +import * as Browser from './common/Platform'; import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; @@ -103,7 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; private _trimListener: XtermListener; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -669,8 +669,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const length = bufferLine.loadCell(i, this._cell).getChars().length; - if (this._cell.getWidth() === 0) { + const length = bufferLine.loadCell(i, this._workCell).getChars().length; + if (this._workCell.getWidth() === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; @@ -755,10 +755,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._cell))) { - bufferLine.loadCell(startCol - 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 0) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + bufferLine.loadCell(startCol - 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -771,10 +771,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._cell))) { - bufferLine.loadCell(endCol + 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 2) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + bufferLine.loadCell(endCol + 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 08bceb34..f4b717d8 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; -import { DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR_DATA } from './Buffer'; import { CellData } from './BufferLine'; const INIT_COLS = 80; @@ -260,7 +260,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should clear a buffer larger than rows', () => { @@ -277,7 +277,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should not break the prompt when cleared twice', () => { @@ -290,7 +290,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index b287aec2..bd2101a9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,11 +21,11 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IBufferLine, IAttributeData } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR_DATA } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; @@ -36,12 +36,11 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './ui/CharMeasure'; -import * as Browser from './core/Platform'; +import * as Browser from './common/Platform'; import { addDisposableDomListener } from './ui/Lifecycle'; import * as Strings from './Strings'; import { MouseHelper } from './ui/MouseHelper'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; -import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './ui/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; @@ -52,6 +51,8 @@ import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; import { clone } from './common/Clone'; +import { Attributes } from './BufferLine'; +import { applyWindowsMode } from './WindowsMode'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -64,10 +65,12 @@ const document = (typeof window !== 'undefined') ? window.document : null; const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. */ -const WRITE_BATCH_SIZE = 300; +const WRITE_TIMEOUT_MS = 12; const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars const MINIMUM_ROWS = 1; @@ -108,7 +111,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas' + rendererType: 'canvas', + windowsMode: false }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -171,7 +175,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _refreshEnd: number; public savedCols: number; - public curAttr: number; + public curAttrData: IAttributeData; + private _eraseAttrData: IAttributeData; public params: (string | number)[]; public currentParam: string | number; @@ -208,6 +213,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _accessibilityManager: AccessibilityManager; private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; + private _windowsMode: IDisposable | undefined; // bufferline to clone/copy from for new blank lines private _blankLine: IBufferLine = null; @@ -237,6 +243,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public dispose(): void { super.dispose(); + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } this._customKeyEventHandler = null; removeTerminalFromCache(this); this.handler = () => {}; @@ -291,7 +301,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Can this be just []? this.charsets = [null]; - this.curAttr = DEFAULT_ATTR; + this.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); this.params = []; this.currentParam = 0; @@ -319,6 +330,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.selectionManager.clearSelection(); this.selectionManager.initBuffersListeners(); } + + if (this.options.windowsMode) { + this._windowsMode = applyWindowsMode(this); + } } /** @@ -331,9 +346,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * back_color_erase feature for xterm. */ - public eraseAttr(): number { - // if (this.is('screen')) return DEFAULT_ATTR; - return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff); + public eraseAttrData(): IAttributeData { + this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000; + return this._eraseAttrData; } /** @@ -341,7 +357,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public focus(): void { if (this.textarea) { - this.textarea.focus(); + this.textarea.focus({ preventScroll: true }); } } @@ -499,6 +515,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'windowsMode': + if (value) { + if (!this._windowsMode) { + this._windowsMode = applyWindowsMode(this); + } + } else { + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } + } + break; } // Inform renderer of changes if (this.renderer) { @@ -566,12 +594,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Firefox doesn't appear to fire the contextmenu event on right click this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); })); } @@ -583,7 +611,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // that the regular click event doesn't fire for the middle mouse button. this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea); + moveTextAreaUnderMouseCursor(event, this); } })); } @@ -738,6 +766,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.mouseHelper = new MouseHelper(this.renderer); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); + if (this.mouseEvents) { + this.selectionManager.disable(); + } else { + this.selectionManager.enable(); + } if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to @@ -854,17 +887,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 127) ch = 127; data.push(ch); } else { - if (ch === 2047) { - data.push(0); + if (ch > 2047) { + data.push(2047); return; } - if (ch < 127) { - data.push(ch); - } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); - } + data.push(ch); } } @@ -1180,8 +1207,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== this.eraseAttr()) { - newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + const eraseAttr = this.eraseAttrData(); + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); this._blankLine = newLine; } newLine.isWrapped = isWrapped; @@ -1360,19 +1388,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - protected _innerWrite(): void { + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { this.writeBuffer = []; } - const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - const data = writeBatch.shift(); + const startTime = Date.now(); + while (this.writeBuffer.length > bufferOffset) { + const data = this.writeBuffer[bufferOffset]; + bufferOffset++; // If XOFF was sent in order to catch up with the pty process, resume it if - // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1390,12 +1419,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } } - if (this.writeBuffer.length > 0) { + if (this.writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; + this.writeBuffer = []; } } @@ -1767,23 +1801,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.ybase = 0; this.buffer.y = 0; for (let i = 1; i < this.rows; i++) { - this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR)); + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); } - /** - * If cur return the back color xterm feature attribute. Else return default attribute. - * @param cur - */ - public ch(cur?: boolean): CharData { - if (cur) { - return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - /** * Evaluate if the current terminal is the given argument. * @param term The terminal name to evaluate @@ -1859,7 +1882,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // blankLine(true) is xterm/linux behavior const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop; this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1); - this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr())); + this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData())); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); } else { @@ -1904,46 +1927,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return false; } - // TODO: Remove when true color is implemented - public matchColor(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < DEFAULT_ANSI_COLORS.length; i++) { - c = DEFAULT_ANSI_COLORS[i].rgba; - r2 = c >>> 24; - g2 = c >>> 16 & 0xFF; - b2 = c >>> 8 & 0xFF; - // assume that alpha is 0xFF - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; - } - private _visualBell(): boolean { return false; // return this.options.bellStyle === 'visual' || @@ -1966,19 +1949,3 @@ function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean { ev.keyCode === 17 || // Ctrl ev.keyCode === 18; // Alt } - -/** - * TODO: - * The below color-related code can be removed when true color is implemented. - * It's only purpose is to match true color requests with the closest matching - * ANSI color code. - */ - -const matchColorCache: {[colorRGBHash: number]: number} = {}; - -// http://stackoverflow.com/questions/1633828 -function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { - return Math.pow(30 * (r1 - r2), 2) - + Math.pow(59 * (g1 - g2), 2) - + Math.pow(11 * (b1 - b2), 2); -} diff --git a/src/Types.ts b/src/Types.ts index 5ed03826..23828b19 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -48,7 +48,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttr: number; + curAttrData: IAttributeData; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -70,7 +70,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { updateRange(y: number): void; scroll(isWrapped?: boolean): void; setgLevel(g: number): void; - eraseAttr(): number; + eraseAttrData(): IAttributeData; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -78,7 +78,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { reset(): void; showCursor(): void; refresh(start: number, end: number): void; - matchColor(r1: number, g1: number, b1: number): number; error(text: string, data?: any): void; setOption(key: string, value: any): void; tabSet(): void; @@ -290,17 +289,17 @@ export interface IBuffer { hasScrollback: boolean; savedY: number; savedX: number; - savedCurAttr: number; + savedCurAttrData: IAttributeData; isCursorInViewport: boolean; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; - getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; - getNullCell(fg?: number, bg?: number): ICellData; - getWhitespaceCell(fg?: number, bg?: number): ICellData; + getNullCell(attr?: IAttributeData): ICellData; + getWhitespaceCell(attr?: IAttributeData): ICellData; } export interface IBufferSet extends IEventEmitter { @@ -309,7 +308,7 @@ export interface IBufferSet extends IEventEmitter { active: IBuffer; activateNormalBuffer(): void; - activateAltBuffer(fillAttr?: number): void; + activateAltBuffer(fillAttr?: IAttributeData): void; } export interface ISelectionManager { @@ -522,11 +521,43 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } -/** Cell data */ -export interface ICellData { - content: number; +/** RGB color type */ +export type IColorRGB = [number, number, number]; + +/** Attribute data */ +export interface IAttributeData { fg: number; bg: number; + + clone(): IAttributeData; + + // flags + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; + + // color modes + getFgColorMode(): number; + getBgColorMode(): number; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; + + // colors + getFgColor(): number; + getBgColor(): number; +} + +/** Cell data */ +export interface ICellData extends IAttributeData { + content: number; combinedData: string; isCombined(): number; getWidth(): number; @@ -561,8 +592,8 @@ export interface IBufferLine { /* direct access to cell attrs */ getWidth(index: number): number; hasWidth(index: number): number; - getFG(index: number): number; - getBG(index: number): number; + getFg(index: number): number; + getBg(index: number): number; hasContent(index: number): number; getCodePoint(index: number): number; isCombined(index: number): number; diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts new file mode 100644 index 00000000..33a9bed5 --- /dev/null +++ b/src/WindowsMode.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'xterm'; +import { ITerminal } from './Types'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './Buffer'; + +export function applyWindowsMode(terminal: ITerminal): IDisposable { + // Winpty does not support wraparound mode which means that lines will never + // be marked as wrapped. This causes issues for things like copying a line + // retaining the wrapped new line characters or if consumers are listening + // in on the data stream. + // + // The workaround for this is to listen to every incoming line feed and mark + // the line as wrapped if the last character in the previous line is not a + // space. This is certainly not without its problems, but generally on + // Windows when text reaches the end of the terminal it's likely going to be + // wrapped. + return terminal.addDisposableListener('linefeed', () => { + const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); + const lastChar = line.get(terminal.cols - 1); + + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { + const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); + nextLine.isWrapped = true; + } + }); +} diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index 359fbd24..2f39102c 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -10,8 +10,7 @@ "outDir": "../../../lib/addons/attach/", "sourceMap": true, "removeComments": true, - "declaration": true, - "preserveWatchOutput": true + "declaration": true }, "include": [ "**/*.ts", diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 489ccdfe..3458d23a 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 05e6df68..0c74c25c 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 87899cda..6a1611a5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 91c18314..e2e19445 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 18105aa2..9c4f1176 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/winptyCompat/Interfaces.ts b/src/addons/winptyCompat/Interfaces.ts deleted file mode 100644 index 6217c860..00000000 --- a/src/addons/winptyCompat/Interfaces.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; - -export interface ITerminalCore { - buffer: any; -} - -export interface IWinptyCompatAddonTerminal extends Terminal { - _core: ITerminalCore; -} diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json deleted file mode 100644 index fc929497..00000000 --- a/src/addons/winptyCompat/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.winptycompat", - "main": "winptyCompat.js", - "private": true -} diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json deleted file mode 100644 index 9fc4d25e..00000000 --- a/src/addons/winptyCompat/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "outDir": "../../../lib/addons/winptyCompat/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "preserveWatchOutput": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts deleted file mode 100644 index c3a7e479..00000000 --- a/src/addons/winptyCompat/winptyCompat.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as winptyCompat from './winptyCompat'; - -class MockTerminal {} - -describe('winptyCompat addon', () => { - describe('apply', () => { - it('should do register the `winptyCompatInit` method', () => { - winptyCompat.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.winptyCompatInit, 'function'); - }); - }); -}); diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts deleted file mode 100644 index aec580ed..00000000 --- a/src/addons/winptyCompat/winptyCompat.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; -import { IWinptyCompatAddonTerminal } from './Interfaces'; - -const CHAR_DATA_CODE_INDEX = 3; -const NULL_CELL_CODE = 0; -const WHITESPACE_CELL_CODE = 32; - -export function winptyCompatInit(terminal: Terminal): void { - const addonTerminal = terminal; - - // Don't do anything when the platform is not Windows - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - if (!isWindows) { - return; - } - - // Winpty does not support wraparound mode which means that lines will never - // be marked as wrapped. This causes issues for things like copying a line - // retaining the wrapped new line characters or if consumers are listening - // in on the data stream. - // - // The workaround for this is to listen to every incoming line feed and mark - // the line as wrapped if the last character in the previous line is not a - // space. This is certainly not without its problems, but generally on - // Windows when text reaches the end of the terminal it's likely going to be - // wrapped. - addonTerminal.on('linefeed', () => { - const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); - const lastChar = line.get(addonTerminal.cols - 1); - - if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { - const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); - nextLine.isWrapped = true; - } - }); -} - -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).winptyCompatInit = function (): void { - winptyCompatInit(this); - }; -} diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 2b49f537..7d821b7c 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 68eb60f7..74a794cd 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { XtermListener } from './Types'; -import { IEventEmitter, IDisposable } from 'xterm'; +import { IDisposable, IEventEmitter, XtermListener } from './Types'; import { Disposable } from './Lifecycle'; export class EventEmitter extends Disposable implements IEventEmitter, IDisposable { diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index 209a3e2a..5fac6e82 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable } from 'xterm'; +import { IDisposable } from './Types'; /** * A base class that can be extended to provide convenience methods for managing the lifecycle of an diff --git a/src/core/Platform.ts b/src/common/Platform.ts similarity index 82% rename from src/core/Platform.ts rename to src/common/Platform.ts index 42c20d9d..ee82cff4 100644 --- a/src/core/Platform.ts +++ b/src/common/Platform.ts @@ -3,6 +3,16 @@ * @license MIT */ +interface INavigator { + userAgent: string; + language: string; + platform: string; +} + +// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but +// we want this module to live in common. +declare const navigator: INavigator; + const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; diff --git a/src/common/Types.ts b/src/common/Types.ts index 8a416bf1..8ad98d99 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -3,7 +3,16 @@ * @license MIT */ -import { IEventEmitter } from 'xterm'; +export interface IDisposable { + dispose(): void; +} + +export interface IEventEmitter { + on(type: string, listener: (...args: any[]) => void): void; + off(type: string, listener: (...args: any[]) => void): void; + emit(type: string, data?: any): void; + addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; +} export type XtermListener = (...args: any[]) => void; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 19dd0273..ccf742e5 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,19 +1,10 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, + "outDir": "../../lib", "types": [ - "../../node_modules/@types/mocha", - "../../" + "../../node_modules/@types/mocha" ] }, - "include": [ - "./**/*" - ] + "include": [ "./**/*" ] } diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 4f024a28..99bf48ca 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,20 +1,13 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, + "outDir": "../../lib", "types": [ - "../../node_modules/@types/mocha", - "../../" + "../../node_modules/@types/mocha" ] }, - "include": [ - "./**/*", - "../common/**/*" + "include": [ "./**/*" ], + "references": [ + { "path": "../common" } ] } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index addd028c..008b216a 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,12 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { ITerminal } from '../Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; +import { ITerminal, ICellData } from '../Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier, DEFAULT_COLOR } from './atlas/Types'; import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { is256Color } from './atlas/CharAtlasUtils'; -import { CellData } from '../BufferLine'; +import { CellData, AttributeData } from '../BufferLine'; +import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -241,7 +241,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } /** @@ -258,17 +258,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { * This is used to validate whether a cached image can be used. * @param bold Whether the text is bold. */ - protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void { - const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { + + // skip cache right away if we draw in RGB + if (cell.isFgRGB() || cell.isBgRGB()) { + this._drawUncachedChars(terminal, cell, x, y); + return; + } + + let fg; + let bg; + if (cell.isInverse()) { + fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); + bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); + } else { + bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); + fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); + } + + const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; fg += drawInBrightColor ? 8 : 0; - this._currentGlyphIdentifier.chars = chars; - this._currentGlyphIdentifier.code = code; + this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; + this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE; this._currentGlyphIdentifier.bg = bg; this._currentGlyphIdentifier.fg = fg; - this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold; - this._currentGlyphIdentifier.dim = dim; - this._currentGlyphIdentifier.italic = italic; + this._currentGlyphIdentifier.bold = cell.isBold() && terminal.options.enableBold; + this._currentGlyphIdentifier.dim = !!cell.isDim(); + this._currentGlyphIdentifier.italic = !!cell.isItalic(); const atlasDidDraw = this._charAtlas && this._charAtlas.draw( this._ctx, this._currentGlyphIdentifier, @@ -277,7 +294,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { ); if (!atlasDidDraw) { - this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic); + this._drawUncachedChars(terminal, cell, x, y); } } @@ -292,31 +309,40 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { + private _drawUncachedChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { this._ctx.save(); - this._ctx.font = this._getFont(terminal, bold, italic); + this._ctx.font = this._getFont(terminal, cell.isBold() && terminal.options.enableBold, !!cell.isItalic()); this._ctx.textBaseline = 'middle'; - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (is256Color(fg)) { - // 256 color support + if (cell.isInverse()) { + if (cell.isBgDefault()) { + this._ctx.fillStyle = this._colors.background.css; + } else if (cell.isBgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else { + this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + } + } else if (cell.isFgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } else if (cell.isFgPalette()) { + let fg = cell.getFgColor(); + if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } this._ctx.fillStyle = this._colors.ansi[fg].css; - } else { - this._ctx.fillStyle = this._colors.foreground.css; } this._clipRow(terminal, y); // Apply alpha to dim the character - if (dim) { + if (cell.isDim()) { this._ctx.globalAlpha = DIM_OPACITY; } // Draw the character this._ctx.fillText( - chars, + cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); this._ctx.restore(); } diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index eb44bb58..8c521dd9 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,12 +1,13 @@ import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; import { CellData } from '../BufferLine'; +import { WHITESPACE_CELL_CHAR } from '../Buffer'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor(private _terminal: ITerminal) { } @@ -43,7 +44,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { } const ranges: [number, number][] = []; - const lineStr = this._terminal.buffer.translateBufferLineToString(row, true); + const lineStr = line.translateToString(true); // Because some cells can be represented by multiple javascript characters, // we track the cell and the string indexes separately. This allows us to @@ -52,21 +53,19 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.getFG(0) >> 9; + let rangeAttrFG = line.getFg(0); + let rangeAttrBG = line.getBg(0); - for (let x = 0; x < this._terminal.cols; x++) { - line.loadCell(x, this._cell); - const chars = this._cell.getChars(); - const width = this._cell.getWidth(); - const attr = this._cell.fg >> 9; + for (let x = 0; x < line.getTrimmedLength(); x++) { + line.loadCell(x, this._workCell); - if (width === 0) { + if (this._workCell.getWidth() === 0) { // If this character is of width 0, skip it. continue; } // End of range - if (attr !== rangeAttr) { + if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) { // If we ended up with a sequence of more than one character, // look for ranges to join. if (x - rangeStartColumn > 1) { @@ -85,10 +84,11 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { // Reset our markers for a new range. rangeStartColumn = x; rangeStartStringIndex = currentStringIndex; - rangeAttr = attr; + rangeAttrFG = this._workCell.fg; + rangeAttrBG = this._workCell.bg; } - currentStringIndex += chars.length; + currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length; } // Process any trailing ranges. @@ -154,7 +154,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { for (let x = startCol; x < this._terminal.cols; x++) { const width = line.getWidth(x); - const length = line.getString(x).length; + const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length; // We skip zero-width characters when creating the string to join the text // so we do the same here diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 02328877..2c1b516a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -70,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 }); + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(this._terminal.element); this.register({ dispose: () => observer.disconnect() }); } @@ -85,6 +85,7 @@ export class Renderer extends EventEmitter implements IRenderer { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { this._terminal.refresh(0, this._terminal.rows - 1); + this._needsFullRefresh = false; } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 81782ee8..fc16a2fb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -55,6 +55,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Selection does not exist if (!start || !end) { + this._clearState(); return; } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index be022239..2547ecb2 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,14 +3,12 @@ * @license MIT */ -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; -import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; -import { CharData, ITerminal } from '../Types'; -import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; +import { NULL_CELL_CODE } from '../Buffer'; +import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; +import { CharData, ITerminal, ICellData } from '../Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { is256Color } from './atlas/CharAtlasUtils'; -import { CellData } from '../BufferLine'; +import { CellData, AttributeData, Content } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -25,7 +23,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterJoinerRegistry: ICharacterJoinerRegistry; - private _cell = new CellData(); + private _workCell = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) { super(container, 'text', zIndex, alpha, colors); @@ -59,14 +57,9 @@ export class TextRenderLayer extends BaseRenderLayer { lastRow: number, joinerRegistry: ICharacterJoinerRegistry | null, callback: ( - code: number, - chars: string, - width: number, + cell: ICellData, x: number, - y: number, - fg: number, - bg: number, - flags: number + y: number ) => void ): void { for (let y = firstRow; y <= lastRow; y++) { @@ -74,14 +67,8 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - (line as any).loadCell(x, this._cell); - let code: number = this._cell.getCode() || WHITESPACE_CELL_CODE; - - // Can either represent character(s) for a single cell or multiple cells - // if indicated by a character joiner. - let chars = this._cell.getChars() || WHITESPACE_CELL_CHAR; - const attr = this._cell.fg; - let width = this._cell.getWidth(); + line.loadCell(x, this._workCell); + let cell = this._workCell; // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -89,7 +76,7 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 - if (width === 0) { + if (cell.getWidth() === 0) { continue; } @@ -102,14 +89,15 @@ export class TextRenderLayer extends BaseRenderLayer { // We already know the exact start and end column of the joined range, // so we get the string and width representing it directly - chars = terminal.buffer.translateBufferLineToString( - row, - true, - range[0], - range[1] - ); - width = range[1] - range[0]; - code = Infinity; + cell = CellData.fromCharData([ + 0, + line.translateToString(true, range[0], range[1]), + range[1] - range[0], + 0xFFFFFF + ]); + // hacky: patch attrs + cell.fg = this._workCell.fg; + cell.bg = this._workCell.bg; // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; @@ -119,7 +107,7 @@ export class TextRenderLayer extends BaseRenderLayer { // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't // yield the same result as rendering the last character individually. - if (!isJoined && this._isOverlapping(chars, width, code)) { + if (!isJoined && this._isOverlapping(cell)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -127,8 +115,10 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).getCode() === NULL_CELL_CODE) { - width = 2; + if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) { + // patch width to 2 + cell.content &= ~Content.WIDTH_MASK; + cell.content |= 2 << Content.WIDTH_SHIFT; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the // overlapping char is no longer to the left of the character and also when @@ -137,32 +127,10 @@ export class TextRenderLayer extends BaseRenderLayer { } } - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; - - // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - callback( - code, - chars, - width, + cell, x, - y, - fg, - bg, - flags + y ); x = lastCharX; @@ -183,14 +151,23 @@ export class TextRenderLayer extends BaseRenderLayer { ctx.save(); - this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => { + this._forEachCell(terminal, firstRow, lastRow, null, (cell, x, y) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. let nextFillStyle = null; // null represents default background color - if (bg === INVERTED_DEFAULT_COLOR) { - nextFillStyle = this._colors.foreground.css; - } else if (is256Color(bg)) { - nextFillStyle = this._colors.ansi[bg].css; + + if (cell.isInverse()) { + if (cell.isFgDefault()) { + nextFillStyle = this._colors.foreground.css; + } else if (cell.isFgRGB()) { + nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } else { + nextFillStyle = this._colors.ansi[cell.getFgColor()].css; + } + } else if (cell.isBgRGB()) { + nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else if (cell.isBgPalette()) { + nextFillStyle = this._colors.ansi[cell.getBgColor()].css; } if (prevFillStyle === null) { @@ -225,29 +202,31 @@ export class TextRenderLayer extends BaseRenderLayer { } private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void { - this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => { - if (flags & FLAGS.INVISIBLE) { + this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => { + if (cell.isInvisible()) { return; } - if (flags & FLAGS.UNDERLINE) { + if (cell.isUnderline()) { this._ctx.save(); - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (is256Color(fg)) { - // 256 color support - this._ctx.fillStyle = this._colors.ansi[fg].css; - } else { - this._ctx.fillStyle = this._colors.foreground.css; + + if (cell.isInverse()) { + if (cell.isBgDefault()) { + this._ctx.fillStyle = this._colors.background.css; + } else if (cell.isBgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else { + this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + } + } else if (cell.isFgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } else if (cell.isFgPalette()) { + this._ctx.fillStyle = this._colors.ansi[cell.getFgColor()].css; } - this.fillBottomLineAtCells(x, y, width); + + this.fillBottomLineAtCells(x, y, cell.getWidth()); this._ctx.restore(); } - this.drawChars( - terminal, chars, code, - width, x, y, - fg, bg, - !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC) - ); + this.drawChars(terminal, cell, x, y); }); } @@ -273,21 +252,23 @@ export class TextRenderLayer extends BaseRenderLayer { /** * Whether a character is overlapping to the next cell. */ - private _isOverlapping(char: string, width: number, code: number): boolean { + private _isOverlapping(cell: ICellData): boolean { // Only single cell characters can be overlapping, rendering issues can // occur without this check - if (width !== 1) { + if (cell.getWidth() !== 1) { return false; } // We assume that any ascii character will not overlap - if (code < 256) { + if (cell.getCode() < 256) { return false; } + const chars = cell.getChars(); + // Deliver from cache if available - if (this._characterOverlapCache.hasOwnProperty(char)) { - return this._characterOverlapCache[char]; + if (this._characterOverlapCache.hasOwnProperty(chars)) { + return this._characterOverlapCache[chars]; } // Setup the font @@ -297,13 +278,13 @@ export class TextRenderLayer extends BaseRenderLayer { // Measure the width of the character, but Math.floor it // because that is what the renderer does when it calculates // the character dimensions we are comparing against - const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth; + const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth; // Restore the original context this._ctx.restore(); // Cache and return - this._characterOverlapCache[char] = overlaps; + this._characterOverlapCache[chars] = overlaps; return overlaps; } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index cadcce2e..38950766 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -4,7 +4,7 @@ */ import { FontWeight } from 'xterm'; -import { isFirefox, isSafari } from '../../core/Platform'; +import { isFirefox, isSafari } from '../../common/Platform'; import { IColor } from '../Types'; import { ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types'; diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index e311c369..cb03a48f 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -8,7 +8,7 @@ import BaseCharAtlas from './BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from '../ColorManager'; import { clearColor } from './CharAtlasGenerator'; import LRUMap from './LRUMap'; -import { isFirefox, isSafari } from '../../core/Platform'; +import { isFirefox, isSafari } from '../../common/Platform'; import { IColor } from '../Types'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..78ccc620 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { ITheme } from 'xterm'; import { EventEmitter } from '../../common/EventEmitter'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; -import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; +import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(document); + this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); @@ -165,12 +165,22 @@ export class DomRenderer extends EventEmitter implements IRenderer { `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + `}`; + // Blink animation + styles += + `@keyframes blink {` + + ` 0 % { opacity: 1.0; }` + + ` 50% { opacity: 0.0; }` + + ` 100 % { opacity: 1.0; }` + + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + ` outline: 1px solid ${this.colorManager.colors.cursor.css};` + ` outline-offset: -1px;` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` + + ` animation: blink 1s step-end infinite;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this.colorManager.colors.cursor.css};` + ` color: ${this.colorManager.colors.cursorAccent.css};` + @@ -328,6 +338,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; const cursorX = this._terminal.buffer.x; + const cursorBlink = this._terminal.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; @@ -336,7 +347,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const row = y + terminal.buffer.ydisp; const lineData = terminal.buffer.lines.get(row); const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); } this._terminal.emit('refresh', {start, end}); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index e06990d7..4402943c 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,26 +6,29 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; -import { FLAGS } from '../Types'; -import { BufferLine, CellData } from '../../BufferLine'; -import { IBufferLine } from '../../Types'; -import { DEFAULT_COLOR } from '../atlas/Types'; +import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR_DATA } from '../../Buffer'; +import { BufferLine, CellData, FgFlags, BgFlags, Attributes } from '../../BufferLine'; +import { IBufferLine, ITerminalOptions } from '../../Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; + const options: ITerminalOptions = {}; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document); + + options.enableBold = true; + options.drawBoldTextInBrightColors = true; + + rowFactory = new DomRendererRowFactory(options, dom.window.document); lineData = createEmptyLineData(2); }); describe('createRow', () => { it('should not create anything for an empty row', () => { - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -35,7 +38,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, undefined])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -43,17 +46,24 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); } }); + it('should add class for cursor blink', () => { + const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20); + assert.equal(getFragmentHtml(fragment), + ` ` + ); + }); + it('should not render cells that go beyond the terminal\'s columns', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -61,26 +71,43 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { - lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); it('should add class for italic', () => { - lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); + it('should add class for dim', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add classes for 256 foreground colors', () => { - const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P256; for (let i = 0; i < 256; i++) { - lineData.setCell(0, CellData.fromCharData([defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + cell.fg &= ~Attributes.PCOLOR_MASK; + cell.fg |= i; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -88,10 +115,13 @@ describe('DomRendererRowFactory', () => { }); it('should add classes for 256 background colors', () => { - const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg |= Attributes.CM_P256; for (let i = 0; i < 256; i++) { - lineData.setCell(0, CellData.fromCharData([defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + cell.bg &= ~Attributes.PCOLOR_MASK; + cell.bg |= i; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -99,38 +129,72 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert colors', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE; + cell.bg |= Attributes.CM_P16 | 1; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should correctly invert default fg color', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= FgFlags.INVERSE; + cell.bg |= Attributes.CM_P16 | 1; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should correctly invert default bg color', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should turn bold fg text bright', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= FgFlags.BOLD | Attributes.CM_P16; for (let i = 0; i < 8; i++) { - lineData.setCell(0, CellData.fromCharData([(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + cell.fg &= ~Attributes.PCOLOR_MASK; + cell.fg |= i; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); } }); + + it('should set style attribute for RBG', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3; + cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + + it('should correctly invert RGB colors', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE; + cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); }); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 5bc5fffa..a6cbeb78 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -4,26 +4,29 @@ */ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; -import { FLAGS } from '../Types'; -import { IBufferLine } from '../../Types'; -import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { CellData } from '../../BufferLine'; +import { IBufferLine, ITerminalOptions } from '../../Types'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { CellData, AttributeData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; +export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; export const CURSOR_CLASS = 'xterm-cursor'; +export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); + constructor( + private _terminalOptions: ITerminalOptions, private _document: Document ) { } - public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); // Find the line length first, this prevents the need to output a bunch of @@ -33,16 +36,15 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - if (lineData.loadCell(x, this._cell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; } } for (let x = 0; x < lineLength; x++) { - lineData.loadCell(x, this._cell); - const attr = this._cell.fg; - const width = this._cell.getWidth(); + lineData.loadCell(x, this._workCell); + const width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -54,13 +56,13 @@ export class DomRendererRowFactory { charElement.style.width = `${cellWidth * width}px`; } - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; - if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); + if (cursorBlink) { + charElement.classList.add(CURSOR_BLINK_CLASS); + } + switch (cursorStyle) { case 'bar': charElement.classList.add(CURSOR_STYLE_BAR_CLASS); @@ -74,39 +76,49 @@ export class DomRendererRowFactory { } } - // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - if (flags & FLAGS.BOLD) { - // Convert the FG color to the bold variant. This should not happen when - // the fg is the inverse default color as there is no bold variant. - if (fg < 8) { - fg += 8; - } + if (this._workCell.isBold() && this._terminalOptions.enableBold) { charElement.classList.add(BOLD_CLASS); } - if (flags & FLAGS.ITALIC) { + if (this._workCell.isItalic()) { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = this._cell.getChars() || WHITESPACE_CELL_CHAR; - if (fg !== DEFAULT_COLOR) { - charElement.classList.add(`xterm-fg-${fg}`); + if (this._workCell.isDim()) { + charElement.classList.add(DIM_CLASS); } - if (bg !== DEFAULT_COLOR) { - charElement.classList.add(`xterm-bg-${bg}`); + + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + + const swapColor = this._workCell.isInverse(); + + // fg + if (this._workCell.isFgRGB()) { + let style = charElement.getAttribute('style') || ''; + style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`; + charElement.setAttribute('style', style); + } else if (this._workCell.isFgPalette()) { + let fg = this._workCell.getFgColor(); + if (this._workCell.isBold() && fg < 8 && !swapColor && + this._terminalOptions.enableBold && this._terminalOptions.drawBoldTextInBrightColors) { + fg += 8; + } + charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`); + } else if (swapColor) { + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); } + + // bg + if (this._workCell.isBgRGB()) { + let style = charElement.getAttribute('style') || ''; + style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`; + charElement.setAttribute('style', style); + } else if (this._workCell.isBgPalette()) { + charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`); + } else if (swapColor) { + charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + } + fragment.appendChild(charElement); } return fragment; diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json new file mode 100644 index 00000000..84d0c924 --- /dev/null +++ b/src/tsconfig-base.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ "es5" ], + "rootDir": ".", + + "sourceMap": true, + "removeComments": true, + "pretty": true, + + "incremental": true + } +} diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json new file mode 100644 index 00000000..66b61f09 --- /dev/null +++ b/src/tsconfig-library-base.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "composite": true, + "strict": true + } +} diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json new file mode 100644 index 00000000..2a53ab89 --- /dev/null +++ b/src/tsconfig.all.json @@ -0,0 +1,14 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "." }, + { "path": "./addons/attach" }, + { "path": "./addons/fit" }, + { "path": "./addons/fullscreen" }, + { "path": "./addons/search" }, + { "path": "./addons/terminado" }, + { "path": "./addons/webLinks" }, + { "path": "./addons/zmodem" } + ] +} diff --git a/tsconfig.json b/src/tsconfig.json similarity index 54% rename from tsconfig.json rename to src/tsconfig.json index 2d1d6e35..f0b1c749 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -1,7 +1,7 @@ { + "extends": "./tsconfig-base", "compilerOptions": { "module": "commonjs", - "target": "es5", "lib": [ "dom", "es5", @@ -9,19 +9,21 @@ "scripthost", "es2015.promise" ], - "rootDir": "src", - "outDir": "lib", - "sourceMap": true, - "removeComments": true, - "preserveWatchOutput": true, + "rootDir": ".", + "outDir": "../lib", + "noUnusedLocals": true, "noImplicitAny": true }, "include": [ - "src/**/*", - "typings/xterm.d.ts" + "./**/*", + "../typings/xterm.d.ts" ], "exclude": [ - "src/addons/**/*" + "./addons/**/*" + ], + "references": [ + { "path": "./common" }, + { "path": "./core" } ] } diff --git a/src/ui/Clipboard.ts b/src/ui/Clipboard.ts index b1acba9d..2570a8b1 100644 --- a/src/ui/Clipboard.ts +++ b/src/ui/Clipboard.ts @@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void { - // Bring textarea at the cursor position - textarea.style.position = 'fixed'; - textarea.style.width = '20px'; - textarea.style.height = '20px'; - textarea.style.left = (ev.clientX - 10) + 'px'; - textarea.style.top = (ev.clientY - 10) + 'px'; - textarea.style.zIndex = '1000'; +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { - textarea.focus(); + // Calculate textarea position relative to the screen element + const pos = term.screenElement.getBoundingClientRect(); + const left = ev.clientX - pos.left - 10; + const top = ev.clientY - pos.top - 10; + + // Bring textarea at the cursor position + term.textarea.style.position = 'absolute'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = `${left}px`; + term.textarea.style.top = `${top}px`; + term.textarea.style.zIndex = '1000'; + + term.textarea.focus(); // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - textarea.style.position = null; - textarea.style.width = null; - textarea.style.height = null; - textarea.style.left = null; - textarea.style.top = null; - textarea.style.zIndex = null; + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; }, 200); } @@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param selectionManager The terminal's selection manager. * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { - moveTextAreaUnderMouseCursor(ev, textarea); +export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, term); if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { selectionManager.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; - textarea.select(); + term.textarea.value = selectionManager.selectionText; + term.textarea.select(); } diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index a232f5b9..372dccc5 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -23,11 +23,13 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; + private _initialSelectionLength: number; constructor( private _terminal: ITerminal @@ -38,6 +40,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); this._clickListener = e => this._onClick(e); } @@ -89,6 +92,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (!this._areZonesActive) { this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.addEventListener('click', this._clickListener); } } @@ -97,6 +101,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (this._areZonesActive) { this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.removeEventListener('click', this._clickListener); } } @@ -153,6 +158,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onMouseDown(e: MouseEvent): void { + // Store current terminal selection length, to check if we're performing + // a selection operation + this._initialSelectionLength = this._terminal.getSelection().length; + // Ignore the event if there are no zones active if (!this._areZonesActive) { return; @@ -169,10 +178,25 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } } + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + private _onClick(e: MouseEvent): void { - // Find the active zone and click it if found + // Find the active zone and click it if found and no selection was + // being performed const zone = this._findZoneEventAt(e); - if (zone) { + const currentSelectionLength = this._terminal.getSelection().length; + + if (zone && currentSelectionLength === this._initialSelectionLength) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 13abee76..c1d75645 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -4,12 +4,13 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData } from '../Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData, IAttributeData } from '../Types'; import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; -import * as Browser from '../core/Platform'; +import * as Browser from '../common/Platform'; import { ITheme, IDisposable, IMarker } from 'xterm'; import { Terminal } from '../Terminal'; +import { AttributeData } from '../BufferLine'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -190,7 +191,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttr: number; + curAttrData = new AttributeData(); savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -225,7 +226,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { setgLevel(g: number): void { throw new Error('Method not implemented.'); } - eraseAttr(): number { + eraseAttrData(): IAttributeData { throw new Error('Method not implemented.'); } eraseRight(x: number, y: number): void { @@ -312,7 +313,7 @@ export class MockBuffer implements IBuffer { scrollTop: number; savedY: number; savedX: number; - savedCurAttr: number; + savedCurAttrData = new AttributeData(); translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { return Buffer.prototype.translateBufferLineToString.apply(this, arguments); } @@ -328,7 +329,7 @@ export class MockBuffer implements IBuffer { setLines(lines: ICircularList): void { this.lines = lines; } - getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { return Buffer.prototype.getBlankLine.apply(this, arguments); } stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { @@ -337,10 +338,10 @@ export class MockBuffer implements IBuffer { iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { return Buffer.prototype.iterator.apply(this, arguments); } - getNullCell(fg: number = 0, bg: number = 0): ICellData { + getNullCell(attr?: IAttributeData): ICellData { throw new Error('Method not implemented.'); } - getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + getWhitespaceCell(attr?: IAttributeData): ICellData { throw new Error('Method not implemented.'); } } diff --git a/src/xterm.css b/src/xterm.css index 24cd475f..e80c2524 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -36,7 +36,6 @@ */ .xterm { - font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; user-select: none; @@ -162,3 +161,7 @@ height: 1px; overflow: hidden; } + +.xterm-dim { + opacity: 0.5; +} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d0b37428..2da89cb4 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -188,6 +188,18 @@ declare module 'xterm' { * The color theme of the terminal. */ theme?: ITheme; + + /** + * Whether "Windows mode" is enabled. Because Windows backends winpty and + * conpty operate by doing line wrapping on their side, xterm.js does not + * have access to wrapped lines. When Windows mode is enabled the following + * changes will be in effect: + * + * - Reflow is disabled. + * - Lines are assumed to be wrapped if the last character of the line is + * not whitespace. + */ + windowsMode?: boolean; } /** diff --git a/yarn.lock b/yarn.lock index 5555321d..440aa4f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1275,11 +1275,6 @@ combined-stream@1.0.6, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" - integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= - commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -1338,21 +1333,6 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" -concurrently@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.6.0.tgz#c25e34b156a9d5bd4f256a0d85f6192438ae481f" - integrity sha512-6XiIYtYzmGEccNZFkih5JOH92jLA4ulZArAYy5j1uDSdrPLB3KzdE8GW7t2fHPcg9ry2+5LP9IEYzXzxw9lFdA== - dependencies: - chalk "^2.4.1" - commander "2.6.0" - date-fns "^1.23.0" - lodash "^4.5.1" - read-pkg "^3.0.0" - rx "2.3.24" - spawn-command "^0.0.2-1" - supports-color "^3.2.3" - tree-kill "^1.1.0" - configstore@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" @@ -1595,11 +1575,6 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" -date-fns@^1.23.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" - integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1933,7 +1908,7 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3625,7 +3600,7 @@ jsesc@^1.3.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -3815,16 +3790,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4037,7 +4002,7 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.5.1: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== @@ -4984,14 +4949,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -5092,13 +5049,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5384,15 +5334,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -5688,11 +5629,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx@2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" - integrity sha1-FPlQpCF9fjXapxu8vljv9o6ksrc= - rxjs@^6.1.0: version "6.3.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.1.tgz#878a1a8c64b8a5da11dcf74b5033fe944cdafb84" @@ -6025,11 +5961,6 @@ sparkles@^1.0.0: resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= - spawn-wrap@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" @@ -6271,11 +6202,6 @@ strip-bom@^1.0.0: first-chunk-stream "^1.0.0" is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -6305,7 +6231,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -6521,11 +6447,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tree-kill@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" - integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -6633,10 +6554,10 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@3.4: + version "3.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" + integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== uglify-es@^3.3.4: version "3.3.9"