diff --git a/.npmignore b/.npmignore index 63069bda..517ba0bd 100644 --- a/.npmignore +++ b/.npmignore @@ -1,16 +1,48 @@ -node_modules/ -*.swp -.lock-wscript -lib/*.test.js -lib/*.test.js.map +# Blacklist - exclude everything except npm defaults such as LICENSE, etc +* +!*/ + +# Whitelist - entries to be included must be negated with "!" +!*.js +!*.json + +# Whitelist - dist/ +!dist/**/*.js +!dist/**/*.js.map + +!dist/**/*.css + +# Whitelist - lib/ +!lib/**/*.d.ts + +!lib/**/*.js +!lib/**/*.js.map + +!lib/**/*.css + +# Whitelist - src/ +!src/**/*.ts +!src/**/*.d.ts + +!src/**/*.js +!src/**/*.js.map + +!src/**/*.css + +# Whitelist - typings/ +!typings/*.d.ts + +# Blacklist - (normal behavior) these will override any whitelist +*.test.ts +*.test.d.ts +*.test.js +*.test.js.map lib/test/ -Makefile.gyp -*.Makefile -*.target.gyp.mk -*.node -example/*.log + docs/ -npm-debug.log /.idea/ -.env +.vscode/ build/ +fixtures/ +coverage/ +demo/ diff --git a/.travis.yml b/.travis.yml index c41ebf79..1f3c9079 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,3 @@ env: notifications: email: false script: npm run $NPM_COMMAND -after_success: npm run coveralls diff --git a/AUTHORS b/AUTHORS index 41f3a523..0fed60b3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Benjamin Woodruff Bill Church Bob Reid bottleofwater +Brandon Bayer Brian Mock Bruno Ribeiro Bruno Ribeito @@ -62,12 +63,16 @@ Jianhui Zhao Joao Moreno Joao Moreno Johannes Zellner +Jon Austin Jon Masters Jörg Breitbart +jpoth Justin Luk Justin Mecham Kirill Merkushev Krasimir Tsonev +Ledion Bitincka +Linus Unnebäck Luca Lucian Buzzo Lukas Drgon @@ -88,8 +93,11 @@ npezza93 Oleksandr Andriienko Paris Kasidiaris Paris Kasidiaris +Peng Xiao Peter Baumgarten Philip Olson +pro-src <34285059+pro-src@users.noreply.github.com> +pro-src Rick Baker runarberg Saad Malik diff --git a/README.md b/README.md index 07baec5a..6ba671a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) +[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) Xterm.js is a terminal front-end component written in JavaScript that works in the browser. @@ -57,8 +57,6 @@ The proposed way to load xterm.js is via the ES6 module syntax. import { Terminal } from 'xterm'; ``` -*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(xterm).fit()`.* - ### Addons Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. @@ -76,6 +74,21 @@ var xterm = new Terminal(); // Instantiate the terminal xterm.fit(); // Use the `fit` method, provided by the `fit` addon ``` +#### Importing Addons in TypeScript + +There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm).fit()`. + +Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.: + +```typescript +import { Terminal } from 'xterm'; +import { fit } from 'xterm/lib/addons/fit/fit'; +const xterm = new Terminal(); + +// Fit the terminal when necessary: +fit(xterm); +``` + #### Third party addons There are also the following third party addons available: @@ -135,6 +148,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**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 +- [**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. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. diff --git a/demo/index.html b/demo/index.html index 760d5990..168f56e4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -9,7 +9,7 @@ -

xterm.js: xterm, in the browser

+

xterm.js: A terminal for the web

Actions

@@ -26,6 +26,9 @@

+

+ +

+

+ +

Size

diff --git a/demo/main.js b/demo/main.js index d9c0151a..682fc5a0 100644 --- a/demo/main.js +++ b/demo/main.js @@ -31,7 +31,9 @@ var terminalContainer = document.getElementById('terminal-container'), cursorStyle: document.querySelector('#option-cursor-style'), macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'), scrollback: document.querySelector('#option-scrollback'), + transparency: document.querySelector('#option-transparency'), tabstopwidth: document.querySelector('#option-tabstopwidth'), + experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'), bellStyle: document.querySelector('#option-bell-style'), screenReaderMode: document.querySelector('#option-screen-reader-mode') }, @@ -74,21 +76,29 @@ actionElements.findPrevious.addEventListener('keypress', function (e) { optionElements.cursorBlink.addEventListener('change', function () { term.setOption('cursorBlink', optionElements.cursorBlink.checked); }); +optionElements.macOptionIsMeta.addEventListener('change', function () { + term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked); +}); +optionElements.transparency.addEventListener('change', function () { + var checked = optionElements.transparency.checked; + term.setOption('allowTransparency', checked); + term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {}); +}); optionElements.cursorStyle.addEventListener('change', function () { term.setOption('cursorStyle', optionElements.cursorStyle.value); }); optionElements.bellStyle.addEventListener('change', function () { term.setOption('bellStyle', optionElements.bellStyle.value); }); -optionElements.macOptionIsMeta.addEventListener('change', function () { - term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked); -}); 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)); }); +optionElements.experimentalCharAtlas.addEventListener('change', function () { + term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value); +}); optionElements.screenReaderMode.addEventListener('change', function () { term.setOption('screenReaderMode', optionElements.screenReaderMode.checked); }); diff --git a/package.json b/package.json index d4a06f82..44cc2e88 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,16 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.3.0", - "ignore": [ - "demo", - "test", - ".gitignore" - ], + "version": "3.4.0", "main": "lib/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", - "files": [ - "*.js", - "*.json", - "dist/*.css", - "dist/**/*.css", - "dist/*.js", - "dist/*.js.map", - "dist/**/*.js", - "dist/**/*.js.map", - "lib/*.css", - "lib/**/*.css", - "lib/*.d.ts", - "lib/*.js", - "lib/*.js.map", - "lib/**/*.d.ts", - "lib/**/*.js", - "lib/**/*.js.map", - "src/*.css", - "src/**/*.css", - "src/*.js", - "src/*.js.map", - "src/*.ts", - "src/**/*.js", - "src/**/*.js.map", - "src/**/*.ts", - "typings/*.d.ts" - ], "devDependencies": { "@types/chai": "^3.4.34", "@types/jsdom": "^11.0.1", "@types/mocha": "^2.2.33", - "@types/node": "^6.0.41", + "@types/node": "6.0.108", "@types/text-encoding": "0.0.32", "browserify": "^13.3.0", "chai": "3.5.0", @@ -78,7 +46,7 @@ "scripts": { "start": "node demo/app", "start-zmodem": "node demo/zmodem/app", - "lint": "tslint src/*.ts src/**/*.ts src/addons/**/*.ts", + "lint": "tslint 'src/**/*.ts'", "test": "npm-run-all mocha lint", "mocha": "gulp test", "build:docs": "jsdoc -c jsdoc.json", @@ -88,6 +56,5 @@ "coveralls": "gulp coveralls", "webpack": "gulp webpack", "watch": "gulp watch" - }, - "dependencies": {} + } } diff --git a/src/Terminal.ts b/src/Terminal.ts index c466396e..2cae7b56 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -48,7 +48,7 @@ import { MouseZoneManager } from './input/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; -import { removeTerminalFromCache } from './renderer/atlas/CharAtlas'; +import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -103,7 +103,9 @@ const DEFAULT_OPTIONS: ITerminalOptions = { cursorStyle: 'block', bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', + drawBoldTextInBrightColors: true, enableBold: true, + experimentalCharAtlas: 'static', fontFamily: 'courier-new, courier, monospace', fontSize: 15, fontWeight: 'normal', @@ -471,6 +473,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.charMeasure.measure(this.options); } break; + case 'experimentalCharAtlas': case 'enableBold': case 'letterSpacing': case 'lineHeight': diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 85465272..7f2e4f05 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -28,7 +28,7 @@ export class AltClickHandler { this._startCol = this._terminal.buffer.x; this._startRow = this._terminal.buffer.y; - [this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords( + let coordinates = this._terminal.mouseHelper.getCoords( this._mouseEvent, this._terminal.element, this._terminal.charMeasure, @@ -36,16 +36,20 @@ export class AltClickHandler { this._terminal.cols, this._terminal.rows, false - ).map((coordinate: number) => { - return coordinate - 1; - }); + ); + + if (coordinates) { + [this._endCol, this._endRow] = coordinates.map((coordinate: number) => { + return coordinate - 1; + }); + } } /** * Writes the escape sequences of arrows to the terminal */ public move(): void { - if (this._mouseEvent.altKey) { + if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) { this._terminal.send(this._arrowSequences()); } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 7a655163..b2a40290 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -6,8 +6,8 @@ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { CharData, ITerminal } from '../Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types'; -import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types'; -import { acquireCharAtlas } from './atlas/CharAtlas'; +import BaseCharAtlas from './atlas/BaseCharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { @@ -20,7 +20,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - private _charAtlas: HTMLCanvasElement | ImageBitmap; + protected _charAtlas: BaseCharAtlas; constructor( private _container: HTMLElement, @@ -83,13 +83,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = null; - const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); - if (result instanceof HTMLCanvasElement) { - this._charAtlas = result; - } else { - result.then(bitmap => this._charAtlas = bitmap); - } + this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas.warmUp(); } public resize(terminal: ITerminal, dim: IRenderDimensions): void { @@ -243,55 +238,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void { - let colorIndex = 0; - if (fg < 256) { - colorIndex = fg + 2; - } else { - // If default color and bold - if (bold && terminal.options.enableBold) { - colorIndex = 1; - } - } - const isAscii = code < 256; - // A color is basic if it is one of the standard normal or bold weight - // colors of the characters held in the char atlas. Note that this excludes - // the normal weight _light_ color characters. - const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold); - const isDefaultColor = fg >= 256; - const isDefaultBackground = bg >= 256; - if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !italic) { - // ImageBitmap's draw about twice as fast as from a canvas - const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8; + fg += drawInBrightColor ? 8 : 0; + const atlasDidDraw = this._charAtlas && this._charAtlas.draw( + this._ctx, + {char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic}, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + ); - // Apply alpha to dim the character - if (dim) { - this._ctx.globalAlpha = DIM_OPACITY; - } - - // Draw the non-bold version of the same color if bold is not enabled - if (bold && !terminal.options.enableBold) { - // Ignore default color as it's not touched above - if (colorIndex > 1) { - colorIndex -= 8; - } - } - - this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, - colorIndex * charAtlasCellHeight, - charAtlasCellWidth, - this._scaledCharHeight, - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop, - charAtlasCellWidth, - this._scaledCharHeight); - } else { + if (!atlasDidDraw) { this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic); } - // This draws the atlas (for debugging purposes) - // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - // this._ctx.drawImage(this._charAtlas, 0, 0); } /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 9404a461..c41dece5 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -150,6 +150,7 @@ export class Renderer extends EventEmitter implements IRenderer { } public onOptionsChanged(): void { + this.colorManager.allowTransparency = this._terminal.options.allowTransparency; this._runOperation(l => l.onOptionsChanged(this._terminal)); } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 8d8d6d4d..d6f96936 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -116,13 +116,6 @@ export class TextRenderLayer extends BaseRenderLayer { } } - if (flags & FLAGS.BOLD) { - // Convert the FG color to the bold variant - if (fg < 8) { - fg += 8; - } - } - callback(code, char, width, x, y, fg, bg, flags); } } @@ -215,6 +208,8 @@ export class TextRenderLayer extends BaseRenderLayer { return; } + this._charAtlas.beginFrame(); + this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1); this._drawBackground(terminal, firstRow, lastRow); this._drawForeground(terminal, firstRow, lastRow); diff --git a/src/renderer/atlas/BaseCharAtlas.ts b/src/renderer/atlas/BaseCharAtlas.ts new file mode 100644 index 00000000..50d35faa --- /dev/null +++ b/src/renderer/atlas/BaseCharAtlas.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IGlyphIdentifier } from './Types'; + +export default abstract class BaseCharAtlas { + private _didWarmUp: boolean = false; + + /** + * Perform any work needed to warm the cache before it can be used. May be called multiple times. + * Implement _doWarmUp instead if you only want to get called once. + */ + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; + } + } + + /** + * Perform any work needed to warm the cache before it can be used. Used by the default + * implementation of warmUp(), and will only be called once. + */ + protected _doWarmUp(): void { } + + /** + * Called when we start drawing a new frame. + * + * TODO: We rely on this getting called by TextRenderLayer. This should really be called by + * Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead + * of BaseRenderLayer. + */ + public beginFrame(): void { } + + /** + * May be called before warmUp finishes, however it is okay for the implementation to + * do nothing and return false in that case. + * + * @param ctx Where to draw the character onto. + * @param glyph Information about what to draw + * @param x The position on the context to start drawing at + * @param y The position on the context to start drawing at + * @returns The success state. True if we drew the character. + */ + public abstract draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean; +} diff --git a/src/renderer/atlas/CharAtlas.ts b/src/renderer/atlas/CharAtlasCache.ts similarity index 66% rename from src/renderer/atlas/CharAtlas.ts rename to src/renderer/atlas/CharAtlasCache.ts index f516919f..eee93d6c 100644 --- a/src/renderer/atlas/CharAtlas.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -6,16 +6,27 @@ import { ITerminal } from '../../Types'; import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; -import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator'; import { generateConfig, configEquals } from './CharAtlasUtils'; +import BaseCharAtlas from './BaseCharAtlas'; +import DynamicCharAtlas from './DynamicCharAtlas'; +import NoneCharAtlas from './NoneCharAtlas'; +import StaticCharAtlas from './StaticCharAtlas'; + +const charAtlasImplementations = { + 'none': NoneCharAtlas, + 'static': StaticCharAtlas, + 'dynamic': DynamicCharAtlas +}; interface ICharAtlasCacheEntry { - bitmap: HTMLCanvasElement | Promise; + atlas: BaseCharAtlas; config: ICharAtlasConfig; + // N.B. This implementation potentially holds onto copies of the terminal forever, so + // this may cause memory leaks. ownedBy: ITerminal[]; } -let charAtlasCache: ICharAtlasCacheEntry[] = []; +const charAtlasCache: ICharAtlasCacheEntry[] = []; /** * Acquires a char atlas, either generating a new one or returning an existing @@ -23,7 +34,12 @@ let charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { +export function acquireCharAtlas( + terminal: ITerminal, + colors: IColorSet, + scaledCharWidth: number, + scaledCharHeight: number +): BaseCharAtlas { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); // TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed) @@ -34,16 +50,15 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC const ownedByIndex = entry.ownedBy.indexOf(terminal); if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { - return entry.bitmap; - } else { - // The configs differ, release the terminal from the entry - if (entry.ownedBy.length === 1) { - charAtlasCache.splice(i, 1); - } else { - entry.ownedBy.splice(ownedByIndex, 1); - } - break; + return entry.atlas; } + // The configs differ, release the terminal from the entry + if (entry.ownedBy.length === 1) { + charAtlasCache.splice(i, 1); + } else { + entry.ownedBy.splice(ownedByIndex, 1); + } + break; } } @@ -53,24 +68,20 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC if (configEquals(entry.config, newConfig)) { // Add the terminal to the cache entry and return entry.ownedBy.push(terminal); - return entry.bitmap; + return entry.atlas; } } - const canvasFactory = (width: number, height: number) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - }; - const newEntry: ICharAtlasCacheEntry = { - bitmap: generateCharAtlas(window, canvasFactory, newConfig), + atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas]( + document, + newConfig + ), config: newConfig, ownedBy: [terminal] }; charAtlasCache.push(newEntry); - return newEntry.bitmap; + return newEntry.atlas; } /** diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 24ed5a48..39284d32 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -8,15 +8,19 @@ import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + // null out some fields that don't matter const clonedColors = { foreground: colors.foreground, background: colors.background, cursor: null, cursorAccent: null, selection: null, + // For the static char atlas, we only use the first 16 colors, but we need all 256 for the + // dynamic character atlas. ansi: colors.ansi.slice(0, 16) }; return { + type: terminal.options.experimentalCharAtlas, devicePixelRatio: window.devicePixelRatio, scaledCharWidth, scaledCharHeight, @@ -35,7 +39,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean return false; } } - return a.devicePixelRatio === b.devicePixelRatio && + return a.type === b.type && + a.devicePixelRatio === b.devicePixelRatio && a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && a.fontWeight === b.fontWeight && diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts new file mode 100644 index 00000000..bf2d6f90 --- /dev/null +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import { IColor } from '../../shared/Types'; +import BaseCharAtlas from './BaseCharAtlas'; +import { DEFAULT_ANSI_COLORS } from '../ColorManager'; +import { clearColor } from '../../shared/atlas/CharAtlasGenerator'; +import LRUMap from './LRUMap'; + +// In practice we're probably never going to exhaust a texture this large. For debugging purposes, +// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. +const TEXTURE_WIDTH = 1024; +const TEXTURE_HEIGHT = 1024; + +const TRANSPARENT_COLOR = { + css: 'rgba(0, 0, 0, 0)', + rgba: 0 +}; + +// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the +// cache in a single frame, give up on trying to cache anything else, and try to finish the current +// frame ASAP. +// +// This helps to limit the amount of damage a program can do when it would otherwise thrash the +// cache. +const FRAME_CACHE_DRAW_LIMIT = 100; + +interface IGlyphCacheValue { + index: number; + isEmpty: boolean; +} + +function getGlyphCacheKey(glyph: IGlyphIdentifier): string { + const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1); + return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`; +} + +export default class DynamicCharAtlas extends BaseCharAtlas { + // An ordered map that we're using to keep track of where each glyph is in the atlas texture. + // It's ordered so that we can determine when to remove the old entries. + private _cacheMap: LRUMap; + + // The texture that the atlas is drawn to + private _cacheCanvas: HTMLCanvasElement; + private _cacheCtx: CanvasRenderingContext2D; + + // A temporary context that glyphs are drawn to before being transfered to the atlas. + private _tmpCtx: CanvasRenderingContext2D; + + // The number of characters stored in the atlas by width/height + private _width: number; + private _height: number; + + private _drawToCacheCount: number = 0; + + constructor(document: Document, private _config: ICharAtlasConfig) { + super(); + this._cacheCanvas = document.createElement('canvas'); + this._cacheCanvas.width = TEXTURE_WIDTH; + this._cacheCanvas.height = TEXTURE_HEIGHT; + // The canvas needs alpha because we use clearColor to convert the background color to alpha. + // It might also contain some characters with transparent backgrounds if allowTransparency is + // set. + this._cacheCtx = this._cacheCanvas.getContext('2d', {alpha: true}); + + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = this._config.scaledCharWidth; + tmpCanvas.height = this._config.scaledCharHeight; + this._tmpCtx = tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}); + + this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); + this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); + const capacity = this._width * this._height; + this._cacheMap = new LRUMap(capacity); + this._cacheMap.prealloc(capacity); + + // This is useful for debugging + // document.body.appendChild(this._cacheCanvas); + } + + public beginFrame(): void { + this._drawToCacheCount = 0; + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean { + const glyphKey = getGlyphCacheKey(glyph); + const cacheValue = this._cacheMap.get(glyphKey); + if (cacheValue != null) { + this._drawFromCache(ctx, cacheValue, x, y); + return true; + } else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) { + let index; + if (this._cacheMap.size < this._cacheMap.capacity) { + index = this._cacheMap.size; + } else { + // we're out of space, so our call to set will delete this item + index = this._cacheMap.peek().index; + } + const cacheValue = this._drawToCache(glyph, index); + this._cacheMap.set(glyphKey, cacheValue); + this._drawFromCache(ctx, cacheValue, x, y); + return true; + } + return false; + } + + private _canCache(glyph: IGlyphIdentifier): boolean { + // Only cache ascii and extended characters for now, to be safe. In the future, we could do + // something more complicated to determine the expected width of a character. + // + // If we switch the renderer over to webgl at some point, we may be able to use blending modes + // to draw overlapping glyphs from the atlas: + // https://github.com/servo/webrender/issues/464#issuecomment-255632875 + // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html + return glyph.code < 256; + } + + private _toCoordinates(index: number): [number, number] { + return [ + (index % this._width) * this._config.scaledCharWidth, + Math.floor(index / this._width) * this._config.scaledCharHeight + ]; + } + + private _drawFromCache( + ctx: CanvasRenderingContext2D, + cacheValue: IGlyphCacheValue, + x: number, + y: number + ): void { + // We don't actually need to do anything if this is whitespace. + if (cacheValue.isEmpty) { + return; + } + const [cacheX, cacheY] = this._toCoordinates(cacheValue.index); + ctx.drawImage( + this._cacheCanvas, + cacheX, + cacheY, + this._config.scaledCharWidth, + this._config.scaledCharHeight, + x, + y, + this._config.scaledCharWidth, + this._config.scaledCharHeight + ); + } + + private _getColorFromAnsiIndex(idx: number): IColor { + if (idx < this._config.colors.ansi.length) { + return this._config.colors.ansi[idx]; + } + return DEFAULT_ANSI_COLORS[idx]; + } + + private _getBackgroundColor(glyph: IGlyphIdentifier): IColor { + if (this._config.allowTransparency) { + // The background color might have some transparency, so we need to render it as fully + // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice + // around the anti-aliased edges of the glyph, and it would look too dark. + return TRANSPARENT_COLOR; + } else if (glyph.bg === INVERTED_DEFAULT_COLOR) { + return this._config.colors.foreground; + } else if (glyph.bg < 256) { + return this._getColorFromAnsiIndex(glyph.bg); + } + return this._config.colors.background; + } + + private _getForegroundColor(glyph: IGlyphIdentifier): IColor { + if (glyph.fg === INVERTED_DEFAULT_COLOR) { + return this._config.colors.background; + } else if (glyph.fg < 256) { + // 256 color support + return this._getColorFromAnsiIndex(glyph.fg); + } + return this._config.colors.foreground; + } + + // TODO: We do this (or something similar) in multiple places. We should split this off + // into a shared function. + private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue { + this._drawToCacheCount++; + + this._tmpCtx.save(); + + // draw the background + const backgroundColor = this._getBackgroundColor(glyph); + // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of + // transparency in backgroundColor + this._tmpCtx.globalCompositeOperation = 'copy'; + this._tmpCtx.fillStyle = backgroundColor.css; + this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); + this._tmpCtx.globalCompositeOperation = 'source-over'; + + // draw the foreground/glyph + const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight; + const fontStyle = glyph.italic ? 'italic' : ''; + this._tmpCtx.font = + `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; + this._tmpCtx.textBaseline = 'top'; + + this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; + + // Apply alpha to dim the character + if (glyph.dim) { + this._tmpCtx.globalAlpha = DIM_OPACITY; + } + // Draw the character + this._tmpCtx.fillText(glyph.char, 0, 0); + this._tmpCtx.restore(); + + // clear the background from the character to avoid issues with drawing over the previous + // character if it extends past it's bounds + const imageData = this._tmpCtx.getImageData( + 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight + ); + let isEmpty = false; + if (!this._config.allowTransparency) { + isEmpty = clearColor(imageData, backgroundColor); + } + + // copy the data from imageData to _cacheCanvas + const [x, y] = this._toCoordinates(index); + // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us + this._cacheCtx.putImageData(imageData, x, y); + + return { + index, + isEmpty + }; + } +} diff --git a/src/renderer/atlas/LRUMap.test.ts b/src/renderer/atlas/LRUMap.test.ts new file mode 100644 index 00000000..ba01e410 --- /dev/null +++ b/src/renderer/atlas/LRUMap.test.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import LRUMap from './LRUMap'; + +describe('LRUMap', () => { + it('can be used to store and retrieve values', () => { + const map = new LRUMap(10); + map.set('keya', 'valuea'); + map.set('keyb', 'valueb'); + map.set('keyc', 'valuec'); + assert.strictEqual(map.get('keya'), 'valuea'); + assert.strictEqual(map.get('keyb'), 'valueb'); + assert.strictEqual(map.get('keyc'), 'valuec'); + }); + + it('maintains a size from insertions', () => { + const map = new LRUMap(10); + assert.strictEqual(map.size, 0); + map.set('a', 'value'); + assert.strictEqual(map.size, 1); + map.set('b', 'value'); + assert.strictEqual(map.size, 2); + }); + + it('deletes the oldest entry when the capacity is exceeded', () => { + const map = new LRUMap(4); + map.set('a', 'value'); + map.set('b', 'value'); + map.set('c', 'value'); + map.set('d', 'value'); + map.set('e', 'value'); + assert.isNull(map.get('a')); + assert.isNotNull(map.get('b')); + assert.isNotNull(map.get('c')); + assert.isNotNull(map.get('d')); + assert.isNotNull(map.get('e')); + assert.strictEqual(map.size, 4); + }); + + it('prevents a recently accessed entry from getting deleted', () => { + const map = new LRUMap(2); + map.set('a', 'value'); + map.set('b', 'value'); + map.get('a'); + // a would normally get deleted here, except that we called get() + map.set('c', 'value'); + assert.isNotNull(map.get('a')); + // b got deleted instead of a + assert.isNull(map.get('b')); + assert.isNotNull(map.get('c')); + }); + + it('supports mutation', () => { + const map = new LRUMap(10); + map.set('keya', 'oldvalue'); + map.set('keya', 'newvalue'); + // mutation doesn't change the size + assert.strictEqual(map.size, 1); + assert.strictEqual(map.get('keya'), 'newvalue'); + }); +}); diff --git a/src/renderer/atlas/LRUMap.ts b/src/renderer/atlas/LRUMap.ts new file mode 100644 index 00000000..4a03f2aa --- /dev/null +++ b/src/renderer/atlas/LRUMap.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface ILinkedListNode { + prev: ILinkedListNode; + next: ILinkedListNode; + key: string; + value: T; +} + +export default class LRUMap { + private _map = {}; + private _head: ILinkedListNode = null; + private _tail: ILinkedListNode = null; + private _nodePool: ILinkedListNode[] = []; + public size: number = 0; + + constructor(public capacity: number) { } + + private _unlinkNode(node: ILinkedListNode): void { + const prev = node.prev; + const next = node.next; + if (node === this._head) { + this._head = next; + } + if (node === this._tail) { + this._tail = prev; + } + if (prev !== null) { + prev.next = next; + } + if (next !== null) { + next.prev = prev; + } + } + + private _appendNode(node: ILinkedListNode): void { + const tail = this._tail; + if (tail !== null) { + tail.next = node; + } + node.prev = tail; + node.next = null; + this._tail = node; + if (this._head === null) { + this._head = node; + } + } + + /** + * Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that + * they're more likely to live next to each other in memory, which seems to improve performance. + * + * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for + * large maps. + */ + public prealloc(count: number): void { + const nodePool = this._nodePool; + for (let i = 0; i < count; i++) { + nodePool.push({ + prev: null, + next: null, + key: null, + value: null + }); + } + } + + public get(key: string): T | null { + // This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However, + // it's faster than calling hasOwnProperty, and in our case, it would never overlap. + const node = this._map[key]; + if (node !== undefined) { + this._unlinkNode(node); + this._appendNode(node); + return node.value; + } + return null; + } + + public peek(): T | null { + const head = this._head; + return head === null ? null : head.value; + } + + public set(key: string, value: T): void { + // This is unsafe: See note above. + let node = this._map[key]; + if (node !== undefined) { + // already exists, we just need to mutate it and move it to the end of the list + node = this._map[key]; + this._unlinkNode(node); + node.value = value; + } else if (this.size >= this.capacity) { + // we're out of space: recycle the head node, move it to the tail + node = this._head; + this._unlinkNode(node); + delete this._map[node.key]; + node.key = key; + node.value = value; + this._map[key] = node; + } else { + // make a new element + const nodePool = this._nodePool; + if (nodePool.length > 0) { + // use a preallocated node if we can + node = nodePool.pop(); + node.key = key; + node.value = value; + } else { + node = { + prev: null, + next: null, + key, + value + }; + } + this._map[key] = node; + this.size++; + } + this._appendNode(node); + } +} diff --git a/src/renderer/atlas/NoneCharAtlas.ts b/src/renderer/atlas/NoneCharAtlas.ts new file mode 100644 index 00000000..1cbc9eea --- /dev/null +++ b/src/renderer/atlas/NoneCharAtlas.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + * + * A dummy CharAtlas implementation that always fails to draw characters. + */ + +import { IGlyphIdentifier } from './Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import BaseCharAtlas from './BaseCharAtlas'; + +export default class NoneCharAtlas extends BaseCharAtlas { + constructor(document: Document, config: ICharAtlasConfig) { + super(); + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean { + return false; + } +} diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts new file mode 100644 index 00000000..b19074b8 --- /dev/null +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { DIM_OPACITY, IGlyphIdentifier } from './Types'; +import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from '../../shared/atlas/Types'; +import { generateStaticCharAtlasTexture } from '../../shared/atlas/CharAtlasGenerator'; +import BaseCharAtlas from './BaseCharAtlas'; + +export default class StaticCharAtlas extends BaseCharAtlas { + private _texture: HTMLCanvasElement | ImageBitmap; + + constructor(private _document: Document, private _config: ICharAtlasConfig) { + super(); + } + + private _canvasFactory = (width: number, height: number) => { + const canvas = this._document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + + public _doWarmUp(): void { + const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config); + if (result instanceof HTMLCanvasElement) { + this._texture = result; + } else { + result.then(texture => { + this._texture = texture; + }); + } + } + + private _isCached(glyph: IGlyphIdentifier, colorIndex: number): boolean { + const isAscii = glyph.code < 256; + // A color is basic if it is one of the 4 bit ANSI colors. + const isBasicColor = glyph.fg < 16; + const isDefaultColor = glyph.fg >= 256; + const isDefaultBackground = glyph.bg >= 256; + return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !glyph.italic; + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean { + // we're not warmed up yet + if (this._texture == null) { + return false; + } + + let colorIndex = 0; + if (glyph.fg < 256) { + colorIndex = 2 + glyph.fg + (glyph.bold ? 16 : 0); + } else { + // If default color and bold + if (glyph.bold) { + colorIndex = 1; + } + } + if (!this._isCached(glyph, colorIndex)) { + return false; + } + // ImageBitmap's draw about twice as fast as from a canvas + const charAtlasCellWidth = this._config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const charAtlasCellHeight = this._config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + + // Apply alpha to dim the character + if (glyph.dim) { + ctx.globalAlpha = DIM_OPACITY; + } + + ctx.drawImage( + this._texture, + glyph.code * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._config.scaledCharHeight, + x, + y, + charAtlasCellWidth, + this._config.scaledCharHeight + ); + + return true; + } +} diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 34f01d39..46e4c9aa 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -5,3 +5,13 @@ export const INVERTED_DEFAULT_COLOR = -1; export const DIM_OPACITY = 0.5; + +export interface IGlyphIdentifier { + char: string; + code: number; + bg: number; + fg: number; + bold: boolean; + dim: boolean; + italic: boolean; +} diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index fc83c7ce..276da78d 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -5,6 +5,7 @@ import { FontWeight } from 'xterm'; import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types'; +import { IColor } from '../Types'; import { isFirefox } from '../utils/Browser'; declare const Promise: any; @@ -20,14 +21,14 @@ export interface IOffscreenCanvas { * Generates a char atlas. * @param context The window or worker context. * @param canvasFactory A function to generate a canvas with a width or height. - * @param request The config for the new char atlas. + * @param config The config for the new char atlas. */ -export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { +export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; const canvas = canvasFactory( /*255 ascii chars*/255 * cellWidth, - (/*default+default bold*/2 + /*0-15*/16) * cellHeight + (/*default+default bold*/2 + /*0-15*/16 + /*0-15 bold*/16) * cellHeight ); const ctx = canvas.getContext('2d', {alpha: config.allowTransparency}); @@ -64,10 +65,6 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number // Colors 0-15 ctx.font = getFont(config.fontWeight, config); for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - ctx.font = getFont(config.fontWeightBold, config); - } const y = (colorIndex + 2) * cellHeight; // Draw ascii characters for (let i = 0; i < 256; i++) { @@ -80,6 +77,22 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number ctx.restore(); } } + + // Colors 0-15 bold + ctx.font = getFont(config.fontWeightBold, config); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + const y = (colorIndex + 2 + 16) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = config.colors.ansi[colorIndex].css; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.restore(); + } + } ctx.restore(); // Support is patchy for createImageBitmap at the moment, pass a canvas back @@ -91,34 +104,38 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number if (canvas instanceof HTMLCanvasElement) { // Just return the HTMLCanvas if it's a HTMLCanvasElement return canvas; - } else { - // Transfer to an ImageBitmap is this is an OffscreenCanvas - return new Promise(r => r(canvas.transferToImageBitmap())); } + // Transfer to an ImageBitmap is this is an OffscreenCanvas + return new Promise(r => r(canvas.transferToImageBitmap())); } const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Remove the background color from the image so characters may overlap - const r = config.colors.background.rgba >>> 24; - const g = config.colors.background.rgba >>> 16 & 0xFF; - const b = config.colors.background.rgba >>> 8 & 0xFF; - clearColor(charAtlasImageData, r, g, b); + clearColor(charAtlasImageData, config.colors.background); return context.createImageBitmap(charAtlasImageData); } /** * Makes a partiicular rgb color in an ImageData completely transparent. + * @returns True if the result is "empty", meaning all pixels are fully transparent. */ -function clearColor(imageData: ImageData, r: number, g: number, b: number): void { +export function clearColor(imageData: ImageData, color: IColor): boolean { + let isEmpty = true; + const r = color.rgba >>> 24; + const g = color.rgba >>> 16 & 0xFF; + const b = color.rgba >>> 8 & 0xFF; for (let offset = 0; offset < imageData.data.length; offset += 4) { if (imageData.data[offset] === r && imageData.data[offset + 1] === g && imageData.data[offset + 2] === b) { imageData.data[offset + 3] = 0; + } else { + isEmpty = false; } } + return isEmpty; } function getFont(fontWeight: FontWeight, config: ICharAtlasConfig): string { diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts index 4a66d554..25eaa716 100644 --- a/src/shared/atlas/Types.ts +++ b/src/shared/atlas/Types.ts @@ -9,6 +9,7 @@ import { IColorSet } from '../Types'; export const CHAR_ATLAS_CELL_SPACING = 1; export interface ICharAtlasConfig { + type: 'none' | 'static' | 'dynamic'; devicePixelRatio: number; fontSize: number; fontFamily: string; diff --git a/src/xterm.css b/src/xterm.css index eec41a05..6e7d2f96 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -126,13 +126,17 @@ line-height: normal; } +.xterm { + cursor: text; +} + .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; } -.xterm:not(.enable-mouse-events) { - cursor: text; +.xterm.xterm-cursor-pointer { + cursor: pointer; } .xterm .xterm-accessibility, @@ -153,7 +157,3 @@ height: 1px; overflow: hidden; } - -.xterm-cursor-pointer { - cursor: pointer; -} diff --git a/tsconfig.json b/tsconfig.json index ffd9ab68..e56930e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,12 @@ "compilerOptions": { "module": "commonjs", "target": "es5", + "lib": [ + "DOM", + "ES5", + "ScriptHost", + "ES2015.Promise" + ], "rootDir": "src", "outDir": "lib", "sourceMap": true, diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 65d533de..a8724922 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -54,6 +54,11 @@ declare module 'xterm' { */ disableStdin?: boolean; + /** + * Whether to draw bold text in bright colors. The default is true. + */ + drawBoldTextInBrightColors?: boolean; + /** * Whether to enable the rendering of bold text. * @@ -61,6 +66,24 @@ declare module 'xterm' { */ enableBold?: boolean; + /** + * What character atlas implementation to use. The character atlas caches drawn characters, + * speeding up rendering significantly. However, it can introduce some minor rendering + * artifacts. + * + * - 'none': Don't use an atlas. + * - 'static': Generate an atlas when the terminal starts or is reconfigured. This atlas will + * only contain ASCII characters in 16 colors. + * - 'dynamic': Generate an atlas using a LRU cache as characters are requested. Limited to + * ASCII characters (for now), but supports 256 colors. For characters covered by the static + * cache, it's slightly slower in comparison, since there's more overhead involved in + * managing the cache. + * + * Currently defaults to 'static'. This option may be removed in the future. If it is, passed + * parameters will be ignored. + */ + experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; + /** * The font size used to render text. */