diff --git a/README.md b/README.md index 85c2ac18c..730fc9af9 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,10 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. +### Code structure + +`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker. + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 0a88bd376..6761dcd54 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -145,6 +145,8 @@ namespace methods_core { const r21: boolean = t.getOption('enableBold'); const r22: number = t.getOption('letterSpacing'); const r23: boolean = t.getOption('macOptionIsMeta'); + const r24: string = t.getOption('fontWeight'); + const r25: string = t.getOption('fontWeightBold'); } { const t: Terminal = new Terminal(); @@ -158,6 +160,10 @@ namespace methods_core { t.setOption('debug', true); t.setOption('disableStdin', true); t.setOption('enableBold', true); + t.setOption('fontWeight', 'normal'); + t.setOption('fontWeight', 'bold'); + t.setOption('fontWeightBold', 'normal'); + t.setOption('fontWeightBold', 'bold'); t.setOption('popOnBell', true); t.setOption('screenKeys', true); t.setOption('useFlowControl', true); diff --git a/package.json b/package.json index 6b1eb2302..cf3df57be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.0.0", + "version": "3.1.0-master", "ignore": [ "demo", "test", diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index fa3a094d3..676a39ed6 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -4,10 +4,11 @@ */ import * as Strings from './Strings'; -import { ITerminal, IBuffer, IDisposable } from './Interfaces'; -import { isMac } from './utils/Browser'; +import { ITerminal, IBuffer } from './Types'; +import { isMac } from './shared/utils/Browser'; import { RenderDebouncer } from './utils/RenderDebouncer'; import { addDisposableListener } from './utils/Dom'; +import { IDisposable } from 'xterm'; const MAX_ROWS_TO_READ = 20; const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-'; diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 210d29718..0607a573d 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { Buffer } from './Buffer'; import { CircularList } from './utils/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; diff --git a/src/Buffer.ts b/src/Buffer.ts index 3e7d61224..462cde56d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; -import { LineData, CharData } from './Types'; +import { LineData, CharData, ITerminal, IBuffer } from './Types'; export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; @@ -177,16 +176,13 @@ export class Buffer implements IBuffer { } // Make sure that the cursor stays on screen - if (this.y >= newRows) { - this.y = newRows - 1; - } + this.x = Math.min(this.x, newCols - 1); + this.y = Math.min(this.y, newRows - 1); if (addToY) { this.y += addToY; } - - if (this.x >= newCols) { - this.x = newCols - 1; - } + this.savedY = Math.min(this.savedY, newRows - 1); + this.savedX = Math.min(this.savedX, newCols - 1); this.scrollTop = 0; } diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index b9c1824db..009ebf2ec 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { BufferSet } from './BufferSet'; import { Buffer } from './Buffer'; import { MockTerminal } from './utils/TestUtils.test'; diff --git a/src/BufferSet.ts b/src/BufferSet.ts index da8c75f24..553b20560 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, IBufferSet } from './Interfaces'; +import { ITerminal, IBufferSet } from './Types'; import { Buffer } from './Buffer'; import { EventEmitter } from './EventEmitter'; diff --git a/src/Charsets.ts b/src/Charsets.ts index e62e3f9c0..fe0112ec7 100644 --- a/src/Charsets.ts +++ b/src/Charsets.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharset } from './Interfaces'; +import { ICharset } from './Types'; /** * The character sets supported by the terminal. These enable several languages diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index e588be783..2aa0449f1 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; interface IPosition { start: number; diff --git a/src/EventEmitter.test.ts b/src/EventEmitter.test.ts index c1f0a0aba..f2d31cf64 100644 --- a/src/EventEmitter.test.ts +++ b/src/EventEmitter.test.ts @@ -13,18 +13,6 @@ describe('EventEmitter', () => { eventEmitter = new EventEmitter(); }); - describe('once', () => { - it('should trigger the listener only once', () => { - let count = 0; - const listener = () => count++; - eventEmitter.once('test', listener); - eventEmitter.emit('test'); - assert.equal(count, 1); - eventEmitter.emit('test'); - assert.equal(count, 1); - }); - }); - describe('emit', () => { it('should emit events to listeners', () => { let count1 = 0; diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 7f090ac1a..cece1b9d9 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { IEventEmitter, IListenerType, IDisposable } from './Interfaces'; +import { XtermListener } from './Types'; +import { IEventEmitter, IDisposable } from 'xterm'; export class EventEmitter implements IEventEmitter { - private _events: {[type: string]: IListenerType[]}; + private _events: {[type: string]: XtermListener[]}; constructor() { // Restore the previous events if available, this will happen if the @@ -14,7 +15,7 @@ export class EventEmitter implements IEventEmitter { this._events = this._events || {}; } - public on(type: string, listener: IListenerType): void { + public on(type: string, listener: XtermListener): void { this._events[type] = this._events[type] || []; this._events[type].push(listener); } @@ -24,7 +25,7 @@ export class EventEmitter implements IEventEmitter { * @param type The event type. * @param handler The handler for the listener. */ - public addDisposableListener(type: string, handler: IListenerType): IDisposable { + public addDisposableListener(type: string, handler: XtermListener): IDisposable { this.on(type, handler); return { dispose: () => { @@ -38,7 +39,7 @@ export class EventEmitter implements IEventEmitter { }; } - public off(type: string, listener: IListenerType): void { + public off(type: string, listener: XtermListener): void { if (!this._events[type]) { return; } @@ -47,7 +48,7 @@ export class EventEmitter implements IEventEmitter { let i = obj.length; while (i--) { - if (obj[i] === listener || obj[i].listener === listener) { + if (obj[i] === listener) { obj.splice(i, 1); return; } @@ -60,16 +61,6 @@ export class EventEmitter implements IEventEmitter { } } - public once(type: string, listener: IListenerType): void { - function on(): void { - let args = Array.prototype.slice.call(arguments); - this.off(type, on); - listener.apply(this, args); - } - (on).listener = listener; - this.on(type, on); - } - public emit(type: string, ...args: any[]): void { if (!this._events[type]) { return; @@ -80,7 +71,7 @@ export class EventEmitter implements IEventEmitter { } } - public listeners(type: string): IListenerType[] { + public listeners(type: string): XtermListener[] { return this._events[type] || []; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1e8022bb7..fdf928359 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,10 +4,9 @@ * @license MIT */ -import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces'; +import { CharData, IInputHandler, IInputHandlingTerminal, ITerminal } from './Types'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; -import { CharData } from './Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; diff --git a/src/Interfaces.ts b/src/Interfaces.ts deleted file mode 100644 index 5a22a1854..000000000 --- a/src/Interfaces.ts +++ /dev/null @@ -1,382 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ICharset, ILinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; -import { IColorSet, IRenderer } from './renderer/Interfaces'; -import { IMouseZoneManager } from './input/Interfaces'; - -export interface IBrowser { - isNode: boolean; - userAgent: string; - platform: string; - isFirefox: boolean; - isMSIE: boolean; - isMac: boolean; - isIpad: boolean; - isIphone: boolean; - isMSWindows: boolean; -} - -export interface IBufferAccessor { - buffer: IBuffer; -} - -export interface IElementAccessor { - element: HTMLElement; -} - -export interface ILinkifierAccessor { - linkifier: ILinkifier; -} - -export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter { - selectionManager: ISelectionManager; - charMeasure: ICharMeasure; - textarea: HTMLTextAreaElement; - renderer: IRenderer; - rows: number; - cols: number; - browser: IBrowser; - writeBuffer: string[]; - cursorHidden: boolean; - cursorState: number; - defAttr: number; - options: ITerminalOptions; - buffers: IBufferSet; - isFocused: boolean; - mouseHelper: IMouseHelper; - bracketedPasteMode: boolean; - - /** - * Emit the 'data' event and populate the given data. - * @param data The data to populate in the event. - */ - handler(data: string): void; - scrollLines(disp: number, suppressScrollEvent?: boolean): void; - scrollToRow(row: number): number; - cancel(ev: Event, force?: boolean): boolean | void; - log(text: string): void; - reset(): void; - showCursor(): void; - blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; - refresh(start: number, end: number): void; -} - -/** - * This interface encapsulates everything needed from the Terminal by the - * InputHandler. This cleanly separates the large amount of methods needed by - * InputHandler cleanly from the ITerminal interface. - */ -export interface IInputHandlingTerminal extends IEventEmitter { - element: HTMLElement; - options: ITerminalOptions; - cols: number; - rows: number; - charset: ICharset; - gcharset: number; - glevel: number; - charsets: ICharset[]; - applicationKeypad: boolean; - applicationCursor: boolean; - originMode: boolean; - insertMode: boolean; - wraparoundMode: boolean; - bracketedPasteMode: boolean; - defAttr: number; - curAttr: number; - prefix: string; - savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: boolean; - sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; - cursorHidden: boolean; - - buffers: IBufferSet; - buffer: IBuffer; - viewport: IViewport; - selectionManager: ISelectionManager; - - bell(): void; - focus(): void; - convertEol: boolean; - updateRange(y: number): void; - scroll(isWrapped?: boolean): void; - setgLevel(g: number): void; - eraseAttr(): number; - eraseRight(x: number, y: number): void; - eraseLine(y: number): void; - eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): LineData; - is(term: string): boolean; - send(data: string): void; - setgCharset(g: number, charset: ICharset): void; - resize(x: number, y: number): void; - log(text: string, data?: any): void; - 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; -} - -export interface ITerminalOptions { - bellSound?: string; - bellStyle?: string; - cancelEvents?: boolean; - cols?: number; - convertEol?: boolean; - cursorBlink?: boolean; - cursorStyle?: string; - debug?: boolean; - disableStdin?: boolean; - enableBold?: boolean; - fontSize?: number; - fontFamily?: string; - handler?: (data: string) => void; - letterSpacing?: number; - lineHeight?: number; - macOptionIsMeta?: boolean; - rows?: number; - screenKeys?: boolean; - screenReaderMode?: boolean; - scrollback?: number; - tabStopWidth?: number; - termName?: string; - theme?: ITheme; - useFlowControl?: boolean; -} - -export interface IBuffer { - lines: ICircularList; - ydisp: number; - ybase: number; - y: number; - x: number; - tabs: any; - scrollBottom: number; - scrollTop: number; - savedY: number; - savedX: number; - isCursorInViewport: boolean; - translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; - nextStop(x?: number): number; - prevStop(x?: number): number; -} - -export interface IBufferSet extends IEventEmitter { - alt: IBuffer; - normal: IBuffer; - active: IBuffer; - - activateNormalBuffer(): void; - activateAltBuffer(): void; -} - -export interface IMouseHelper { - getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; - getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; -} - -export interface IViewport { - syncScrollArea(): void; - onWheel(ev: WheelEvent): void; - onTouchStart(ev: TouchEvent): void; - onTouchMove(ev: TouchEvent): void; - onThemeChanged(colors: IColorSet): void; -} - -export interface ISelectionManager { - selectionText: string; - selectionStart: [number, number]; - selectionEnd: [number, number]; - - disable(): void; - enable(): void; - setSelection(row: number, col: number, length: number): void; -} - -export interface ICompositionHelper { - compositionstart(): void; - compositionupdate(ev: CompositionEvent): void; - compositionend(): void; - updateCompositionElements(dontRecurse?: boolean): void; - keydown(ev: KeyboardEvent): boolean; -} - -export interface ICharMeasure { - width: number; - height: number; - measure(options: ITerminalOptions): void; -} - -export interface ILinkifier extends IEventEmitter { - attachToDom(mouseZoneManager: IMouseZoneManager): void; - linkifyRows(start: number, end: number): void; - setHypertextLinkHandler(handler: LinkMatcherHandler): void; - setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; - registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; - deregisterLinkMatcher(matcherId: number): boolean; -} - -export interface ICircularList extends IEventEmitter { - length: number; - maxLength: number; - forEach: (callbackfn: (value: T, index: number) => void) => void; - - get(index: number): T; - set(index: number, value: T): void; - push(value: T): void; - pop(): T; - splice(start: number, deleteCount: number, ...items: T[]): void; - trimStart(count: number): void; - shiftElements(start: number, count: number, offset: number): void; -} - -export interface IEventEmitter { - on(type: string, listener: IListenerType): void; - off(type: string, listener: IListenerType): void; - emit(type: string, data?: any): void; - addDisposableListener(type: string, handler: IListenerType): IDisposable; -} - -export interface IListenerType { - (data?: any): void; - listener?: (data?: any) => void; -} - -export interface ILinkMatcherOptions { - /** - * The index of the link from the regex.match(text) call. This defaults to 0 - * (for regular expressions without capture groups). - */ - matchIndex?: number; - /** - * A callback that validates an individual link, returning true if valid and - * false if invalid. - */ - validationCallback?: LinkMatcherValidationCallback; - /** - * A callback that fires when the mouse hovers over a link. - */ - tooltipCallback?: LinkMatcherHandler; - /** - * A callback that fires when the mouse leaves a link that was hovered. - */ - leaveCallback?: () => void; - /** - * The priority of the link matcher, this defines the order in which the link - * matcher is evaluated relative to others, from highest to lowest. The - * default value is 0. - */ - priority?: number; -} - -/** - * Handles actions generated by the parser. - */ -export interface IInputHandler { - addChar(char: string, code: number): void; - - /** C0 BEL */ bell(): void; - /** C0 LF */ lineFeed(): void; - /** C0 CR */ carriageReturn(): void; - /** C0 BS */ backspace(): void; - /** C0 HT */ tab(): void; - /** C0 SO */ shiftOut(): void; - /** C0 SI */ shiftIn(): void; - - /** CSI @ */ insertChars(params?: number[]): void; - /** CSI A */ cursorUp(params?: number[]): void; - /** CSI B */ cursorDown(params?: number[]): void; - /** CSI C */ cursorForward(params?: number[]): void; - /** CSI D */ cursorBackward(params?: number[]): void; - /** CSI E */ cursorNextLine(params?: number[]): void; - /** CSI F */ cursorPrecedingLine(params?: number[]): void; - /** CSI G */ cursorCharAbsolute(params?: number[]): void; - /** CSI H */ cursorPosition(params?: number[]): void; - /** CSI I */ cursorForwardTab(params?: number[]): void; - /** CSI J */ eraseInDisplay(params?: number[]): void; - /** CSI K */ eraseInLine(params?: number[]): void; - /** CSI L */ insertLines(params?: number[]): void; - /** CSI M */ deleteLines(params?: number[]): void; - /** CSI P */ deleteChars(params?: number[]): void; - /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[]): void; - /** CSI X */ eraseChars(params?: number[]): void; - /** CSI Z */ cursorBackwardTab(params?: number[]): void; - /** CSI ` */ charPosAbsolute(params?: number[]): void; - /** CSI a */ HPositionRelative(params?: number[]): void; - /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[]): void; - /** CSI d */ linePosAbsolute(params?: number[]): void; - /** CSI e */ VPositionRelative(params?: number[]): void; - /** CSI f */ HVPosition(params?: number[]): void; - /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[]): void; - /** CSI l */ resetMode(params?: number[]): void; - /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[]): void; - /** CSI p */ softReset(params?: number[]): void; - /** CSI q */ setCursorStyle(params?: number[]): void; - /** CSI r */ setScrollRegion(params?: number[]): void; - /** CSI s */ saveCursor(params?: number[]): void; - /** CSI u */ restoreCursor(params?: number[]): void; -} - -export interface ITheme { - foreground?: string; - background?: string; - cursor?: string; - cursorAccent?: string; - selection?: string; - black?: string; - red?: string; - green?: string; - yellow?: string; - blue?: string; - magenta?: string; - cyan?: string; - white?: string; - brightBlack?: string; - brightRed?: string; - brightGreen?: string; - brightYellow?: string; - brightBlue?: string; - brightMagenta?: string; - brightCyan?: string; - brightWhite?: string; -} - -export interface IDisposable { - dispose(): void; -} - -export interface ILinkMatcher { - id: number; - regex: RegExp; - handler: LinkMatcherHandler; - hoverTooltipCallback?: LinkMatcherHandler; - hoverLeaveCallback?: () => void; - matchIndex?: number; - validationCallback?: LinkMatcherValidationCallback; - priority?: number; -} - -export interface ICharset { - [key: string]: string; -} - -export interface ILinkHoverEvent { - x: number; - y: number; - length: number; -} diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 8b66dcaa5..14b1ac0f2 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -4,10 +4,9 @@ */ import { assert } from 'chai'; -import { ITerminal, ILinkifier, ILinkMatcher, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces'; +import { IMouseZoneManager, IMouseZone } from './input/Types'; +import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types'; import { Linkifier } from './Linkifier'; -import { LineData } from './Types'; -import { IMouseZoneManager, IMouseZone } from './input/Interfaces'; import { MockBuffer } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 767719301..da901a6ed 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { ILinkHoverEvent, ILinkMatcher, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes } from './Types'; -import { IMouseZoneManager } from './input/Interfaces'; +import { IMouseZoneManager } from './input/Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; import { MouseZone } from './input/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; @@ -82,12 +81,12 @@ export class Linkifier extends EventEmitter implements ILinkifier { } // Increase range to linkify - if (!this._rowsToLinkify.start) { + if (this._rowsToLinkify.start === null) { this._rowsToLinkify.start = start; this._rowsToLinkify.end = end; } else { - this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start; - this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end; + this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start); + this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end); } // Clear out any existing links on this row range diff --git a/src/Parser.ts b/src/Parser.ts index 6f4b0932c..3ac03e4fb 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,7 +5,7 @@ */ import { C0 } from './EscapeSequences'; -import { IInputHandler } from './Interfaces'; +import { IInputHandler } from './Types'; import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {}; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 97afcf2b7..5c53565fc 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -5,14 +5,13 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ITerminal, ICircularList, IBuffer } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; +import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; -import { LineData, CharData } from './Types'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 608ca4124..3d66c23db 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,14 +3,13 @@ * @license MIT */ +import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types'; import { MouseHelper } from './utils/MouseHelper'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import { ITerminal, ICircularList, ISelectionManager, IBuffer, IListenerType } from './Interfaces'; import { SelectionModel } from './SelectionModel'; -import { LineData, CharData } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; /** @@ -95,7 +94,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; - private _trimListener: IListenerType; + private _trimListener: XtermListener; constructor( private _terminal: ITerminal, diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index eda947188..ed483dbe1 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils.test'; diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index 5982b1e91..a9a3c89eb 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; /** * Represents a selection within the buffer. This model only cares about column diff --git a/src/Terminal.ts b/src/Terminal.ts index d293c256a..6d3f066a3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,6 +21,9 @@ * http://linux.die.net/man/7/urxvt */ +import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; +import { IMouseZoneManager } from './input/Types'; +import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; @@ -35,20 +38,16 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; -import { CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; -import { ITerminal, IBrowser, ICharset, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces'; import { BELL_SOUND } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; -import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; -import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; -import { IRenderer } from './renderer/Interfaces'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; +import { ITheme } from 'xterm'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -78,6 +77,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { enableBold: true, fontFamily: 'courier-new, courier, monospace', fontSize: 15, + fontWeight: 'normal', + fontWeightBold: 'bold', lineHeight: 1.0, letterSpacing: 0, scrollback: 1000, @@ -369,6 +370,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT value = 'block'; } break; + case 'fontWeight': + if (!value) { + value = 'normal'; + } + break; + case 'fontWeightBold': + if (!value) { + value = 'bold'; + } + break; case 'lineHeight': if (value < 1) { console.warn(`${key} cannot be less than 1, value: ${value}`); @@ -422,11 +433,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'enableBold': case 'letterSpacing': case 'lineHeight': + case 'fontWeight': + case 'fontWeightBold': + const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold'); + // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows, false); + this.renderer.onResize(this.cols, this.rows, didCharSizeChange); this.refresh(0, this.rows - 1); - // this.charMeasure.measure(this.options); case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); @@ -589,8 +603,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.document = this.parent.ownerDocument; this.body = this.document.body; - initializeCharAtlas(this.document); - this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio)); @@ -2186,13 +2198,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } private visualBell(): boolean { - return this.options.bellStyle === 'visual' || - this.options.bellStyle === 'both'; + return false; + // return this.options.bellStyle === 'visual' || + // this.options.bellStyle === 'both'; } private soundBell(): boolean { - return this.options.bellStyle === 'sound' || - this.options.bellStyle === 'both'; + return this.options.bellStyle === 'sound'; + // return this.options.bellStyle === 'sound' || + // this.options.bellStyle === 'both'; } private syncBellSound(): void { diff --git a/src/Types.ts b/src/Types.ts index 3263282e0..0b877930a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,16 +3,335 @@ * @license MIT */ -export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; -export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; +import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm'; +import { IColorSet, IRenderer } from './renderer/Types'; +import { IMouseZoneManager } from './input/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; +export type XtermListener = (...args: any[]) => void; + export type CharData = [number, string, number, number]; export type LineData = CharData[]; +export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; +export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; + export enum LinkHoverEventTypes { HOVER = 'linkhover', TOOLTIP = 'linktooltip', LEAVE = 'linkleave' } + +/** + * This interface encapsulates everything needed from the Terminal by the + * InputHandler. This cleanly separates the large amount of methods needed by + * InputHandler cleanly from the ITerminal interface. + */ +export interface IInputHandlingTerminal extends IEventEmitter { + element: HTMLElement; + options: ITerminalOptions; + cols: number; + rows: number; + charset: ICharset; + gcharset: number; + glevel: number; + charsets: ICharset[]; + applicationKeypad: boolean; + applicationCursor: boolean; + originMode: boolean; + insertMode: boolean; + wraparoundMode: boolean; + bracketedPasteMode: boolean; + defAttr: number; + curAttr: number; + prefix: string; + savedCols: number; + x10Mouse: boolean; + vt200Mouse: boolean; + normalMouse: boolean; + mouseEvents: boolean; + sendFocus: boolean; + utfMouse: boolean; + sgrMouse: boolean; + urxvtMouse: boolean; + cursorHidden: boolean; + + buffers: IBufferSet; + buffer: IBuffer; + viewport: IViewport; + selectionManager: ISelectionManager; + + bell(): void; + focus(): void; + convertEol: boolean; + updateRange(y: number): void; + scroll(isWrapped?: boolean): void; + setgLevel(g: number): void; + eraseAttr(): number; + eraseRight(x: number, y: number): void; + eraseLine(y: number): void; + eraseLeft(x: number, y: number): void; + blankLine(cur?: boolean, isWrapped?: boolean): LineData; + is(term: string): boolean; + send(data: string): void; + setgCharset(g: number, charset: ICharset): void; + resize(x: number, y: number): void; + log(text: string, data?: any): void; + 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; +} + +export interface IViewport { + syncScrollArea(): void; + onWheel(ev: WheelEvent): void; + onTouchStart(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): void; + onThemeChanged(colors: IColorSet): void; +} + +export interface ICompositionHelper { + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + +/** + * Handles actions generated by the parser. + */ +export interface IInputHandler { + addChar(char: string, code: number): void; + + /** C0 BEL */ bell(): void; + /** C0 LF */ lineFeed(): void; + /** C0 CR */ carriageReturn(): void; + /** C0 BS */ backspace(): void; + /** C0 HT */ tab(): void; + /** C0 SO */ shiftOut(): void; + /** C0 SI */ shiftIn(): void; + + /** CSI @ */ insertChars(params?: number[]): void; + /** CSI A */ cursorUp(params?: number[]): void; + /** CSI B */ cursorDown(params?: number[]): void; + /** CSI C */ cursorForward(params?: number[]): void; + /** CSI D */ cursorBackward(params?: number[]): void; + /** CSI E */ cursorNextLine(params?: number[]): void; + /** CSI F */ cursorPrecedingLine(params?: number[]): void; + /** CSI G */ cursorCharAbsolute(params?: number[]): void; + /** CSI H */ cursorPosition(params?: number[]): void; + /** CSI I */ cursorForwardTab(params?: number[]): void; + /** CSI J */ eraseInDisplay(params?: number[]): void; + /** CSI K */ eraseInLine(params?: number[]): void; + /** CSI L */ insertLines(params?: number[]): void; + /** CSI M */ deleteLines(params?: number[]): void; + /** CSI P */ deleteChars(params?: number[]): void; + /** CSI S */ scrollUp(params?: number[]): void; + /** CSI T */ scrollDown(params?: number[]): void; + /** CSI X */ eraseChars(params?: number[]): void; + /** CSI Z */ cursorBackwardTab(params?: number[]): void; + /** CSI ` */ charPosAbsolute(params?: number[]): void; + /** CSI a */ HPositionRelative(params?: number[]): void; + /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; + /** CSI c */ sendDeviceAttributes(params?: number[]): void; + /** CSI d */ linePosAbsolute(params?: number[]): void; + /** CSI e */ VPositionRelative(params?: number[]): void; + /** CSI f */ HVPosition(params?: number[]): void; + /** CSI g */ tabClear(params?: number[]): void; + /** CSI h */ setMode(params?: number[]): void; + /** CSI l */ resetMode(params?: number[]): void; + /** CSI m */ charAttributes(params?: number[]): void; + /** CSI n */ deviceStatus(params?: number[]): void; + /** CSI p */ softReset(params?: number[]): void; + /** CSI q */ setCursorStyle(params?: number[]): void; + /** CSI r */ setScrollRegion(params?: number[]): void; + /** CSI s */ saveCursor(params?: number[]): void; + /** CSI u */ restoreCursor(params?: number[]): void; +} + +export interface ILinkMatcher { + id: number; + regex: RegExp; + handler: LinkMatcherHandler; + hoverTooltipCallback?: LinkMatcherHandler; + hoverLeaveCallback?: () => void; + matchIndex?: number; + validationCallback?: LinkMatcherValidationCallback; + priority?: number; +} + +export interface ICharset { + [key: string]: string; +} + +export interface ILinkHoverEvent { + x: number; + y: number; + length: number; +} + +export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { + selectionManager: ISelectionManager; + charMeasure: ICharMeasure; + renderer: IRenderer; + browser: IBrowser; + writeBuffer: string[]; + cursorHidden: boolean; + cursorState: number; + defAttr: number; + options: ITerminalOptions; + buffer: IBuffer; + buffers: IBufferSet; + isFocused: boolean; + mouseHelper: IMouseHelper; + bracketedPasteMode: boolean; + + /** + * Emit the 'data' event and populate the given data. + * @param data The data to populate in the event. + */ + handler(data: string): void; + scrollLines(disp: number, suppressScrollEvent?: boolean): void; + scrollToRow(row: number): number; + cancel(ev: Event, force?: boolean): boolean | void; + log(text: string): void; + showCursor(): void; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; +} + +export interface IBufferAccessor { + buffer: IBuffer; +} + +export interface IElementAccessor { + element: HTMLElement; +} + +export interface ILinkifierAccessor { + linkifier: ILinkifier; +} + +export interface IMouseHelper { + getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; +} + +export interface ICharMeasure { + width: number; + height: number; + measure(options: ITerminalOptions): void; +} + +// TODO: The options that are not in the public API should be reviewed +export interface ITerminalOptions extends IPublicTerminalOptions { + cancelEvents?: boolean; + convertEol?: boolean; + debug?: boolean; + handler?: (data: string) => void; + screenKeys?: boolean; + termName?: string; + useFlowControl?: boolean; +} + +export interface IBuffer { + lines: ICircularList; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + savedY: number; + savedX: number; + isCursorInViewport: boolean; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + nextStop(x?: number): number; + prevStop(x?: number): number; +} + +export interface IBufferSet extends IEventEmitter { + alt: IBuffer; + normal: IBuffer; + active: IBuffer; + + activateNormalBuffer(): void; + activateAltBuffer(): void; +} + +export interface ICircularList extends IEventEmitter { + length: number; + maxLength: number; + forEach: (callbackfn: (value: T, index: number) => void) => void; + + get(index: number): T; + set(index: number, value: T): void; + push(value: T): void; + pop(): T; + splice(start: number, deleteCount: number, ...items: T[]): void; + trimStart(count: number): void; + shiftElements(start: number, count: number, offset: number): void; +} + +export interface ISelectionManager { + selectionText: string; + selectionStart: [number, number]; + selectionEnd: [number, number]; + + disable(): void; + enable(): void; + setSelection(row: number, col: number, length: number): void; +} + +export interface ILinkifier extends IEventEmitter { + attachToDom(mouseZoneManager: IMouseZoneManager): void; + linkifyRows(start: number, end: number): void; + setHypertextLinkHandler(handler: LinkMatcherHandler): void; + setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; + registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; + deregisterLinkMatcher(matcherId: number): boolean; +} + +export interface ILinkMatcherOptions { + /** + * The index of the link from the regex.match(text) call. This defaults to 0 + * (for regular expressions without capture groups). + */ + matchIndex?: number; + /** + * A callback that validates an individual link, returning true if valid and + * false if invalid. + */ + validationCallback?: LinkMatcherValidationCallback; + /** + * A callback that fires when the mouse hovers over a link. + */ + tooltipCallback?: LinkMatcherHandler; + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + leaveCallback?: () => void; + /** + * The priority of the link matcher, this defines the order in which the link + * matcher is evaluated relative to others, from highest to lowest. The + * default value is 0. + */ + priority?: number; +} + +export interface IBrowser { + isNode: boolean; + userAgent: string; + platform: string; + isFirefox: boolean; + isMSIE: boolean; + isMac: boolean; + isIpad: boolean; + isIphone: boolean; + isMSWindows: boolean; +} diff --git a/src/Viewport.ts b/src/Viewport.ts index fcac68ad4..87ecd69ec 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { ITerminal, IViewport } from './Interfaces'; +import { IColorSet } from './renderer/Types'; +import { ITerminal, IViewport } from './Types'; import { CharMeasure } from './utils/CharMeasure'; -import { IColorSet } from './renderer/Interfaces'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. diff --git a/src/addons/attach/Intefaces.ts b/src/addons/attach/Interfaces.ts similarity index 100% rename from src/addons/attach/Intefaces.ts rename to src/addons/attach/Interfaces.ts diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index d963a658f..cbfd4a248 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -8,7 +8,7 @@ /// import { Terminal } from 'xterm'; -import { IAttachAddonTerminal } from './Intefaces'; +import { IAttachAddonTerminal } from './Interfaces'; /** * Attaches the given terminal to the given socket. diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index dd941682a..2abc3b38a 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -113,8 +113,23 @@ export class SearchHelper implements ISearchHelper { private _findInLine(term: string, y: number): ISearchResult { const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); - const searchIndex = lowerStringLine.indexOf(lowerTerm); + let searchIndex = lowerStringLine.indexOf(lowerTerm); if (searchIndex >= 0) { + const line = this._terminal.buffer.lines.get(y); + for (let i = 0; i < searchIndex; i++) { + const charData = line[i]; + // Adjust the searchIndex to normalize emoji into single chars + const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; + if (char.length > 1) { + searchIndex -= char.length - 1; + } + // Adjust the searchIndex for empty characters following wide unicode + // chars (eg. CJK) + const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; + if (charWidth === 0) { + searchIndex++; + } + } return { term, col: searchIndex, diff --git a/src/addons/terminado/Intefaces.ts b/src/addons/terminado/Interfaces.ts similarity index 100% rename from src/addons/terminado/Intefaces.ts rename to src/addons/terminado/Interfaces.ts diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index d5cf72d1b..4acf9dd84 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -9,7 +9,7 @@ /// import { Terminal } from 'xterm'; -import { ITerminadoAddonTerminal } from './Intefaces'; +import { ITerminadoAddonTerminal } from './Interfaces'; /** * Attaches the given terminal to the given socket. diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index cf6661fe2..fd2b3d779 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager } from '../Interfaces'; +import { ITerminal, ISelectionManager } from '../Types'; interface IWindow extends Window { clipboardData?: { diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 6fbe1c673..98377f36c 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IMouseZoneManager, IMouseZone } from './Interfaces'; -import { ITerminal } from '../Interfaces'; +import { ITerminal } from '../Types'; +import { IMouseZoneManager, IMouseZone } from './Types'; const HOVER_DURATION = 500; diff --git a/src/input/Interfaces.ts b/src/input/Types.ts similarity index 100% rename from src/input/Interfaces.ts rename to src/input/Types.ts diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index b4becbce0..20a41cfa3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -3,10 +3,9 @@ * @license MIT */ -import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces'; -import { ITerminal, ITerminalOptions } from '../Interfaces'; +import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; +import { CharData, ITerminal, ITerminalOptions } from '../Types'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; -import { CharData } from '../Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; export const INVERTED_DEFAULT_COLOR = -1; @@ -201,7 +200,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 = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + this._ctx.font = this._getFont(terminal, false); this._ctx.textBaseline = 'top'; this._clipRow(terminal, y); this._ctx.fillText( @@ -269,7 +268,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { charAtlasCellWidth, this._scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim); + this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim); } // This draws the atlas (for debugging purposes) // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -289,10 +288,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void { this._ctx.save(); - this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; - if (bold && terminal.options.enableBold) { - this._ctx.font = `bold ${this._ctx.font}`; - } + this._ctx.font = this._getFont(terminal, bold); this._ctx.textBaseline = 'top'; if (fg === INVERTED_DEFAULT_COLOR) { @@ -332,5 +328,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._scaledCellHeight); this._ctx.clip(); } + + /** + * Gets the current font. + * @param terminal The terminal. + * @param isBold If we should use the bold fontWeight. + */ + protected _getFont(terminal: ITerminal, isBold: boolean): string { + const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; + + return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + } } diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 9ac1e1618..05cf72014 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -3,15 +3,18 @@ * @license MIT */ -import { ITerminal, ITheme } from '../Interfaces'; -import { IColorSet } from '../renderer/Interfaces'; -import { isFirefox } from '../utils/Browser'; +import { ITerminal } from '../Types'; +import { IColorSet } from './Types'; +import { isFirefox } from '../shared/utils/Browser'; +import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; export const CHAR_ATLAS_CELL_SPACING = 1; interface ICharAtlasConfig { fontSize: number; fontFamily: string; + fontWeight: string; + fontWeightBold: string; scaledCharWidth: number; scaledCharHeight: number; colors: IColorSet; @@ -63,8 +66,28 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC } } + const canvasFactory = (width: number, height: number) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + const charAtlasConfig: ICharAtlasRequest = { + scaledCharWidth, + scaledCharHeight, + fontSize: terminal.options.fontSize, + fontFamily: terminal.options.fontFamily, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + background: colors.background, + foreground: colors.foreground, + ansiColors: colors.ansi, + devicePixelRatio: window.devicePixelRatio + }; + const newEntry: ICharAtlasCacheEntry = { - bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi), + bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), config: newConfig, ownedBy: [terminal] }; @@ -86,6 +109,8 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi scaledCharHeight, fontFamily: terminal.options.fontFamily, fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, colors: clonedColors }; } @@ -98,125 +123,10 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { } return a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && + a.fontWeight === b.fontWeight && + a.fontWeightBold === b.fontWeightBold && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } - -let generator: CharAtlasGenerator; - -/** - * Initializes the char atlas generator. - * @param document The document. - */ -export function initialize(document: Document): void { - if (!generator) { - generator = new CharAtlasGenerator(document); - } -} - -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor(private _document: Document) { - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d', {alpha: false}); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { - const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; - this._canvas.width = 255 * cellWidth; - this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; - - this._ctx.fillStyle = background; - this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); - - this._ctx.save(); - this._ctx.fillStyle = foreground; - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); - this._ctx.restore(); - } - // Default color bold - this._ctx.save(); - this._ctx.font = `bold ${this._ctx.font}`; - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); - this._ctx.restore(); - } - this._ctx.restore(); - - // Colors 0-15 - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = `bold ${this._ctx.font}`; - } - const y = (colorIndex + 2) * cellHeight; - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillStyle = ansiColors[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); - this._ctx.restore(); - } - } - this._ctx.restore(); - - // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. Firefox is also - // included here as ImageBitmap appears both buggy and has horrible - // performance (tested on v55). - if (!('createImageBitmap' in window) || isFirefox) { - // Regenerate canvas and context as they are now owned by the char atlas - const result = this._canvas; - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - return result; - } - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - - // Remove the background color from the image so characters may overlap - const r = parseInt(background.substr(1, 2), 16); - const g = parseInt(background.substr(3, 2), 16); - const b = parseInt(background.substr(5, 2), 16); - this._clearColor(charAtlasImageData, r, g, b); - - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } - - private _clearColor(imageData: ImageData, r: number, g: number, b: number): void { - 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; - } - } - } -} diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 6c0d29454..b45c86469 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IColorSet, IColorManager } from './Interfaces'; -import { ITheme } from '../Interfaces'; +import { IColorSet, IColorManager } from './Types'; +import { ITheme } from 'xterm'; const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 6ac489fcc..d430b81d6 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,13 +3,11 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CharData } from '../Types'; +import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; interface ICursorState { x: number; diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts deleted file mode 100644 index 74f80a580..000000000 --- a/src/renderer/Interfaces.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces'; - -export interface IRenderer extends IEventEmitter { - dimensions: IRenderDimensions; - colorManager: IColorManager; - - setTheme(theme: ITheme): IColorSet; - onWindowResize(devicePixelRatio: number): void; - onResize(cols: number, rows: number, didCharSizeChange: boolean): void; - onCharSizeChanged(): void; - onBlur(): void; - onFocus(): void; - onSelectionChanged(start: [number, number], end: [number, number]): void; - onCursorMove(): void; - onOptionsChanged(): void; - clear(): void; - refreshRows(start: number, end: number): void; -} - -export interface IRenderLayer { - /** - * Called when the terminal loses focus. - */ - onBlur(terminal: ITerminal): void; - - /** - * * Called when the terminal gets focus. - */ - onFocus(terminal: ITerminal): void; - - /** - * Called when the cursor is moved. - */ - onCursorMove(terminal: ITerminal): void; - - /** - * Called when options change. - */ - onOptionsChanged(terminal: ITerminal): void; - - /** - * Called when the theme changes. - */ - onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; - - /** - * Called when the data in the grid has changed (or needs to be rendered - * again). - */ - onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; - - /** - * Calls when the selection changes. - */ - onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; - - /** - * Resize the render layer. - */ - resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; - - /** - * Clear the state of the render layer. - */ - reset(terminal: ITerminal): void; -} - -export interface IColorManager { - colors: IColorSet; -} - -export interface IColorSet { - foreground: string; - background: string; - cursor: string; - cursorAccent: string; - selection: string; - ansi: string[]; -} - -export interface IRenderDimensions { - scaledCharWidth: number; - scaledCharHeight: number; - scaledCellWidth: number; - scaledCellHeight: number; - scaledCharLeft: number; - scaledCharTop: number; - scaledCanvasWidth: number; - scaledCanvasHeight: number; - canvasWidth: number; - canvasHeight: number; - actualCellWidth: number; - actualCellHeight: number; -} diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 87fbddbdf..61352f154 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,13 +3,11 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ILinkHoverEvent, ITerminal, ILinkifierAccessor } from '../Interfaces'; +import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; -import { LinkHoverEventTypes } from '../Types'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkHoverEvent = null; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 8798f334f..80bc9f44a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -3,18 +3,19 @@ * @license MIT */ -import { ITerminal, ITheme } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces'; +import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types'; +import { ITerminal } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { EventEmitter } from '../EventEmitter'; import { RenderDebouncer } from '../utils/RenderDebouncer'; import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; +import { ITheme } from 'xterm'; export class Renderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 9740bb9a6..53fc9b39d 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Types'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; export class SelectionRenderLayer extends BaseRenderLayer { diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 99a6feae9..3c01cea3b 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,12 +3,10 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; +import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types'; import { GridCache } from './GridCache'; -import { CharData } from '../Types'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; /** @@ -33,7 +31,7 @@ export class TextRenderLayer extends BaseRenderLayer { super.resize(terminal, dim, charSizeChanged); // Clear the character width cache if the font or width has changed - const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + const terminalFont = this._getFont(terminal, false); if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { this._characterWidth = dim.scaledCharWidth; this._characterFont = terminalFont; @@ -166,7 +164,7 @@ export class TextRenderLayer extends BaseRenderLayer { this._ctx.save(); if (flags & FLAGS.BOLD) { - this._ctx.font = `bold ${this._ctx.font}`; + this._ctx.font = this._getFont(terminal, true); // Convert the FG color to the bold variant if (fg < 8) { fg += 8; diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 834f88135..6f6e5f0ee 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -3,7 +3,10 @@ * @license MIT */ - /** +import { ITerminal } from '../Types'; +import { IEventEmitter, ITheme } from 'xterm'; + +/** * Flags used to render terminal text properly. */ export enum FLAGS { @@ -14,3 +17,96 @@ export enum FLAGS { INVISIBLE = 16, DIM = 32 } + +export interface IRenderer extends IEventEmitter { + dimensions: IRenderDimensions; + colorManager: IColorManager; + + setTheme(theme: ITheme): IColorSet; + onWindowResize(devicePixelRatio: number): void; + onResize(cols: number, rows: number, didCharSizeChange: boolean): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number], end: [number, number]): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + refreshRows(start: number, end: number): void; +} + +export interface IColorManager { + colors: IColorSet; +} + +export interface IColorSet { + foreground: string; + background: string; + cursor: string; + cursorAccent: string; + selection: string; + ansi: string[]; +} + +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +export interface IRenderLayer { + /** + * Called when the terminal loses focus. + */ + onBlur(terminal: ITerminal): void; + + /** + * * Called when the terminal gets focus. + */ + onFocus(terminal: ITerminal): void; + + /** + * Called when the cursor is moved. + */ + onCursorMove(terminal: ITerminal): void; + + /** + * Called when options change. + */ + onOptionsChanged(terminal: ITerminal): void; + + /** + * Called when the theme changes. + */ + onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; + + /** + * Called when the data in the grid has changed (or needs to be rendered + * again). + */ + onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; + + /** + * Calls when the selection changes. + */ + onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; + + /** + * Resize the render layer. + */ + resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; + + /** + * Clear the state of the render layer. + */ + reset(terminal: ITerminal): void; +} diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/CharAtlasGenerator.ts new file mode 100644 index 000000000..9ae9f4b31 --- /dev/null +++ b/src/shared/CharAtlasGenerator.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { isFirefox } from './utils/Browser'; + +declare const Promise: any; + +export interface IOffscreenCanvas { + width: number; + height: number; + getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D; + transferToImageBitmap(): ImageBitmap; +} + +export interface ICharAtlasRequest { + scaledCharWidth: number; + scaledCharHeight: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + background: string; + foreground: string; + ansiColors: string[]; + devicePixelRatio: number; +} + +export const CHAR_ATLAS_CELL_SPACING = 1; + +/** + * 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. + */ +export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise { + const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const canvas = canvasFactory( + /*255 ascii chars*/255 * cellWidth, + (/*default+default bold*/2 + /*0-15*/16) * cellHeight + ); + const ctx = canvas.getContext('2d', {alpha: false}); + + ctx.fillStyle = request.background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.fillStyle = request.foreground; + ctx.font = getFont(request.fontWeight, request); + ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.restore(); + } + // Default color bold + ctx.save(); + ctx.font = getFont(request.fontWeightBold, request); + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.restore(); + } + ctx.restore(); + + // Colors 0-15 + ctx.font = getFont(request.fontWeight, request); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + ctx.font = getFont(request.fontWeightBold, request); + } + const y = (colorIndex + 2) * 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 = request.ansiColors[colorIndex]; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.restore(); + } + } + ctx.restore(); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in context) || isFirefox) { + // Don't attempt to clear background colors if createImageBitmap is not supported + 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())); + } + } + + const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Remove the background color from the image so characters may overlap + const r = parseInt(request.background.substr(1, 2), 16); + const g = parseInt(request.background.substr(3, 2), 16); + const b = parseInt(request.background.substr(5, 2), 16); + clearColor(charAtlasImageData, r, g, b); + + return context.createImageBitmap(charAtlasImageData); +} + +/** + * Makes a partiicular rgb color in an ImageData completely transparent. + */ +function clearColor(imageData: ImageData, r: number, g: number, b: number): void { + 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; + } + } +} + +function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string { + return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; +} diff --git a/src/utils/Browser.ts b/src/shared/utils/Browser.ts similarity index 76% rename from src/utils/Browser.ts rename to src/shared/utils/Browser.ts index 48c0c3748..be71d875e 100644 --- a/src/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -3,8 +3,6 @@ * @license MIT */ -import { contains } from './Generic'; - const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; @@ -20,3 +18,12 @@ export const isIpad = platform === 'iPad'; export const isIphone = platform === 'iPhone'; export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); export const isLinux = platform.indexOf('Linux') >= 0; + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +function contains(arr: any[], el: any): boolean { + return arr.indexOf(el) >= 0; +} diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index f50ce96b7..e4f4e1d6b 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -4,8 +4,8 @@ */ import jsdom = require('jsdom'); +import { ICharMeasure, ITerminal } from '../Types'; import { assert } from 'chai'; -import { ICharMeasure, ITerminal } from '../Interfaces'; import { CharMeasure } from './CharMeasure'; describe('CharMeasure', () => { diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 62291ab23..91cfce5ff 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -3,8 +3,8 @@ * @license MIT */ +import { ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; import { EventEmitter } from '../EventEmitter'; -import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; /** * Utility class that measures the size of a character. Measurements are done in diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 97a32b790..6b74971b6 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -4,7 +4,7 @@ */ import { EventEmitter } from '../EventEmitter'; -import { ICircularList } from '../Interfaces'; +import { ICircularList } from '../Types'; /** * Represents a circular list; a list with a maximum size that wraps around when push is called, diff --git a/src/utils/Dom.ts b/src/utils/Dom.ts index b6c81e1f9..889c88c2f 100644 --- a/src/utils/Dom.ts +++ b/src/utils/Dom.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable } from '../Interfaces'; +import { IDisposable } from 'xterm'; /** * Adds a disposabe listener to a node in the DOM, returning the disposable. diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts deleted file mode 100644 index 4bc6c487f..000000000 --- a/src/utils/Generic.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -/** - * Return if the given array contains the given element - * @param {Array} array The array to search for the given element. - * @param {Object} el The element to look for into the array - */ -export function contains(arr: any[], el: any): boolean { - return arr.indexOf(el) >= 0; -} diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts index d7d8f6981..f62593eb3 100644 --- a/src/utils/MouseHelper.ts +++ b/src/utils/MouseHelper.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { ICharMeasure } from '../Interfaces'; -import { IRenderer } from '../renderer/Interfaces'; +import { ICharMeasure } from '../Types'; +import { IRenderer } from '../renderer/Types'; export class MouseHelper { constructor(private _renderer: IRenderer) {} @@ -24,7 +24,7 @@ export class MouseHelper { while (element) { x -= element.offsetLeft; y -= element.offsetTop; - element = 'offsetParent' in element ? element.offsetParent : element.parentElement; + element = element.offsetParent; } element = originalElement; while (element && element !== element.ownerDocument.body) { diff --git a/src/utils/RenderDebouncer.ts b/src/utils/RenderDebouncer.ts index 0d07b1a1e..ab88fb5f0 100644 --- a/src/utils/RenderDebouncer.ts +++ b/src/utils/RenderDebouncer.ts @@ -1,4 +1,5 @@ -import { ITerminal, IDisposable } from '../Interfaces'; +import { ITerminal } from '../Types'; +import { IDisposable } from 'xterm'; /** * Debounces calls to render terminal rows using animation frames. diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 69adf3a20..91dabc26d 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -3,13 +3,76 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper, IDisposable } from '../Interfaces'; -import { LineData } from '../Types'; +import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; +import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types'; import { Buffer } from '../Buffer'; -import * as Browser from './Browser'; -import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; +import * as Browser from '../shared/utils/Browser'; +import { ITheme, IDisposable } from 'xterm'; export class MockTerminal implements ITerminal { + enterNavigationMode(): void { + throw new Error('Method not implemented.'); + } + getOption(key: any): any { + throw new Error('Method not implemented.'); + } + setOption(key: any, value: any): void { + throw new Error('Method not implemented.'); + } + blur(): void { + throw new Error('Method not implemented.'); + } + focus(): void { + throw new Error('Method not implemented.'); + } + resize(columns: number, rows: number): void { + throw new Error('Method not implemented.'); + } + writeln(data: string): void { + throw new Error('Method not implemented.'); + } + open(parent: HTMLElement): void { + throw new Error('Method not implemented.'); + } + attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { + throw new Error('Method not implemented.'); + } + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { + throw new Error('Method not implemented.'); + } + deregisterLinkMatcher(matcherId: number): void { + throw new Error('Method not implemented.'); + } + hasSelection(): boolean { + throw new Error('Method not implemented.'); + } + getSelection(): string { + throw new Error('Method not implemented.'); + } + clearSelection(): void { + throw new Error('Method not implemented.'); + } + selectAll(): void { + throw new Error('Method not implemented.'); + } + destroy(): void { + throw new Error('Method not implemented.'); + } + scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + scrollToTop(): void { + throw new Error('Method not implemented.'); + } + scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + clear(): void { + throw new Error('Method not implemented.'); + } + write(data: string): void { + throw new Error('Method not implemented.'); + } bracketedPasteMode: boolean; mouseHelper: IMouseHelper; renderer: IRenderer; @@ -39,10 +102,10 @@ export class MockTerminal implements ITerminal { on(event: string, callback: () => void): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } - addDisposableListener(type: string, handler: IListenerType): IDisposable { + addDisposableListener(type: string, handler: XtermListener): IDisposable { throw new Error('Method not implemented.'); } scrollLines(disp: number, suppressScrollEvent: boolean): void { @@ -190,16 +253,16 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { setOption(key: string, value: any): void { this.options[key] = value; } - on(type: string, listener: IListenerType): void { + on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } emit(type: string, data?: any): void { throw new Error('Method not implemented.'); } - addDisposableListener(type: string, handler: IListenerType): IDisposable { + addDisposableListener(type: string, handler: XtermListener): IDisposable { throw new Error('Method not implemented.'); } } @@ -229,16 +292,16 @@ export class MockBuffer implements IBuffer { export class MockRenderer implements IRenderer { colorManager: IColorManager; - on(type: string, listener: IListenerType): void { + on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } emit(type: string, data?: any): void { throw new Error('Method not implemented.'); } - addDisposableListener(type: string, handler: IListenerType): IDisposable { + addDisposableListener(type: string, handler: XtermListener): IDisposable { throw new Error('Method not implemented.'); } dimensions: IRenderDimensions; diff --git a/tsconfig.json b/tsconfig.json index a3aa790cf..38bfd2faf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,7 @@ "rootDir": "src", "outDir": "lib", "sourceMap": true, - "removeComments": true, - "declaration": true + "removeComments": true }, "include": [ "src/**/*" diff --git a/tslint.json b/tslint.json index b5367f6a0..f6ad759a7 100644 --- a/tslint.json +++ b/tslint.json @@ -24,6 +24,7 @@ "parameter" ], "eofline": true, + "no-duplicate-imports": true, "no-eval": true, "no-internal-module": true, "no-trailing-whitespace": true, diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b9796328a..ce4684e51 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -8,6 +8,11 @@ */ declare module 'xterm' { + /** + * A string representing text font weight. + */ + export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + /** * An object containing start up options for the terminal. */ @@ -57,6 +62,16 @@ declare module 'xterm' { */ fontFamily?: string; + /** + * The font weight used to render non-bold text. + */ + fontWeight?: FontWeight; + + /** + * The font weight used to render bold text. + */ + fontWeightBold?: FontWeight; + /** * The spacing in whole pixels between characters.. */ @@ -67,6 +82,11 @@ declare module 'xterm' { */ lineHeight?: number; + /** + * Whether to treat option as the meta key. + */ + macOptionIsMeta?: boolean; + /** * The number of rows in the terminal. */ @@ -179,10 +199,24 @@ declare module 'xterm' { priority?: number; } + 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; + } + + /** + * An object that can be disposed via a dipose function. + */ + export interface IDisposable { + dispose(): void; + } + /** * The class that represents an xterm.js terminal. */ - export class Terminal { + export class Terminal implements IEventEmitter { /** * The element containing the terminal. */ @@ -231,7 +265,7 @@ declare module 'xterm' { * @param type The type of the event. * @param listener The listener. */ - on(type: 'data', listener: (data?: string) => void): void; + on(type: 'data', listener: (...args: any[]) => void): void; /** * Registers an event listener. * @param type The type of the event. @@ -282,6 +316,10 @@ declare module 'xterm' { */ off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; + emit(type: string, data?: any): void; + + addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; + /** * Resizes the terminal. * @param x The number of columns to resize to. @@ -359,22 +397,6 @@ declare module 'xterm' { */ selectAll(): void; - // /** - // * Find the next instance of the term, then scroll to and select it. If it - // * doesn't exist, do nothing. - // * @param term Tne search term. - // * @return Whether a result was found. - // */ - // findNext(term: string): boolean; - - // /** - // * Find the previous instance of the term, then scroll to and select it. If it - // * doesn't exist, do nothing. - // * @param term Tne search term. - // * @return Whether a result was found. - // */ - // findPrevious(term: string): boolean; - /** * Destroys the terminal and detaches it from the DOM. */ @@ -417,12 +439,12 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; + getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -450,6 +472,12 @@ declare module 'xterm' { * @param value The option value. */ setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void; /** * Sets an option on the terminal. * @param key The option key.