Merge branch 'master' into 426_alt_scroll

This commit is contained in:
Daniel Imms
2018-03-08 05:37:16 -08:00
committed by GitHub
36 changed files with 346 additions and 232 deletions
+2
View File
@@ -32,6 +32,7 @@ Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
@@ -74,6 +75,7 @@ Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
+1 -1
View File
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run test
RUN npm run build && npm run webpack && npm run test
+13 -1
View File
@@ -144,7 +144,19 @@ Then open your project's [Public URL](https://help.sourcelair.com/projects/the-p
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
To run the just demo (with no editing access). run the following command in your terminal:
```
docker run -p 3000:3000 xtermjs/xterm.js
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
+3
View File
@@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
@@ -121,6 +123,7 @@ function createTerminal() {
term.open(terminalContainer);
term.winptyCompatInit();
term.webLinksInit();
term.fit();
term.focus();
+1
View File
@@ -2,6 +2,7 @@ version: "3"
services:
web:
image: xtermjs/xterm.js:latest
build: .
volumes:
- ./:/usr/src/app
+1 -1
View File
@@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
const addons = fs.readdirSync(`${__dirname}/src/addons`);
// Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.1.0",
"version": "3.2.0",
"ignore": [
"demo",
"test",
+3 -9
View File
@@ -101,12 +101,6 @@ describe('Linkifier', () => {
linkifier.attachToDom(mouseZoneManager);
});
describe('http links', () => {
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => {
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
@@ -200,19 +194,19 @@ describe('Linkifier', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
});
});
+3 -47
View File
@@ -8,30 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidatio
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
/**
* The ID of the built in http(s) link matcher.
*/
const HYPERTEXT_LINK_MATCHER_ID = 0;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
@@ -47,7 +23,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
constructor(
@@ -58,7 +34,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
start: null,
end: null
};
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
@@ -111,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._rowsToLinkify.end = null;
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior for
* tandard http(s) links.
* @param handler The handler to use, this can be cleared with null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
}
/**
* Attaches a validation callback for hypertext links.
* @param callback The callback to use, this can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -139,7 +97,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
if (!handler) {
throw new Error('handler must be defined');
}
const matcher: ILinkMatcher = {
@@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
@@ -222,7 +179,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
// Find the first match
let match = text.match(matcher.regex);
+5
View File
@@ -634,6 +634,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
}
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
+8 -45
View File
@@ -472,11 +472,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
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, didCharSizeChange);
this.renderer.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1);
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
@@ -702,14 +700,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('resize', () => this.renderer.onResize(this.cols, this.rows));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.charMeasure);
@@ -1346,36 +1344,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.customKeyEventHandler = customKeyEventHandler;
}
/**
* Attaches a http(s) link handler, forcing web links to behave differently to
* regular <a> tags. This will trigger a refresh as links potentially need to be
* reconstructed. Calling this with null will remove the handler.
* @param handler The handler callback function.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
}
this.linkifier.setHypertextLinkHandler(handler);
// Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/**
* Attaches a validation callback for hypertext links. This is useful to use
* validation logic or to do something with the link's element and url.
* @param callback The callback to use, this can
* be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
}
this.linkifier.setHypertextValidationCallback(callback);
// // Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -1387,12 +1355,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number {
if (this.linkifier) {
const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
this.refresh(0, this.rows - 1);
return matcherId;
}
return 0;
const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
this.refresh(0, this.rows - 1);
return matcherId;
}
/**
@@ -1400,10 +1365,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param matcherId The link matcher's ID (returned after register)
*/
public deregisterLinkMatcher(matcherId: number): void {
if (this.linkifier) {
if (this.linkifier.deregisterLinkMatcher(matcherId)) {
this.refresh(0, this.rows - 1);
}
if (this.linkifier.deregisterLinkMatcher(matcherId)) {
this.refresh(0, this.rows - 1);
}
}
+2 -3
View File
@@ -14,7 +14,7 @@ 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 LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
@@ -253,6 +253,7 @@ export interface IBuffer {
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
@@ -299,8 +300,6 @@ export interface ISelectionManager {
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;
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.weblinks",
"main": "weblinks.js",
"private": true
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as webLinks from './webLinks';
class MockTerminal {
public regex: RegExp;
public handler: (event: MouseEvent, uri: string) => void;
public options?: any;
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
this.regex = regex;
this.handler = handler;
this.options = options;
return 0;
}
}
describe('webLinks addon', () => {
describe('apply', () => {
it('should do register the `webLinksInit` method', () => {
webLinks.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
let match = row.match(term.regex);
let uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
});
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal, ILinkMatcherOptions } from 'xterm';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
window.open(uri, '_blank');
}
/**
* Initialize the web links addon, registering the link matcher.
* @param term The terminal to use web links within.
* @param handler A custom handler to use.
* @param options Custom options to use, matchIndex will always be ignored.
*/
export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
webLinksInit(this, handler, options);
};
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "xterm.winptyCompat",
"name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
+12 -3
View File
@@ -56,9 +56,13 @@ export class AltClickHandler {
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
// The alt buffer should try to navigate between rows
if (!this._terminal.buffer.hasScrollback) {
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
}
// Only move horizontally for the normal buffer
return this._moveHorizontallyOnly();
}
/**
@@ -113,6 +117,11 @@ export class AltClickHandler {
).length, this._sequence(direction));
}
private _moveHorizontallyOnly(): string {
let direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
/**
* Utility functions
*/
+12 -6
View File
@@ -8,13 +8,19 @@ import * as Terminal from '../Terminal';
import * as Clipboard from './Clipboard';
describe('evaluatePastedTextProcessing', () => {
it('should replace carriage return + line feed with line feed on windows', () => {
const pastedText = 'foo\r\nbar\r\n';
const processedText = Clipboard.prepareTextForTerminal(pastedText, false);
const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
it('should replace carriage return and/or line feed with carriage return', () => {
const pastedText = {
unix: 'foo\nbar\n',
windows: 'foo\r\nbar\r\n'
};
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\rbar\r');
const processedText = {
unix: Clipboard.prepareTextForTerminal(pastedText.unix),
windows: Clipboard.prepareTextForTerminal(pastedText.windows)
};
assert.equal(processedText.unix, 'foo\rbar\r');
assert.equal(processedText.windows, 'foo\rbar\r');
});
it('should bracket pasted text in bracketedPasteMode', () => {
const pastedText = 'foo bar';
+3 -6
View File
@@ -18,11 +18,8 @@ declare var window: IWindow;
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
if (isMSWindows) {
return text.replace(/\r?\n/g, '\r');
}
return text;
export function prepareTextForTerminal(text: string): string {
return text.replace(/\r?\n/g, '\r');
}
/**
@@ -62,7 +59,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
let text: string;
let dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, term.bracketedPasteMode);
term.handler(text);
term.textarea.value = '';
+5 -8
View File
@@ -5,12 +5,11 @@
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal, ITerminalOptions } from '../Types';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types';
import { acquireCharAtlas } from './atlas/CharAtlas';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1;
const DIM_OPACITY = 0.5;
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx: CanvasRenderingContext2D;
@@ -93,7 +92,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
@@ -110,9 +109,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this.clearAll();
}
if (charSizeChanged) {
this._refreshCharAtlas(terminal, this._colors);
}
this._refreshCharAtlas(terminal, this._colors);
}
public abstract reset(terminal: ITerminal): void;
+8 -1
View File
@@ -3,14 +3,21 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ColorManager } from './ColorManager';
describe('ColorManager', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: Window;
beforeEach(() => {
cm = new ColorManager();
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
cm = new ColorManager(document);
});
describe('constructor', () => {
+52 -22
View File
@@ -67,8 +67,10 @@ function toPaddedHex(c: number): string {
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _document: Document;
constructor() {
constructor(document: Document) {
this._document = document;
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
@@ -85,26 +87,54 @@ export class ColorManager implements IColorManager {
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = theme.background || DEFAULT_BACKGROUND;
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2];
this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3];
this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4];
this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5];
this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6];
this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7];
this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8];
this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9];
this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10];
this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11];
this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12];
this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13];
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
this.colors.foreground = this._validateColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._validateColor(theme.cursor, DEFAULT_CURSOR);
this.colors.cursorAccent = this._validateColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
this.colors.selection = this._validateColor(theme.selection, DEFAULT_SELECTION);
this.colors.ansi[0] = this._validateColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._validateColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._validateColor(theme.green, DEFAULT_ANSI_COLORS[2]);
this.colors.ansi[3] = this._validateColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
this.colors.ansi[4] = this._validateColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
this.colors.ansi[5] = this._validateColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
this.colors.ansi[6] = this._validateColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
this.colors.ansi[7] = this._validateColor(theme.white, DEFAULT_ANSI_COLORS[7]);
this.colors.ansi[8] = this._validateColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
this.colors.ansi[9] = this._validateColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
this.colors.ansi[10] = this._validateColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
this.colors.ansi[11] = this._validateColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
this.colors.ansi[12] = this._validateColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
this.colors.ansi[13] = this._validateColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._validateColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._validateColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
}
private _validateColor(color: string, fallback: string): string {
if (!color) {
return fallback;
}
const isColorValid = this._isColorValid(color);
if (!isColorValid) {
console.warn(`Color: ${color} is invalid using fallback ${fallback}`);
}
return isColorValid ? color : fallback;
}
private _isColorValid(color: string): boolean {
const litmus = 'red';
const d = this._document.createElement('div');
d.style.color = litmus;
d.style.color = color;
// Element's style.color will be reverted to litmus or set to '' if an invalid color is given
if (color !== litmus && (d.style.color === litmus || d.style.color === '')) {
return false;
}
return true;
}
}
+2 -2
View File
@@ -45,8 +45,8 @@ export class CursorRenderLayer extends BaseRenderLayer {
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: null,
+4 -3
View File
@@ -7,7 +7,8 @@ import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure,
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null;
@@ -18,8 +19,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = null;
}
+5 -5
View File
@@ -31,7 +31,7 @@ export class Renderer extends EventEmitter implements IRenderer {
constructor(private _terminal: ITerminal, theme: ITheme) {
super();
this.colorManager = new ColorManager();
this.colorManager = new ColorManager(document);
if (theme) {
this.colorManager.setTheme(theme);
}
@@ -84,7 +84,7 @@ export class Renderer extends EventEmitter implements IRenderer {
// and the terminal needs to refreshed
if (this._devicePixelRatio !== devicePixelRatio) {
this._devicePixelRatio = devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
}
@@ -106,12 +106,12 @@ export class Renderer extends EventEmitter implements IRenderer {
return this.colorManager.colors;
}
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
// Force a refresh
if (this._isPaused) {
@@ -131,7 +131,7 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
public onBlur(): void {
+2 -2
View File
@@ -20,8 +20,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
};
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
+4 -3
View File
@@ -6,8 +6,9 @@
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { BaseRenderLayer } from './BaseRenderLayer';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -27,8 +28,8 @@ export class TextRenderLayer extends BaseRenderLayer {
this._state = new GridCache<CharData>();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(terminal, false);
+2 -2
View File
@@ -24,7 +24,7 @@ export interface IRenderer extends IEventEmitter {
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
@@ -103,7 +103,7 @@ export interface IRenderLayer {
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
resize(terminal: ITerminal, dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
@@ -3,23 +3,12 @@
* @license MIT
*/
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;
allowTransparency: boolean;
colors: IColorSet;
}
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from './Types';
import { isFirefox } from '../../shared/utils/Browser';
import { generateCharAtlas, ICharAtlasRequest } from '../../shared/atlas/CharAtlasGenerator';
import { generateConfig, configEquals } from './CharAtlasUtils';
interface ICharAtlasCacheEntry {
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
@@ -96,41 +85,3 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
charAtlasCache.push(newEntry);
return newEntry.bitmap;
}
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../../Types';
import { ITheme } from 'xterm';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from './Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { IColorSet } from '../Types';
export const INVERTED_DEFAULT_COLOR = -1;
export const DIM_OPACITY = 0.5;
export interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
@@ -4,7 +4,8 @@
*/
import { FontWeight } from 'xterm';
import { isFirefox } from './utils/Browser';
import { CHAR_ATLAS_CELL_SPACING } from './Types';
import { isFirefox } from '../utils/Browser';
declare const Promise: any;
@@ -29,8 +30,6 @@ export interface ICharAtlasRequest {
allowTransparency: boolean;
}
export const CHAR_ATLAS_CELL_SPACING = 1;
/**
* Generates a char atlas.
* @param context The window or worker context.
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export const CHAR_ATLAS_CELL_SPACING = 1;
+2 -1
View File
@@ -276,6 +276,7 @@ export class MockBuffer implements IBuffer {
lines: ICircularList<[number, string, number, number][]>;
ydisp: number;
ybase: number;
hasScrollback: boolean;
y: number;
x: number;
tabs: any;
@@ -310,7 +311,7 @@ export class MockRenderer implements IRenderer {
}
dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
onResize(cols: number, rows: number): void {}
onCharSizeChanged(): void {}
onBlur(): void {}
onFocus(): void {}
+1 -1
View File
@@ -395,7 +395,7 @@ declare module 'xterm' {
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
/**
* (EXPERIMENTAL) Deregisters a link matcher if it has been registered.