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/README.md b/README.md index 48a46fd9..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. @@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t @@ -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 29b6fcf0..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

diff --git a/package.json b/package.json index 49ccc387..5f3d835e 100644 --- a/package.json +++ b/package.json @@ -2,47 +2,15 @@ "name": "xterm", "description": "Full xterm terminal, in your browser", "version": "3.3.0", - "ignore": [ - "demo", - "test", - ".gitignore" - ], "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", @@ -67,6 +35,7 @@ "npm-run-all": "^4.1.2", "sorcery": "^0.10.0", "tslint": "^5.9.1", + "tslint-consistent-codestyle": "^1.13.0", "typescript": "~2.7.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", @@ -77,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", @@ -87,6 +56,5 @@ "coveralls": "gulp coveralls", "webpack": "gulp webpack", "watch": "gulp watch" - }, - "dependencies": {} + } } diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 5a1f4770..a7e205e3 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -12,9 +12,9 @@ import { IDisposable } from 'xterm'; const MAX_ROWS_TO_READ = 20; -enum BoundaryPosition { - Top, - Bottom +const enum BoundaryPosition { + TOP, + BOTTOM } export class AccessibilityManager implements IDisposable { @@ -54,8 +54,8 @@ export class AccessibilityManager implements IDisposable { this._rowContainer.appendChild(this._rowElements[i]); } - this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top); - this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom); + this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP); + this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM); this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); @@ -101,11 +101,11 @@ export class AccessibilityManager implements IDisposable { private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { const boundaryElement = e.target; - const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2]; + const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2]; // Don't scroll if the buffer top has reached the end in that direction const posInSet = boundaryElement.getAttribute('aria-posinset'); - const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`; + const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`; if (posInSet === lastRowPos) { return; } @@ -119,7 +119,7 @@ export class AccessibilityManager implements IDisposable { // Remove old boundary element from array let topBoundaryElement: HTMLElement; let bottomBoundaryElement: HTMLElement; - if (position === BoundaryPosition.Top) { + if (position === BoundaryPosition.TOP) { topBoundaryElement = boundaryElement; bottomBoundaryElement = this._rowElements.pop()!; this._rowContainer.removeChild(bottomBoundaryElement); @@ -134,7 +134,7 @@ export class AccessibilityManager implements IDisposable { bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener); // Add new element to array/DOM - if (position === BoundaryPosition.Top) { + if (position === BoundaryPosition.TOP) { const newElement = this._createAccessibilityTreeNode(); this._rowElements.unshift(newElement); this._rowContainer.insertAdjacentElement('afterbegin', newElement); @@ -149,10 +149,10 @@ export class AccessibilityManager implements IDisposable { this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); // Scroll up - this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1); + this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1); // Focus new boundary before element - this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus(); + this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus(); // Prevent the standard behavior e.preventDefault(); diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 389cb782..b721b7f1 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -92,11 +92,10 @@ export class CompositionHelper { } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { // Continue composing if the keyCode is a modifier key return false; - } else { - // Finish composition immediately. This is mainly here for the case where enter is - // pressed and the handler needs to be triggered before the command is executed. - this._finalizeComposition(false); } + // Finish composition immediately. This is mainly here for the case where enter is + // pressed and the handler needs to be triggered before the command is executed. + this._finalizeComposition(false); } if (ev.keyCode === 229) { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 4df3e695..7696324e 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1263,6 +1263,9 @@ export class InputHandler implements IInputHandler { } else if (p === 1) { // bold text flags |= FLAGS.BOLD; + } else if (p === 3) { + // italic text + flags |= FLAGS.ITALIC; } else if (p === 4) { // underlined text flags |= FLAGS.UNDERLINE; diff --git a/src/Parser.ts b/src/Parser.ts index 21e5a612..372d8443 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -150,7 +150,7 @@ csiStateHandler['s'] = (handler, params) => handler.saveCursor(params); csiStateHandler['u'] = (handler, params) => handler.restoreCursor(params); csiStateHandler[C0.CAN] = (handler, params, prefix, postfix, parser) => parser.setState(ParserState.NORMAL); -export enum ParserState { +export const enum ParserState { NORMAL = 0, ESCAPED = 1, CSI_PARAM = 2, diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index a50203bd..93da887a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -54,7 +54,7 @@ interface IWordPosition { /** * A selection mode, this drives how the selection behaves on mouse move. */ -enum SelectionMode { +const enum SelectionMode { NORMAL, WORD, LINE diff --git a/src/Terminal.ts b/src/Terminal.ts index 13495b2c..fc3998d8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -103,6 +103,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { cursorStyle: 'block', bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', + drawBoldTextInBrightColors: true, enableBold: true, experimentalCharAtlas: 'static', fontFamily: 'courier-new, courier, monospace', diff --git a/src/Types.ts b/src/Types.ts index 2442b027..15498ce1 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -17,7 +17,7 @@ export type LineData = CharData[]; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; -export enum LinkHoverEventTypes { +export const enum LinkHoverEventTypes { HOVER = 'linkhover', TOOLTIP = 'linktooltip', LEAVE = 'linkleave' diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index f77637ea..7f2e4f05 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -6,11 +6,11 @@ import { ITerminal, ICircularList, LineData } from '../Types'; import { C0 } from '../EscapeSequences'; -enum Direction { - Up = 'A', - Down = 'B', - Right = 'C', - Left = 'D' +const enum Direction { + UP = 'A', + DOWN = 'B', + RIGHT = 'C', + LEFT = 'D' } export class AltClickHandler { @@ -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()); } } @@ -73,12 +77,11 @@ export class AltClickHandler { private _resetStartingRow(): string { if (this._moveToRequestedRow().length === 0) { return ''; - } else { - return repeat(this._bufferLine( - this._startCol, this._startRow, this._startCol, - this._startRow - this._wrappedRowsForRow(this._startRow), false - ).length, this._sequence(Direction.Left)); } + return repeat(this._bufferLine( + this._startCol, this._startRow, this._startCol, + this._startRow - this._wrappedRowsForRow(this._startRow), false + ).length, this._sequence(Direction.LEFT)); } /** @@ -110,7 +113,7 @@ export class AltClickHandler { return repeat(this._bufferLine( this._startCol, startRow, this._endCol, endRow, - direction === Direction.Right + direction === Direction.RIGHT ).length, this._sequence(direction)); } @@ -133,7 +136,7 @@ export class AltClickHandler { let endRow = this._endRow - this._wrappedRowsForRow(this._endRow); for (let i = 0; i < Math.abs(startRow - endRow); i++) { - let direction = this._verticalDirection() === Direction.Up ? -1 : 1; + let direction = this._verticalDirection() === Direction.UP ? -1 : 1; if ((this._lines.get(startRow + (direction * i))).isWrapped) { wrappedRows++; @@ -179,10 +182,9 @@ export class AltClickHandler { startRow <= this._endRow) || // down/right or same y/right (this._startCol >= this._endCol && startRow < this._endRow)) { // down/left or same y/left - return Direction.Right; - } else { - return Direction.Left; + return Direction.RIGHT; } + return Direction.LEFT; } /** @@ -190,10 +192,9 @@ export class AltClickHandler { */ private _verticalDirection(): Direction { if (this._startRow > this._endRow) { - return Direction.Up; - } else { - return Direction.Down; + return Direction.UP; } + return Direction.DOWN; } /** diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index c5b43b3a..b2a40290 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -214,7 +214,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param color The color of the character. */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { - this._ctx.font = this._getFont(terminal, false); + this._ctx.font = this._getFont(terminal, false, false); this._ctx.textBaseline = 'top'; this._clipRow(terminal, y); this._ctx.fillText( @@ -237,16 +237,18 @@ 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 drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void { + 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 { + 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}, + {char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic}, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop ); if (!atlasDidDraw) { - this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim); + this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic); } } @@ -261,9 +263,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void { + private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { this._ctx.save(); - this._ctx.font = this._getFont(terminal, bold); + this._ctx.font = this._getFont(terminal, bold, italic); this._ctx.textBaseline = 'top'; if (fg === INVERTED_DEFAULT_COLOR) { @@ -309,10 +311,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param terminal The terminal. * @param isBold If we should use the bold fontWeight. */ - protected _getFont(terminal: ITerminal, isBold: boolean): string { + protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string { const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; + const fontStyle = isItalic ? 'italic' : ''; - return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index ddbdca22..d6f96936 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -32,7 +32,7 @@ export class TextRenderLayer extends BaseRenderLayer { super.resize(terminal, dim); // Clear the character width cache if the font or width has changed - const terminalFont = this._getFont(terminal, false); + const terminalFont = this._getFont(terminal, false, false); if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { this._characterWidth = dim.scaledCharWidth; this._characterFont = terminalFont; @@ -116,30 +116,63 @@ 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); } } } + /** + * Draws the background for a specified range of columns. Tries to batch adjacent cells of the + * same color together to reduce draw calls. + */ private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void { + const ctx = this._ctx; + const cols = terminal.cols; + let startX: number = 0; + let startY: number = 0; + let prevFillStyle: string | null = null; + + ctx.save(); + this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. - const isDefaultBackground = bg >= 256; - if (!isDefaultBackground) { - this._ctx.save(); - this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css); - this.fillCells(x, y, width, 1); - this._ctx.restore(); + let nextFillStyle = null; // null represents default background color + if (bg === INVERTED_DEFAULT_COLOR) { + nextFillStyle = this._colors.foreground.css; + } else if (bg < 256) { + nextFillStyle = this._colors.ansi[bg].css; } + + if (prevFillStyle === null) { + // This is either the first iteration, or the default background was set. Either way, we + // don't need to draw anything. + startX = x; + startY = y; + } if (y !== startY) { + // our row changed, draw the previous row + ctx.fillStyle = prevFillStyle; + this.fillCells(startX, startY, cols - startX, 1); + startX = x; + startY = y; + } else if (prevFillStyle !== nextFillStyle) { + // our color changed, draw the previous characters in this row + ctx.fillStyle = prevFillStyle; + this.fillCells(startX, startY, x - startX, 1); + startX = x; + startY = y; + } + + prevFillStyle = nextFillStyle; }); + + // flush the last color we encountered + if (prevFillStyle !== null) { + ctx.fillStyle = prevFillStyle; + this.fillCells(startX, startY, cols - startX, 1); + } + + ctx.restore(); } private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void { @@ -164,7 +197,7 @@ export class TextRenderLayer extends BaseRenderLayer { terminal, char, code, width, x, y, fg, bg, - !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM) + !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC) ); }); } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 8c464bec..edecf8b0 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -10,13 +10,14 @@ import { IColorSet } from '../shared/Types'; /** * Flags used to render terminal text properly. */ -export enum FLAGS { +export const enum FLAGS { BOLD = 1, UNDERLINE = 2, BLINK = 4, INVERSE = 8, INVISIBLE = 16, - DIM = 32 + DIM = 32, + ITALIC = 64 } export interface IRenderer extends IEventEmitter { diff --git a/src/renderer/atlas/BaseCharAtlas.ts b/src/renderer/atlas/BaseCharAtlas.ts index 2680bd10..50d35faa 100644 --- a/src/renderer/atlas/BaseCharAtlas.ts +++ b/src/renderer/atlas/BaseCharAtlas.ts @@ -48,6 +48,6 @@ export default abstract class BaseCharAtlas { ctx: CanvasRenderingContext2D, glyph: IGlyphIdentifier, x: number, - y: number, + y: number ): boolean; } diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index 19e5497a..eee93d6c 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -15,7 +15,7 @@ import StaticCharAtlas from './StaticCharAtlas'; const charAtlasImplementations = { 'none': NoneCharAtlas, 'static': StaticCharAtlas, - 'dynamic': DynamicCharAtlas, + 'dynamic': DynamicCharAtlas }; interface ICharAtlasCacheEntry { @@ -38,7 +38,7 @@ export function acquireCharAtlas( terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, - scaledCharHeight: number, + scaledCharHeight: number ): BaseCharAtlas { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); @@ -51,15 +51,14 @@ export function acquireCharAtlas( if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { return entry.atlas; - } 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; } + // 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; } } @@ -76,10 +75,10 @@ export function acquireCharAtlas( const newEntry: ICharAtlasCacheEntry = { atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas]( document, - newConfig, + newConfig ), config: newConfig, - ownedBy: [terminal], + ownedBy: [terminal] }; charAtlasCache.push(newEntry); return newEntry.atlas; diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 9ec65b7a..39284d32 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -17,7 +17,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number 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), + ansi: colors.ansi.slice(0, 16) }; return { type: terminal.options.experimentalCharAtlas, diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 73e36e13..bf2d6f90 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -18,8 +18,8 @@ const TEXTURE_HEIGHT = 1024; const TRANSPARENT_COLOR = { css: 'rgba(0, 0, 0, 0)', - rgba: 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 @@ -35,7 +35,8 @@ interface IGlyphCacheValue { } function getGlyphCacheKey(glyph: IGlyphIdentifier): string { - return `${glyph.bg}_${glyph.fg}_${glyph.bold ? 0 : 1}${glyph.dim ? 0 : 1}${glyph.char}`; + 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 { @@ -89,7 +90,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { ctx: CanvasRenderingContext2D, glyph: IGlyphIdentifier, x: number, - y: number, + y: number ): boolean { const glyphKey = getGlyphCacheKey(glyph); const cacheValue = this._cacheMap.get(glyphKey); @@ -108,9 +109,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._cacheMap.set(glyphKey, cacheValue); this._drawFromCache(ctx, cacheValue, x, y); return true; - } else { - return false; } + return false; } private _canCache(glyph: IGlyphIdentifier): boolean { @@ -151,7 +151,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { x, y, this._config.scaledCharWidth, - this._config.scaledCharHeight, + this._config.scaledCharHeight ); } @@ -172,9 +172,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas { return this._config.colors.foreground; } else if (glyph.bg < 256) { return this._getColorFromAnsiIndex(glyph.bg); - } else { - return this._config.colors.background; } + return this._config.colors.background; } private _getForegroundColor(glyph: IGlyphIdentifier): IColor { @@ -183,9 +182,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas { } else if (glyph.fg < 256) { // 256 color support return this._getColorFromAnsiIndex(glyph.fg); - } else { - return this._config.colors.foreground; } + return this._config.colors.foreground; } // TODO: We do this (or something similar) in multiple places. We should split this off @@ -206,11 +204,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas { // draw the foreground/glyph const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight; + const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = - `${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - if (glyph.bold) { - this._tmpCtx.font = `bold ${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; @@ -226,7 +222,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { // 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, + 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight ); let isEmpty = false; if (!this._config.allowTransparency) { @@ -240,7 +236,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { return { index, - isEmpty, + isEmpty }; } } diff --git a/src/renderer/atlas/LRUMap.ts b/src/renderer/atlas/LRUMap.ts index 3f19c30a..4a03f2aa 100644 --- a/src/renderer/atlas/LRUMap.ts +++ b/src/renderer/atlas/LRUMap.ts @@ -4,10 +4,10 @@ */ interface ILinkedListNode { - prev: ILinkedListNode, - next: ILinkedListNode, - key: string, - value: T, + prev: ILinkedListNode; + next: ILinkedListNode; + key: string; + value: T; } export default class LRUMap { @@ -56,14 +56,14 @@ export default class LRUMap { * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for * large maps. */ - public prealloc(count: number) { + 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, + value: null }); } } @@ -114,7 +114,7 @@ export default class LRUMap { prev: null, next: null, key, - value, + value }; } this._map[key] = node; diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index ae29dd0a..b19074b8 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -22,33 +22,31 @@ export default class StaticCharAtlas extends BaseCharAtlas { return canvas; } - public _doWarmUp() { + 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 + 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 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 && glyph.fg < 16) && (glyph.fg < 8 || glyph.bold); + // 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; + return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !glyph.italic; } public draw( ctx: CanvasRenderingContext2D, glyph: IGlyphIdentifier, x: number, - y: number, + y: number ): boolean { // we're not warmed up yet if (this._texture == null) { @@ -57,7 +55,7 @@ export default class StaticCharAtlas extends BaseCharAtlas { let colorIndex = 0; if (glyph.fg < 256) { - colorIndex = glyph.fg + 2; + colorIndex = 2 + glyph.fg + (glyph.bold ? 16 : 0); } else { // If default color and bold if (glyph.bold) { @@ -76,14 +74,6 @@ export default class StaticCharAtlas extends BaseCharAtlas { ctx.globalAlpha = DIM_OPACITY; } - // Draw the non-bold version of the same color if bold is not enabled - /*if (glyph.bold && !terminal.options.enableBold) { - // Ignore default color as it's not touched above - if (colorIndex > 1) { - colorIndex -= 8; - } - }*/ - ctx.drawImage( this._texture, glyph.code * charAtlasCellWidth, diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 84481f6d..46e4c9aa 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -13,4 +13,5 @@ export interface IGlyphIdentifier { fg: number; bold: boolean; dim: boolean; + italic: boolean; } diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index 3e62c39d..276da78d 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -28,7 +28,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( 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}); @@ -65,10 +65,6 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( // 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++) { @@ -81,6 +77,22 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( 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 @@ -92,10 +104,9 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( 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); 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/tslint.json b/tslint.json index d42fda71..ac1b9c95 100644 --- a/tslint.json +++ b/tslint.json @@ -1,4 +1,7 @@ { + "rulesDirectory": [ + "tslint-consistent-codestyle" + ], "rules": { "array-type": [ true, @@ -86,6 +89,17 @@ "check-type", "check-type-operator", "check-preblock" + ], + + "naming-convention": [ + true, + {"type": "property", "modifiers": ["public", "static", "const"], "format": "UPPER_CASE"} + ], + "no-else-after-return": { + "options": "allow-else-if" + }, + "prefer-const-enum": [ + true ] } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f549180c..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. *