Merge branch 'master' into 1103_search_wide_emoji

This commit is contained in:
Daniel Imms
2018-01-23 09:06:06 -08:00
committed by GitHub
63 changed files with 947 additions and 1247 deletions
-5
View File
@@ -121,11 +121,6 @@ export class Buffer implements IBuffer {
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
}
+14 -3
View File
@@ -61,24 +61,35 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the normal Buffer of the BufferSet as its currently active Buffer
*/
public activateNormalBuffer(): void {
if (this._activeBuffer === this._normal) {
return;
}
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
this._alt.clear();
this._activeBuffer = this._normal;
this.emit('activate', this._normal);
this.emit('activate', {
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
}
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
if (this._activeBuffer === this._alt) {
return;
}
// Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it.
this._alt.fillViewportRows();
this._activeBuffer = this._alt;
this.emit('activate', this._alt);
this.emit('activate', {
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
}
/**
+3 -3
View File
@@ -3,19 +3,19 @@
* @license MIT
*/
import { Charset } from './Types';
import { ICharset } from './Interfaces';
/**
* The character sets supported by the terminal. These enable several languages
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported.
*/
export const CHARSETS: { [key: string]: Charset } = {};
export const CHARSETS: { [key: string]: ICharset } = {};
/**
* The default character set, US.
*/
export const DEFAULT_CHARSET: Charset = CHARSETS['B'];
export const DEFAULT_CHARSET: ICharset = CHARSETS['B'];
/**
* DEC Special Character and Line Drawing Set.
+2 -2
View File
@@ -217,7 +217,7 @@ export class CompositionHelper {
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
};
}
/**
* Clears the textarea's position so that the cursor does not blink on IE.
@@ -226,5 +226,5 @@ export class CompositionHelper {
private clearTextareaPosition(): void {
this.textarea.style.left = '';
this.textarea.style.top = '';
};
}
}
+1 -1
View File
@@ -76,4 +76,4 @@ export namespace C0 {
export const SP = '\x20';
/** Delete (Caret = ^?) */
export const DEL = '\x7f';
};
}
+7 -9
View File
@@ -26,7 +26,7 @@ export class InputHandler implements IInputHandler {
if (char >= ' ') {
// calculate print space
// expensive call, therefore we save width in line buffer
const ch_width = wcwidth(code);
const chWidth = wcwidth(code);
if (this._terminal.charset && this._terminal.charset[char]) {
char = this._terminal.charset[char];
@@ -36,7 +36,7 @@ export class InputHandler implements IInputHandler {
// insert combining char in last cell
// FIXME: needs handling after cursor jumps
if (!ch_width && this._terminal.buffer.x) {
if (!chWidth && this._terminal.buffer.x) {
// dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
@@ -56,7 +56,7 @@ export class InputHandler implements IInputHandler {
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
this._terminal.buffer.x = 0;
@@ -70,7 +70,7 @@ export class InputHandler implements IInputHandler {
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
}
} else {
if (ch_width === 2) // FIXME: check for xterm behavior
if (chWidth === 2) // FIXME: check for xterm behavior
return;
}
}
@@ -79,7 +79,7 @@ export class InputHandler implements IInputHandler {
// insert mode: move characters to right
if (this._terminal.insertMode) {
// do this twice for a fullwidth char
for (let moves = 0; moves < ch_width; ++moves) {
for (let moves = 0; moves < chWidth; ++moves) {
// remove last cell, if it's width is 0
// we have to adjust the second last cell as well
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
@@ -94,12 +94,12 @@ export class InputHandler implements IInputHandler {
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (ch_width === 2) {
if (chWidth === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
}
@@ -928,7 +928,6 @@ export class InputHandler implements IInputHandler {
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer();
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
@@ -1100,7 +1099,6 @@ export class InputHandler implements IInputHandler {
// if (params[0] === 1049) {
// this.restoreCursor(params);
// }
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
+31 -9
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { ICharset, ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData, FontWeight } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
@@ -74,10 +74,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
options: ITerminalOptions;
cols: number;
rows: number;
charset: Charset;
charset: ICharset;
gcharset: number;
glevel: number;
charsets: Charset[];
charsets: ICharset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
@@ -116,7 +116,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
@@ -137,12 +137,14 @@ export interface ITerminalOptions {
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
enableBold?: boolean;
fontSize?: number;
fontFamily?: string;
fontWeight?: FontWeight;
fontWeightBold?: FontWeight;
handler?: (data: string) => void;
letterSpacing?: number;
lineHeight?: number;
macOptionIsMeta?: boolean;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
@@ -169,7 +171,7 @@ export interface IBuffer {
prevStop(x?: number): number;
}
export interface IBufferSet {
export interface IBufferSet extends IEventEmitter {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
@@ -198,7 +200,6 @@ export interface ISelectionManager {
disable(): void;
enable(): void;
setBuffer(buffer: IBuffer): void;
setSelection(row: number, col: number, length: number): void;
}
@@ -248,7 +249,7 @@ export interface IEventEmitter {
export interface IListenerType {
(data?: any): void;
listener?: (data?: any) => void;
};
}
export interface ILinkMatcherOptions {
/**
@@ -352,3 +353,24 @@ export interface ITheme {
brightCyan?: string;
brightWhite?: string;
}
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;
}
+3 -3
View File
@@ -4,9 +4,9 @@
*/
import { assert } from 'chai';
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { ITerminal, ILinkifier, ILinkMatcher, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { Linkifier } from './Linkifier';
import { LinkMatcher, LineData } from './Types';
import { LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
@@ -17,7 +17,7 @@ class TestLinkifier extends Linkifier {
Linkifier.TIME_BEFORE_LINKIFY = 0;
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
}
+13 -13
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
import { ILinkHoverEvent, ILinkMatcher, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes } from './Types';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
@@ -44,7 +44,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: LinkMatcher[] = [];
protected _linkMatchers: ILinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
@@ -82,12 +82,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
@@ -143,7 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
throw new Error('handler must be defined');
}
const matcher: LinkMatcher = {
const matcher: ILinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
@@ -163,7 +163,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* considered after older link matchers.
* @param matcher The link matcher to be added.
*/
private _addLinkMatcherToList(matcher: LinkMatcher): void {
private _addLinkMatcherToList(matcher: ILinkMatcher): void {
if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher);
return;
@@ -219,7 +219,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
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;
@@ -264,7 +264,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
*/
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
this._mouseZoneManager.add(new MouseZone(
x + 1,
x + 1 + uri.length,
@@ -276,17 +276,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.HOVER, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer';
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.TOOLTIP, <ILinkHoverEvent>{ x, y, length: uri.length});
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.LEAVE, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = '';
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
+2 -3
View File
@@ -21,10 +21,9 @@ class TestMockTerminal extends MockTerminal {
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: IBuffer,
charMeasure: CharMeasure
) {
super(terminal, buffer, charMeasure);
super(terminal, charMeasure);
}
public get model(): SelectionModel { return this._model; }
@@ -59,7 +58,7 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, buffer, null);
selectionManager = new TestSelectionManager(terminal, null);
});
function stringToRow(text: string): LineData {
+23 -17
View File
@@ -8,7 +8,7 @@ import * as Browser from './utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
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,10 +95,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: IListenerType;
constructor(
private _terminal: ITerminal,
private _buffer: IBuffer,
private _charMeasure: CharMeasure
) {
super();
@@ -109,18 +109,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
/**
* Initializes listener variables.
*/
private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount));
this.initBuffersListeners();
}
public initBuffersListeners(): void {
this._terminal.buffer.lines.on('trim', this._trimListener);
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
}
/**
@@ -139,16 +145,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._enabled = true;
}
/**
* Sets the active buffer, this should be called when the alt buffer is
* switched in or out.
* @param buffer The active buffer.
*/
public setBuffer(buffer: IBuffer): void {
this._buffer = buffer;
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
@@ -547,6 +543,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.emit('selection');
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
}
/**
* Converts a viewport column to the character index on the buffer line, the
* latter takes into account wide characters.
+1 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet';
import { BufferSet } from './BufferSet';
import { MockTerminal } from './utils/TestUtils.test';
class TestSelectionModel extends SelectionModel {
+18 -18
View File
@@ -15,17 +15,17 @@ import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
let primitive_pty: any;
let primitivePty: any;
// fake sychronous pty write - read
// we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode
function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitive_pty.slave, data);
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
let b = new Buffer(64000);
let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null);
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
});
}
@@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void {
/* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states
function formatError(in_: string, out_: string, expected: string): string {
function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
let counter = start || 0;
return function(s: string): string {
@@ -47,9 +47,9 @@ function formatError(in_: string, out_: string, expected: string): string {
}
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(in_);
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n';
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s;
@@ -58,15 +58,15 @@ function formatError(in_: string, out_: string, expected: string): string {
// simple debug output of terminal cells
function terminalToString(term: Terminal): string {
let result = '';
let line_s = '';
let lineText = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
line_s = '';
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');
result += line_s;
lineText = lineText.replace(/\s+$/, '');
result += lineText;
result += '\n';
}
return result;
@@ -83,7 +83,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitive_pty = pty.native.open(COLS, ROWS);
primitivePty = pty.native.open(COLS, ROWS);
/** tests */
describe('xterm output comparison', () => {
@@ -118,24 +118,24 @@ if (os.platform() !== 'win32') {
((filename: string) => {
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
let in_file = fs.readFileSync(filename, 'utf8');
ptyWriteRead(in_file, from_pty => {
let inFile = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(from_pty);
xterm.writeBuffer.push(fromPty);
xterm.innerWrite();
let from_emulator = terminalToString(xterm);
let fromEmulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (from_emulator !== expectedRightTrimmed) {
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(in_file, from_emulator, expected));
throw new Error(formatError(inFile, fromEmulator, expected));
// throw new Error('mismatch');
}
done();
+32
View File
@@ -503,6 +503,9 @@ describe('term.js addons', () => {
it('should return \\x1b[5C for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
@@ -515,6 +518,19 @@ describe('term.js addons', () => {
it('should return \\x1bf for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
});
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
@@ -597,6 +613,22 @@ describe('term.js addons', () => {
};
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should interfere with the alt key on keyDown', () => {
evKeyDown.altKey = true;
evKeyDown.keyCode = 81;
assert.equal(term.keyDown(evKeyDown), false);
evKeyDown.altKey = true;
evKeyDown.keyCode = 192;
assert.equal(term.keyDown(evKeyDown), false);
});
});
describe('On Mac OS', () => {
beforeEach(() => {
term.browser.isMac = true;
+60 -43
View File
@@ -38,9 +38,9 @@ import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
import { MouseHelper } from './utils/MouseHelper';
import { CHARSETS } from './Charsets';
import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces';
import { BellSound } from './utils/Sounds';
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';
@@ -70,16 +70,18 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
termName: 'xterm',
cursorBlink: false,
cursorStyle: 'block',
bellSound: BellSound,
bellSound: BELL_SOUND,
bellStyle: 'none',
enableBold: true,
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
fontWeight: 'normal',
fontWeightBold: 'bold',
lineHeight: 1.0,
letterSpacing: 0,
scrollback: 1000,
screenKeys: false,
debug: false,
macOptionIsMeta: false,
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
@@ -131,10 +133,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// charset
// The current charset
public charset: Charset;
public charset: ICharset;
public gcharset: number;
public glevel: number;
public charsets: Charset[];
public charsets: ICharset[];
// mouse properties
private decLocator: boolean; // This is unstable and never set
@@ -181,7 +183,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private writeStopped: boolean;
// leftover surrogate high from previous write invocation
private surrogate_high: string;
private surrogateHigh: string;
// Store if user went browsing history in scrollback
private userScrolling: boolean;
@@ -192,7 +194,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public selectionManager: SelectionManager;
public linkifier: ILinkifier;
public buffers: BufferSet;
public buffer: Buffer;
public viewport: IViewport;
private compositionHelper: ICompositionHelper;
public charMeasure: CharMeasure;
@@ -280,7 +281,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.xoffSentToCatchUp = false;
this.writeStopped = false;
this.surrogate_high = '';
this.surrogateHigh = '';
this.userScrolling = false;
this.inputHandler = new InputHandler(this);
@@ -293,17 +294,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Create the terminal's buffers and set the current buffer
this.buffers = new BufferSet(this);
this.buffer = this.buffers.active; // Convenience shortcut;
this.buffers.on('activate', (buffer: Buffer) => {
this.buffer = buffer;
});
// Ensure the selection manager has the correct buffer
if (this.selectionManager) {
this.selectionManager.setBuffer(this.buffer);
this.selectionManager.clearSelection();
this.selectionManager.initBuffersListeners();
}
}
/**
* Convenience property to active buffer.
*/
public get buffer(): Buffer {
return this.buffers.active;
}
/**
* back_color_erase feature for xterm.
*/
@@ -361,6 +364,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}`);
@@ -411,14 +424,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.renderer.clear();
this.charMeasure.measure(this.options);
break;
case 'enableBold':
case 'letterSpacing':
case 'lineHeight':
case 'fontWeight':
case 'fontWeightBold':
const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold');
// 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();
@@ -443,7 +458,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
};
}
/**
* Blur the terminal, calling the blur function on the terminal's underlying
@@ -640,7 +655,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
this.selectionManager.on('newselection', text => {
@@ -1373,7 +1388,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true);
}
if (isThirdLevelShift(this.browser, ev)) {
if (this._isThirdLevelShift(this.browser, ev)) {
return true;
}
@@ -1394,6 +1409,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true);
}
private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
/**
* Returns an object that determines how a KeyboardEvent should be handled. The key of the
* returned value is the new key code to pass to the PTY.
@@ -1573,6 +1601,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// page up
if (ev.shiftKey) {
result.scrollLines = -(this.rows - 1);
} else if (modifiers) {
result.key = C0.ESC + '[5;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[5~';
}
@@ -1581,6 +1611,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// page down
if (ev.shiftKey) {
result.scrollLines = this.rows - 1;
} else if (modifiers) {
result.key = C0.ESC + '[6;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[6~';
}
@@ -1694,8 +1726,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// On Mac this is a third level shift. Use <Esc> instead.
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32);
} else if (ev.keyCode === 192) {
@@ -1728,7 +1760,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param g
* @param charset
*/
public setgCharset(g: number, charset: Charset): void {
public setgCharset(g: number, charset: ICharset): void {
this.charsets[g] = charset;
if (this.glevel === g) {
this.charset = charset;
@@ -1761,7 +1793,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
if (!key || (
(ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev)
(ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev)
)) {
return false;
}
@@ -2075,11 +2107,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.options.cols = this.cols;
const customKeyEventHandler = this.customKeyEventHandler;
const inputHandler = this.inputHandler;
const buffers = this.buffers;
this.setup();
this.customKeyEventHandler = customKeyEventHandler;
this.inputHandler = inputHandler;
this.buffers = buffers;
this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea();
}
@@ -2155,19 +2185,6 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo
el.removeEventListener(type, handler, capture);
}
function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
@@ -2220,7 +2237,7 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2:
return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2);
};
}
function matchColor_(r1: number, g1: number, b1: number): number {
+3 -18
View File
@@ -3,33 +3,18 @@
* @license MIT
*/
export type LinkMatcher = {
id: number,
regex: RegExp,
handler: LinkMatcherHandler,
hoverTooltipCallback?: LinkMatcherHandler,
hoverLeaveCallback?: () => void,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type Charset = {[key: string]: string};
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkHoverEvent = {
x: number,
y: number,
length: number
};
export enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
};
}
export type FontWeight = 'normal' | 'bold' | 'bolder' | 'lighter' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
+3 -3
View File
@@ -119,7 +119,7 @@ export class Viewport implements IViewport {
this.viewportElement.scrollTop += ev.deltaY * multiplier;
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
};
}
/**
* Handles the touchstart event, recording the touch occurred.
@@ -127,7 +127,7 @@ export class Viewport implements IViewport {
*/
public onTouchStart(ev: TouchEvent): void {
this.lastTouchY = ev.touches[0].pageY;
};
}
/**
* Handles the touchmove event, scrolling the viewport if the position shifted.
@@ -141,5 +141,5 @@ export class Viewport implements IViewport {
}
this.viewportElement.scrollTop += deltaY;
ev.preventDefault();
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as attach from './attach'
import * as attach from './attach';
class MockTerminal {}
describe('attach addon', () => {
describe('apply', () => {
it('should do register the `attach` and `detach` methods', () => {
attach.apply(MockTerminal);
attach.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function');
});
+55 -55
View File
@@ -5,118 +5,118 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IAttachAddonTerminal } from './Intefaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function attach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <IAttachAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
var myTextDecoder;
let myTextDecoder;
term._getMessage = function(ev) {
var str;
if (typeof ev.data === "object") {
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
if (typeof ev.data === 'object') {
if (ev.data instanceof ArrayBuffer) {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
str = myTextDecoder.decode( ev.data );
}
else {
throw "TODO: handle Blob?";
} else {
throw 'TODO: handle Blob?';
}
}
if (buffered) {
term._pushToBuffer(str || ev.data);
addonTerminal.__pushToBuffer(str || ev.data);
} else {
term.write(str || ev.data);
addonTerminal.write(str || ev.data);
}
};
term._sendData = function(data) {
addonTerminal.__sendData = (data: string) => {
if (socket.readyState !== 1) {
return;
}
socket.send(data);
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', term.detach.bind(term, socket));
};
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Terminal} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function detach(term, socket) {
term.off('data', term._sendData);
export function detach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <IAttachAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
return attach(this, socket, bidirectional, buffered);
(<any>terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
attach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.detach = function(socket) {
return detach(this, socket);
(<any>terminalConstructor.prototype).detach = function (socket: WebSocket): void {
detach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fit from './fit'
import * as fit from './fit';
class MockTerminal {}
describe('fit addon', () => {
describe('apply', () => {
it('should do register the `proposeGeometry` and `fit` methods', () => {
fit.apply(MockTerminal);
fit.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function');
});
+35 -26
View File
@@ -13,43 +13,52 @@
* row and truncate its width with the current number of columns).
*/
export function proposeGeometry(term) {
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
export interface IGeometry {
rows: number;
cols: number;
}
export function proposeGeometry(term: Terminal): IGeometry {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
var elementStyle = window.getComputedStyle(term.element);
var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor;
var geometry = {
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
const elementStyle = window.getComputedStyle(term.element);
const elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
const elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
};
return geometry;
};
}
export function fit(term) {
var geometry = proposeGeometry(term);
export function fit(term: Terminal): void {
const geometry = proposeGeometry(term);
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term.renderer.clear();
(<any>term).renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.proposeGeometry = function() {
return proposeGeometry(this);
}
terminalConstructor.prototype.fit = function() {
return fit(this);
}
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {
return proposeGeometry(this);
};
(<any>terminalConstructor.prototype).fit = function (): void {
fit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'
import * as fullscreen from './fullscreen';
class MockTerminal {}
describe('fullscreen addon', () => {
describe('apply', () => {
it('should do register the `toggleFullscreen` method', () => {
fullscreen.apply(MockTerminal);
fullscreen.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
});
});
+13 -9
View File
@@ -3,15 +3,19 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
* Toggle the given terminal's fullscreen mode.
* @param {Terminal} term - The terminal to toggle full screen mode
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
* @param term The terminal to toggle full screen mode
* @param fullscreen Toggle fullscreen on (true) or off (false)
*/
export function toggleFullScreen(term, fullscreen) {
var fn;
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
let fn: string;
if (typeof fullscreen == 'undefined') {
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) {
fn = 'remove';
@@ -20,10 +24,10 @@ export function toggleFullScreen(term, fullscreen) {
}
term.element.classList[fn]('fullscreen');
};
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
return toggleFullScreen(this, fullscreen);
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {
toggleFullScreen(this, fullscreen);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
buffer: any;
selectionManager: any;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
}
+4 -5
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
// import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
interface ISearchResult {
term: string;
@@ -15,8 +14,8 @@ interface ISearchResult {
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper {
constructor(private _terminal: any) {
export class SearchHelper implements ISearchHelper {
constructor(private _terminal: ISearchAddonTerminal) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
@@ -149,7 +148,7 @@ export class SearchHelper {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp, false);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
return true;
}
}
+25 -20
View File
@@ -3,8 +3,11 @@
* @license MIT
*/
import { SearchHelper } from './SearchHelper';
/// <reference path="../../../typings/xterm.d.ts"/>
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
@@ -12,12 +15,13 @@ import { SearchHelper } from './SearchHelper';
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findNext(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
export function findNext(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return (<SearchHelper>terminal.searchHelper).findNext(term);
};
return addonTerminal.__searchHelper.findNext(term);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
@@ -25,19 +29,20 @@ export function findNext(terminal: any, term: string): boolean {
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findPrevious(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
}
return (<SearchHelper>terminal.searchHelper).findPrevious(term);
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.findNext = function(term) {
return findNext(this, term);
}
terminalConstructor.prototype.findPrevious = function(term) {
return findPrevious(this, term);
export function findPrevious(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term);
};
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface ITerminadoAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
__setSize?(size: {rows: number, cols: number}): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as terminado from './terminado'
import * as terminado from './terminado';
class MockTerminal {}
describe('terminado addon', () => {
describe('apply', () => {
it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => {
terminado.apply(MockTerminal);
terminado.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function');
});
+52 -51
View File
@@ -6,106 +6,107 @@
* WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { ITerminadoAddonTerminal } from './Intefaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function terminadoAttach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
term._getMessage = function(ev) {
var data = JSON.parse(ev.data)
if( data[0] == "stdout" ) {
addonTerminal.__getMessage = (ev: MessageEvent) => {
const data = JSON.parse(ev.data);
if (data[0] === 'stdout') {
if (buffered) {
term._pushToBuffer(data[1]);
addonTerminal.__pushToBuffer(data[1]);
} else {
term.write(data[1]);
addonTerminal.write(data[1]);
}
}
};
term._sendData = function(data) {
addonTerminal.__sendData = (data: string) => {
socket.send(JSON.stringify(['stdin', data]));
};
term._setSize = function(size) {
addonTerminal.__setSize = (size: {rows: number, cols: number}) => {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
term.on('resize', term._setSize);
addonTerminal.on('resize', addonTerminal.__setSize);
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
};
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Xterm} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function terminadoDetach(term, socket) {
term.off('data', term._sendData);
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket - The socket to attach the current terminal.
* @param bidirectional - Whether the terminal should send data to the socket as well.
* @param buffered - Whether the rendering of incoming data should happen instantly or at a
* maximum frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) {
(<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
return terminadoAttach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.terminadoDetach = function(socket) {
(<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {
return terminadoDetach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
buffer: any;
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'
import * as winptyCompat from './winptyCompat';
class MockTerminal {}
describe('winptyCompat addon', () => {
describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(MockTerminal);
winptyCompat.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
});
});
+34 -27
View File
@@ -3,36 +3,43 @@
* @license MIT
*/
export function winptyCompatInit(terminal): void {
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
terminal.on('linefeed', () => {
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
const lastChar = line[terminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
});
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.winptyCompatInit = function(): void {
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
winptyCompatInit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as zmodem from './zmodem'
import * as zmodem from './zmodem';
class MockTerminal {}
describe('zmodem addon', () => {
describe('apply', () => {
it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => {
zmodem.apply(MockTerminal);
zmodem.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object');
});
+31 -29
View File
@@ -1,3 +1,12 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
*
* Allow xterm.js to handle ZMODEM uploads and downloads.
@@ -27,47 +36,40 @@
* via `detach()` and a re-`attach()`.)
*/
let Zmodem;
let zmodem;
export function zmodemAttach(term, ws, opts) {
if (!opts) opts = {};
export interface IZModemOptions {
noTerminalWriteOutsideSession?: boolean;
}
var senderFunc = function _ws_sender_func(octets) {
ws.send(new Uint8Array(octets));
};
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void {
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
var zsentry;
let zsentry;
function _shouldWrite() {
function _shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new Zmodem.Sentry( {
to_terminal: function _to_terminal(octets) {
zsentry = new zmodem.Sentry({
to_terminal: (octets: ArrayLike<number>) => {
if (_shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
}
},
sender: senderFunc,
on_retract: () => (<any>term).emit('zmodemRetract'),
on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)
});
on_retract: function _on_retract() {
term.emit('zmodemRetract');
},
function handleWSMessage(evt: MessageEvent): void {
on_detect: function _on_detect(detection) {
term.emit('zmodemDetect', detection);
},
} );
function handleWSMessage(evt) {
//In testing with xterm.js’s demo the first message was
//always text even if the rest were binary. While that
//may be specific to xterm.js’s demo, ultimately we
//should reject anything that isn’t binary.
// In testing with xterm.js’s demo the first message was
// always text even if the rest were binary. While that
// may be specific to xterm.js’s demo, ultimately we
// should reject anything that isn’t binary.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
term.write(evt.data);
@@ -82,9 +84,9 @@ export function zmodemAttach(term, ws, opts) {
ws.addEventListener('message', handleWSMessage);
}
export function apply(terminalConstructor) {
Zmodem = (typeof window == 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
export function apply(terminalConstructor: typeof Terminal): void {
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this);
terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this);
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
}
+1 -1
View File
@@ -178,7 +178,7 @@ export class MouseZoneManager implements IMouseZoneManager {
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
return zone;
}
};
}
return null;
}
}
+14 -13
View File
@@ -201,7 +201,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(
@@ -230,7 +230,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
colorIndex = fg + 2;
} else {
// If default color and bold
if (bold && terminal.options.enableBold) {
if (bold) {
colorIndex = 1;
}
}
@@ -251,13 +251,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw the non-bold version of the same color if bold is not enabled
if (bold && !terminal.options.enableBold) {
// Ignore default color as it's not touched above
if (colorIndex > 1) {
colorIndex -= 8;
}
}
this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth,
@@ -289,10 +282,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 +322,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}`;
}
}
+16 -6
View File
@@ -12,6 +12,8 @@ export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
fontWeight: string;
fontWeightBold: string;
scaledCharWidth: number;
scaledCharHeight: number;
colors: IColorSet;
@@ -64,7 +66,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
}
const newEntry: ICharAtlasCacheEntry = {
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, terminal.options.fontWeight, terminal.options.fontWeightBold, colors.background, colors.foreground, colors.ansi),
config: newConfig,
ownedBy: [terminal]
};
@@ -86,6 +88,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,6 +102,8 @@ 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 &&
@@ -126,7 +132,7 @@ class CharAtlasGenerator {
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<ImageBitmap> {
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, fontWeight: string, fontWeightBold: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._canvas.width = 255 * cellWidth;
@@ -137,7 +143,7 @@ class CharAtlasGenerator {
this._ctx.save();
this._ctx.fillStyle = foreground;
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.font = this._getFont(fontWeight, fontSize, fontFamily);
this._ctx.textBaseline = 'top';
// Default color
@@ -151,7 +157,7 @@ class CharAtlasGenerator {
}
// Default color bold
this._ctx.save();
this._ctx.font = `bold ${this._ctx.font}`;
this._ctx.font = this._getFont(fontWeightBold, fontSize, fontFamily);
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
@@ -163,11 +169,11 @@ class CharAtlasGenerator {
this._ctx.restore();
// Colors 0-15
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.font = this._getFont(fontWeight, fontSize, fontFamily);
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = `bold ${this._ctx.font}`;
this._ctx.font = this._getFont(fontWeightBold, fontSize, fontFamily);
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
@@ -219,4 +225,8 @@ class CharAtlasGenerator {
}
}
}
private _getFont(fontWeight: string, fontSize: number, fontFamily: string): string {
return `${fontWeight} ${fontSize * window.devicePixelRatio}px ${fontFamily}`;
}
}
+4 -3
View File
@@ -11,7 +11,7 @@ import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData } from '../Types';
interface CursorState {
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
@@ -25,7 +25,7 @@ interface CursorState {
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: CursorState;
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean;
@@ -108,9 +108,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Only render if the animation frame is not active
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
}
+7 -7
View File
@@ -4,20 +4,20 @@
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { IBuffer, ICharMeasure, ILinkHoverEvent, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
import { LinkHoverEventTypes } from '../Types';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: LinkHoverEvent = null;
private _state: ILinkHoverEvent = null;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
super(container, 'link', zIndex, true, colors);
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
@@ -37,13 +37,13 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
private _onLinkHover(e: LinkHoverEvent): void {
private _onLinkHover(e: ILinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length);
this._state = e;
}
private _onLinkLeave(e: LinkHoverEvent): void {
private _onLinkLeave(e: ILinkHoverEvent): void {
this._clearCurrentLink();
}
}
+1
View File
@@ -57,6 +57,7 @@ export class Renderer extends EventEmitter implements IRenderer {
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this.onOptionsChanged();
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
+5 -5
View File
@@ -33,7 +33,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 +166,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;
@@ -192,9 +192,9 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
/**
* Whether a character is overlapping to the next cell.
*/
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(charData: CharData): boolean {
// Only single cell characters can be overlapping, rendering issues can
// occur without this check
+1 -1
View File
@@ -13,4 +13,4 @@ export enum FLAGS {
INVERSE = 8,
INVISIBLE = 16,
DIM = 32
};
}
+1 -1
View File
@@ -10,4 +10,4 @@
*/
export function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
};
}
+1 -1
View File
@@ -7,4 +7,4 @@
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const BellSound = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export const BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';