Merge branch 'master' into feature-778

This commit is contained in:
Daniel Imms
2018-01-05 11:03:33 -08:00
committed by GitHub
63 changed files with 1855 additions and 911 deletions
+161
View File
@@ -0,0 +1,161 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1])
return false;
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1])
min = mid + 1;
else if (ucs < data[mid][0])
max = mid - 1;
else
return true;
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0)
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return opts.control;
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
return 0;
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
}
return 1;
}
function isWideBMP(ucs: number): boolean {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH))
return 0;
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function init_table(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
return control | 0;
if (num < 127)
return 1;
let t = table || init_table();
if (num < 65536)
return t[num >> 4] >> ((num & 15) << 1) & 3;
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
+14 -1
View File
@@ -5,7 +5,6 @@
import { assert } from 'chai';
import { InputHandler } from './InputHandler';
import { wcwidth } from './InputHandler';
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
describe('InputHandler', () => {
@@ -66,4 +65,18 @@ describe('InputHandler', () => {
assert.equal(terminal.options['cursorBlink'], false);
});
});
describe('setMode', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
let terminal = new MockInputHandlingTerminal();
terminal.prefix = '?';
terminal.bracketedPasteMode = false;
let inputHandler = new InputHandler(terminal);
// Set bracketed paste mode
inputHandler.setMode([2004]);
assert.equal(terminal.bracketedPasteMode, true);
// Reset bracketed paste mode
inputHandler.resetMode([2004]);
assert.equal(terminal.bracketedPasteMode, false);
});
});
});
+9 -159
View File
@@ -10,6 +10,7 @@ import { DEFAULT_CHARSET } from './Charsets';
import { CharData } from './Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
/**
* The terminal's standard implementation of IInputHandler, this handles all
@@ -133,9 +134,9 @@ export class InputHandler implements IInputHandler {
/**
* This event is emitted whenever the terminal outputs a LF or NL.
*
* @event lineFeed
* @event linefeed
*/
this._terminal.emit('lineFeed');
this._terminal.emit('linefeed');
}
/**
@@ -931,6 +932,9 @@ export class InputHandler implements IInputHandler {
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
this._terminal.bracketedPasteMode = true;
break;
}
}
}
@@ -1101,6 +1105,9 @@ export class InputHandler implements IInputHandler {
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
this._terminal.bracketedPasteMode = false;
break;
}
}
}
@@ -1446,160 +1453,3 @@ export class InputHandler implements IInputHandler {
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
}
}
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1])
return false;
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1])
min = mid + 1;
else if (ucs < data[mid][0])
max = mid - 1;
else
return true;
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0)
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return opts.control;
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
return 0;
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
}
return 1;
}
function isWideBMP(ucs: number): boolean {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH))
return 0;
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function init_table(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
return control | 0;
if (num < 127)
return 1;
let t = table || init_table();
if (num < 65536)
return t[num >> 4] >> ((num & 15) << 1) & 3;
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
+2 -1
View File
@@ -48,6 +48,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
bracketedPasteMode: boolean;
/**
* Emit the 'data' event and populate the given data.
@@ -82,6 +83,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
@@ -138,7 +140,6 @@ export interface ITerminalOptions {
enableBold?: boolean;
fontSize?: number;
fontFamily?: string;
geometry?: [number, number];
handler?: (data: string) => void;
letterSpacing?: number;
lineHeight?: number;
+7
View File
@@ -286,36 +286,43 @@ export class Parser {
// Single Shift Select of G2 Character Set
// ( SS2 is 0x8e). This affects next character only.
case 'N':
this._state = ParserState.NORMAL;
break;
// ESC O
// Single Shift Select of G3 Character Set
// ( SS3 is 0x8f). This affects next character only.
case 'O':
this._state = ParserState.NORMAL;
break;
// ESC n
// Invoke the G2 Character Set as GL (LS2).
case 'n':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC o
// Invoke the G3 Character Set as GL (LS3).
case 'o':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC |
// Invoke the G3 Character Set as GR (LS3R).
case '|':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC }
// Invoke the G2 Character Set as GR (LS2R).
case '}':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC ~
// Invoke the G1 Character Set as GR (LS1R).
case '~':
this._terminal.setgLevel(1);
this._state = ParserState.NORMAL;
break;
// ESC 7 Save Cursor (DECSC).
+5 -1
View File
@@ -14,6 +14,10 @@ import { BufferSet } from './BufferSet';
import { MockTerminal } from './utils/TestUtils.test';
import { LineData, CharData } from './Types';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
}
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
@@ -48,7 +52,7 @@ describe('SelectionManager', () => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
terminal = new MockTerminal();
terminal = new TestMockTerminal();
terminal.cols = 80;
terminal.rows = 2;
terminal.options.scrollback = 100;
+5 -1
View File
@@ -288,6 +288,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
public selectAll(): void {
this._model.isSelectAllActive = true;
this.refresh();
this._terminal.emit('selection');
}
/**
@@ -560,7 +561,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
} else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
@@ -574,6 +575,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _onMouseUp(event: MouseEvent): void {
this._removeMouseDownListeners();
if (this.hasSelection)
this._terminal.emit('selection');
}
/**
+5 -4
View File
@@ -3,8 +3,9 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import * as attach from './addons/attach/attach';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
@@ -42,9 +43,9 @@ describe('term.js addons', () => {
};
});
it('should load addons with Terminal.loadAddon', () => {
Terminal.loadAddon('attach');
// Test that addon was loaded successfully, adding attach to Terminal's
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
// prototype.
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
});
+35 -39
View File
@@ -47,12 +47,6 @@ import { MouseZoneManager } from './input/MouseZoneManager';
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
import { IRenderer } from './renderer/Interfaces';
// Declares required for loadAddon
declare var exports: any;
declare var module: any;
declare var define: any;
declare var require: any;
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -70,9 +64,10 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
const WRITE_BATCH_SIZE = 300;
const DEFAULT_OPTIONS: ITerminalOptions = {
cols: 80,
rows: 24,
convertEol: false,
termName: 'xterm',
geometry: [80, 24],
cursorBlink: false,
cursorStyle: 'block',
bellSound: BellSound,
@@ -133,6 +128,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public originMode: boolean;
public insertMode: boolean;
public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false
public bracketedPasteMode: boolean;
// charset
// The current charset
@@ -206,7 +202,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public cols: number;
public rows: number;
public geometry: [/*cols*/number, /*rows*/number];
/**
* Creates a new `Terminal` object.
@@ -242,9 +237,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// TODO: WHy not document.body?
this.parent = document ? document.body : null;
this.cols = this.options.cols || this.options.geometry[0];
this.rows = this.options.rows || this.options.geometry[1];
this.geometry = [this.cols, this.rows];
this.cols = this.options.cols;
this.rows = this.options.rows;
if (this.options.handler) {
this.on('data', this.options.handler);
@@ -261,6 +255,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.originMode = false;
this.insertMode = false;
this.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this.bracketedPasteMode = false;
// charset
this.charset = null;
@@ -322,7 +317,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* Focus the terminal. Delegates focus handling to the terminal's DOM element.
*/
public focus(): void {
this.textarea.focus();
if (this.textarea) {
this.textarea.focus();
}
}
public get isFocused(): boolean {
@@ -584,19 +581,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.element = this.document.createElement('div');
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.setAttribute('tabindex', '0');
this.parent.appendChild(this.element);
// Performance: Use a document fragment to build the terminal
// viewport and helper elements detached from the DOM
const fragment = document.createDocumentFragment();
this.viewportElement = document.createElement('div');
this.viewportElement.classList.add('xterm-viewport');
this.element.appendChild(this.viewportElement);
fragment.appendChild(this.viewportElement);
this.viewportScrollArea = document.createElement('div');
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
// preload audio
this.syncBellSound();
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
@@ -605,8 +602,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// capturing DOM Events. Then produce the helpers.
this.helperContainer = document.createElement('div');
this.helperContainer.classList.add('xterm-helpers');
// TODO: This should probably be inserted once it's filled to prevent an additional layout
this.element.appendChild(this.helperContainer);
fragment.appendChild(this.helperContainer);
this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('autocorrect', 'off');
@@ -624,11 +621,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.charSizeStyleElement = document.createElement('style');
this.helperContainer.appendChild(this.charSizeStyleElement);
this.parent.appendChild(this.element);
this.charMeasure = new CharMeasure(document, this.helperContainer);
// Preload audio, this relied on helperContainer
this.syncBellSound();
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this.renderer = new Renderer(this, this.options.theme);
this.options.theme = null;
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
@@ -673,6 +673,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Listen for mouse events and translate
// them into terminal mouse protocols.
this.bindMouse();
}
/**
@@ -687,22 +688,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
/**
* Attempts to load an add-on using CommonJS or RequireJS (whichever is available).
* @param {string} addon The name of the addon to load
* @static
* Apply the provided addon on the `Terminal` class.
* @param addon The addon to apply.
*/
public static loadAddon(addon: string, callback?: Function): boolean | any {
// TODO: Improve return type and documentation
if (typeof exports === 'object' && typeof module === 'object') {
// CommonJS
return require('./addons/' + addon + '/' + addon);
} else if (typeof define === 'function') {
// RequireJS
return (<any>require)(['./addons/' + addon + '/' + addon], callback);
} else {
console.error('Cannot load a module without a CommonJS or RequireJS environment.');
return false;
}
public static applyAddon(addon: any): void {
addon.apply(Terminal);
}
/**
@@ -1460,6 +1450,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (ev.shiftKey) {
result.key = C0.BS; // ^H
break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^?
break;
}
result.key = C0.DEL; // ^?
break;
@@ -1866,8 +1859,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.charMeasure.measure(this.options);
this.refresh(0, this.rows - 1);
this.geometry = [this.cols, this.rows];
this.emit('resize', {cols: x, rows: y});
}
@@ -2129,6 +2120,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
private syncBellSound(): void {
// Don't update anything if the terminal has not been opened yet
if (!this.element) {
return;
}
if (this.soundBell() && this.bellAudioElement) {
this.bellAudioElement.setAttribute('src', this.options.bellSound);
} else if (this.soundBell()) {
-125
View File
@@ -1,125 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
(function (attach) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = attach(require('../../Terminal').Terminal);
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], attach);
} else {
/*
* Plain browser environment
*/
attach(window.Terminal);
}
})(function (Terminal) {
'use strict';
var exports = {};
/**
* 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.
*/
exports.attach = function (term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
term._flushBuffer = function () {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
};
term._pushToBuffer = function (data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
}
};
term._getMessage = function (ev) {
if (buffered) {
term._pushToBuffer(ev.data);
} else {
term.write(ev.data);
}
};
term._sendData = function (data) {
socket.send(data);
};
socket.addEventListener('message', term._getMessage);
if (bidirectional) {
term.on('data', term._sendData);
}
socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', term.detach.bind(term, 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.
*/
exports.detach = function (term, socket) {
term.off('data', term._sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
}
delete term.socket;
};
/**
* 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.
*/
Terminal.prototype.attach = function (socket, bidirectional, buffered) {
return exports.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.
*/
Terminal.prototype.detach = function (socket) {
return exports.detach(this, socket);
};
return exports;
});
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as attach from './attach'
class MockTerminal {}
describe('attach addon', () => {
describe('apply', () => {
it('should do register the `attach` and `detach` methods', () => {
attach.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function');
});
});
});
+122
View File
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
/**
* 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.
*/
export function attach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
}
};
var myTextDecoder;
term._getMessage = function(ev) {
var 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?";
}
}
if (buffered) {
term._pushToBuffer(str || ev.data);
} else {
term.write(str || ev.data);
}
};
term._sendData = function(data) {
if (socket.readyState !== 1) {
return;
}
socket.send(data);
};
socket.addEventListener('message', term._getMessage);
if (bidirectional) {
term.on('data', term._sendData);
}
socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', term.detach.bind(term, 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.
*/
export function detach(term, socket) {
term.off('data', term._sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
}
delete term.socket;
};
export function apply(terminalConstructor) {
/**
* 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.
*/
terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
return 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.
*/
terminalConstructor.prototype.detach = function(socket) {
return detach(this, socket);
};
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true
}
}
-81
View File
@@ -1,81 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Fit terminal columns and rows to the dimensions of its DOM element.
*
* ## Approach
*
* Rows: Truncate the division of the terminal parent element height by the
* terminal row height.
* Columns: Truncate the division of the terminal parent element width by the
* terminal character width (apply display: inline at the terminal
* row and truncate its width with the current number of columns).
*/
(function (fit) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = fit(require('../../Terminal').Terminal);
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], fit);
} else {
/*
* Plain browser environment
*/
fit(window.Terminal);
}
})(function (Terminal) {
var exports = {};
exports.proposeGeometry = function (term) {
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.charMeasure.width),
rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight')))
};
return geometry;
};
exports.fit = function (term) {
// Wrap fit in a setTimeout as charMeasure needs time to get initialized
// after calling Terminal.open
setTimeout(function () {
var geometry = exports.proposeGeometry(term);
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
}, 0);
};
Terminal.prototype.proposeGeometry = function () {
return exports.proposeGeometry(this);
};
Terminal.prototype.fit = function () {
return exports.fit(this);
};
return exports;
});
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as fit from './fit'
class MockTerminal {}
describe('fit addon', () => {
describe('apply', () => {
it('should do register the `proposeGeometry` and `fit` methods', () => {
fit.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function');
});
});
});
+55
View File
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Fit terminal columns and rows to the dimensions of its DOM element.
*
* ## Approach
*
* Rows: Truncate the division of the terminal parent element height by the
* terminal row height.
* Columns: Truncate the division of the terminal parent element width by the
* terminal character width (apply display: inline at the terminal
* row and truncate its width with the current number of columns).
*/
export function proposeGeometry(term) {
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)
};
return geometry;
};
export function fit(term) {
var geometry = proposeGeometry(term);
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
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);
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true
}
}
-50
View File
@@ -1,50 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*/
(function (fullscreen) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = fullscreen(require('../../Terminal').Terminal);
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], fullscreen);
} else {
/*
* Plain browser environment
*/
fullscreen(window.Terminal);
}
})(function (Terminal) {
var exports = {};
/**
* 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)
*/
exports.toggleFullScreen = function (term, fullscreen) {
var fn;
if (typeof fullscreen == 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) {
fn = 'remove';
} else {
fn = 'add';
}
term.element.classList[fn]('fullscreen');
};
Terminal.prototype.toggleFullscreen = function (fullscreen) {
exports.toggleFullScreen(this, fullscreen);
};
return exports;
});
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'
class MockTerminal {}
describe('fullscreen addon', () => {
describe('apply', () => {
it('should do register the `toggleFullscreen` method', () => {
fullscreen.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
});
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* 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)
*/
export function toggleFullScreen(term, fullscreen) {
var fn;
if (typeof fullscreen == 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) {
fn = 'remove';
} else {
fn = 'add';
}
term.element.classList[fn]('fullscreen');
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
return toggleFullScreen(this, fullscreen);
};
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.search",
"main": "search.js",
"private": true
}
+33 -47
View File
@@ -5,53 +5,39 @@
import { SearchHelper } from './SearchHelper';
declare var exports: any;
declare var module: any;
declare var define: any;
declare var require: any;
declare var window: any;
(function (addon) {
if (typeof window !== 'undefined' && 'Terminal' in window) {
/**
* Plain browser environment
*/
addon(window.Terminal);
} else if (typeof exports === 'object' && typeof module === 'object') {
/**
* CommonJS environment
*/
module.exports = addon(require('../../Terminal').Terminal);
} else if (typeof define === 'function') {
/**
* Require.js is available
*/
define(['../../xterm'], addon);
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findNext(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
}
})((Terminal: any) => {
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findNext = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this);
}
return (<SearchHelper>this.searchHelper).findNext(term);
};
return (<SearchHelper>terminal.searchHelper).findNext(term);
};
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findPrevious = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this);
}
return (<SearchHelper>this.searchHelper).findPrevious(term);
};
});
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
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);
}
}
-134
View File
@@ -1,134 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*
* This module provides methods for attaching a terminal to a terminado
* WebSocket stream.
*/
(function (attach) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = attach(require('../../Terminal').Terminal);
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], attach);
} else {
/*
* Plain browser environment
*/
attach(window.Terminal);
}
})(function (Terminal) {
'use strict';
var exports = {};
/**
* 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.
*/
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
term._flushBuffer = function () {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
};
term._pushToBuffer = function (data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
}
};
term._getMessage = function (ev) {
var data = JSON.parse(ev.data)
if( data[0] == "stdout" ) {
if (buffered) {
term._pushToBuffer(data[1]);
} else {
term.write(data[1]);
}
}
};
term._sendData = function (data) {
socket.send(JSON.stringify(['stdin', data]));
};
term._setSize = function (size) {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', term._getMessage);
if (bidirectional) {
term.on('data', term._sendData);
}
term.on('resize', term._setSize);
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', term.terminadoDetach.bind(term, 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.
*/
exports.terminadoDetach = function (term, socket) {
term.off('data', term._sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
}
delete term.socket;
};
/**
* 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.
*/
Terminal.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
return exports.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.
*/
Terminal.prototype.terminadoDetach = function (socket) {
return exports.terminadoDetach(this, socket);
};
return exports;
});
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as terminado from './terminado'
class MockTerminal {}
describe('terminado addon', () => {
describe('apply', () => {
it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => {
terminado.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function');
});
});
});
+111
View File
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*
* This module provides methods for attaching a terminal to a terminado
* WebSocket stream.
*/
/**
* 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.
*/
export function terminadoAttach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
}
};
term._getMessage = function(ev) {
var data = JSON.parse(ev.data)
if( data[0] == "stdout" ) {
if (buffered) {
term._pushToBuffer(data[1]);
} else {
term.write(data[1]);
}
}
};
term._sendData = function(data) {
socket.send(JSON.stringify(['stdin', data]));
};
term._setSize = function(size) {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', term._getMessage);
if (bidirectional) {
term.on('data', term._sendData);
}
term.on('resize', term._setSize);
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', term.terminadoDetach.bind(term, 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.
*/
export function terminadoDetach(term, socket) {
term.off('data', term._sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
}
delete term.socket;
};
export function apply(terminalConstructor) {
/**
* 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.
*/
terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) {
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.
*/
terminalConstructor.prototype.terminadoDetach = function(socket) {
return terminadoDetach(this, socket);
};
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.winptyCompat",
"main": "winptyCompat.js",
"private": true
}
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'
class MockTerminal {}
describe('winptyCompat addon', () => {
describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
});
});
});
+12 -27
View File
@@ -3,28 +3,7 @@
* @license MIT
*/
declare var exports: any;
declare var define: any;
(function (addon) {
if (typeof window !== 'undefined' && 'Terminal' in window) {
/**
* Plain browser environment
*/
addon((<any>window).Terminal);
} else if (typeof exports === 'object' && typeof module === 'object') {
/**
* CommonJS environment
*/
module.exports = addon(require('../../Terminal').Terminal);
} else if (typeof define === 'function') {
/**
* Require.js is available
*/
define(['../../xterm'], addon);
}
})((Terminal: any) => {
Terminal.prototype.winptyCompatInit = function(): void {
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) {
@@ -41,13 +20,19 @@ declare var define: any;
// 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.
this.on('lineFeed', () => {
const line = this.buffer.lines.get(this.buffer.ybase + this.buffer.y - 1);
const lastChar = line[this.cols - 1];
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 = this.buffer.lines.get(this.buffer.ybase + this.buffer.y);
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 {
winptyCompatInit(this);
};
});
}
+87
View File
@@ -0,0 +1,87 @@
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var os = require('os');
var pty = require('node-pty');
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../../../../build'));
app.use('/demo', express.static(__dirname + '/../../../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../../../node_modules/zmodem.js/dist'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
app.get('/main.js', function(req, res){
res.sendFile(__dirname + '/main.js');
});
app.post('/terminals', function (req, res) {
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
encoding: null,
name: 'xterm-color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
});
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
});
app.post('/terminals/:pid/size', function (req, res) {
var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = terminals[pid];
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
term.on('data', function(data) {
try {
ws.send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
});
ws.on('message', function(msg) {
term.write(msg);
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
console.log('App listening to http://' + host + ':' + port);
app.listen(port, host);
+128
View File
@@ -0,0 +1,128 @@
<!doctype html>
<html>
<head>
<title>xterm.js demo</title>
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="stylesheet" href="/demo/style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
<script src="/build/xterm.js" ></script>
<script src="/build/addons/attach/attach.js" ></script>
<script src="/zmodemjs/zmodem.js"></script>
<script src="/build/addons/zmodem/zmodem.js" ></script>
<script src="/build/addons/fit/fit.js" ></script>
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
<script src="/build/addons/search/search.js" ></script>
</head>
<body>
<h1>xterm.js: xterm, in the browser</h1>
<div id="terminal-container"></div>
<div id="zmodem_controls">
<form id="zm_start" style="display: none" action="javascript:void(0)">
ZMODEM detected: Start ZMODEM session?
<label><input id="zmstart_yes" name="zmstart" type=radio checked value="1"> Yes</label>
&nbsp;
<label><input name="zmstart" type=radio value=""> No</label>
<button type="submit">Submit</button>
</form>
<form id="zm_offer" style="display: none" action="javascript:void(0)">
<p>ZMODEM File offered!</p>
<label><input id="zmaccept_yes" name="zmaccept" type=radio checked value="1"> Accept</label>
&nbsp;
<label><input name="zmaccept" type=radio value=""> Skip</label>
<button type="submit">Submit</button>
</form>
<div id="zm_file" style="display: none">
<div>Name: <span id="name"></span></div>
<div>Size: <span id="size"></span></div>
<div>Last modified: <span id="mtime"></span></div>
<div>Mode: <span id="mode"></span></div>
<br>
<div>Conversion: <span id="zfile_conversion"></span></div>
<div>Management: <span id="zfile_management"></span></div>
<div>Transport: <span id="zfile_transport"></span></div>
<div>Sparse? <span id="zfile_sparse"></span></div>
<br>
<div>Files remaining in batch: <span id="files_remaining"></span></div>
<div>Bytes remaining in batch: <span id="bytes_remaining"></span></div>
</div>
<form id="zm_progress" style="display: none" action="javascript:void(0)">
<div><span id="percent_received"></span>% (<span id="bytes_received"></span> bytes) received</div>
<button id="zm_progress_skipper" type="button" onclick="skip_current_file();">Skip File</button>
</form>
<form id="zm_choose" style="display: none" action="javascript:void(0)">
<label>Choose file(s): <input id="zm_files" type="file" multiple></label>
</form>
</div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" checked id="zmodem-auto"> Accept all ZMODEM prompts<sup>*</sup></label>
</p>
<p>
<label>
cursorStyle
<select id="option-cursor-style">
<option value="block">block</option>
<option value="underline">underline</option>
<option value="bar">bar</option>
</select>
</label>
</p>
<p>
<label>
bellStyle
<select id="option-bell-style">
<option value="">none</option>
<option value="sound">sound</option>
<option value="visual">visual</option>
<option value="both">both</option>
</select>
</label>
</p>
<p>
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
</p>
<p>
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
</p>
<div>
<h3>Size</h3>
<div>
<div style="display: inline-block; margin-right: 16px;">
<label for="cols">Columns</label>
<input type="number" id="cols" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<label for="rows">Rows</label>
<input type="number" id="rows" />
</div>
</div>
</div>
</div>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<p><sup>*</sup> ZMODEM file transfers are supported via an addon. To try it out, install <a href="https://ohse.de/uwe/software/lrzsz.html"><code>lrzsz</code></a> onto the remote peer, then run <code>rz</code> to send from your browser or <code>sz &lt;file&gt;</code> to send from the remote peer.</p>
<script src="main.js" defer ></script>
</body>
</html>
+383
View File
@@ -0,0 +1,383 @@
"use strict";
var term,
protocol,
socketURL,
socket,
pid;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
var height = (rows * term.charMeasure.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.resize(cols, rows);
}
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
optionElements.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
createTerminal();
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({
cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
});
term.on('resize', function (size) {
if (!pid) {
return;
}
var cols = size.cols,
rows = size.rows,
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, {method: 'POST'});
});
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer);
term.fit();
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
term.zmodemAttach(socket, {
noTerminalWriteOutsideSession: true,
} );
term.on("zmodemRetract", () => {
start_form.style.display = "none";
start_form.onsubmit = null;
});
term.on("zmodemDetect", (detection) => {
function do_zmodem() {
term.detach();
let zsession = detection.confirm();
var promise;
if (zsession.type === "receive") {
promise = _handle_receive_session(zsession);
}
else {
promise = _handle_send_session(zsession);
}
promise.catch( console.error.bind(console) ).then( () => {
term.attach(socket);
} );
}
if (_auto_zmodem()) {
do_zmodem();
}
else {
start_form.style.display = "";
start_form.onsubmit = function(e) {
start_form.style.display = "none";
if (document.getElementById("zmstart_yes").checked) {
do_zmodem();
}
else {
detection.deny();
}
};
}
});
});
});
}, 0);
}
//----------------------------------------------------------------------
// UI STUFF
function _show_file_info(xfer) {
var file_info = xfer.get_details();
document.getElementById("name").textContent = file_info.name;
document.getElementById("size").textContent = file_info.size;
document.getElementById("mtime").textContent = file_info.mtime;
document.getElementById("files_remaining").textContent = file_info.files_remaining;
document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining;
document.getElementById("mode").textContent = "0" + file_info.mode.toString(8);
var xfer_opts = xfer.get_options();
["conversion", "management", "transport", "sparse"].forEach( (lbl) => {
document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl];
} );
document.getElementById("zm_file").style.display = "";
}
function _hide_file_info() {
document.getElementById("zm_file").style.display = "none";
}
function _save_to_disk(xfer, buffer) {
return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name);
}
var skipper_button = document.getElementById("zm_progress_skipper");
var skipper_button_orig_text = skipper_button.textContent;
function _show_progress() {
skipper_button.disabled = false;
skipper_button.textContent = skipper_button_orig_text;
document.getElementById("bytes_received").textContent = 0;
document.getElementById("percent_received").textContent = 0;
document.getElementById("zm_progress").style.display = "";
}
function _update_progress(xfer) {
var total_in = xfer.get_offset();
document.getElementById("bytes_received").textContent = total_in;
var percent_received = 100 * total_in / xfer.get_details().size;
document.getElementById("percent_received").textContent = percent_received.toFixed(2);
}
function _hide_progress() {
document.getElementById("zm_progress").style.display = "none";
}
var start_form = document.getElementById("zm_start");
function _auto_zmodem() {
return document.getElementById("zmodem-auto").checked;
}
// END UI STUFF
//----------------------------------------------------------------------
function _handle_receive_session(zsession) {
zsession.on("offer", function(xfer) {
current_receive_xfer = xfer;
_show_file_info(xfer);
var offer_form = document.getElementById("zm_offer");
function on_form_submit() {
offer_form.style.display = "none";
//START
//if (offer_form.zmaccept.value) {
if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) {
_show_progress();
var FILE_BUFFER = [];
xfer.on("input", (payload) => {
_update_progress(xfer);
FILE_BUFFER.push( new Uint8Array(payload) );
});
xfer.accept().then(
() => {
_save_to_disk(xfer, FILE_BUFFER);
},
console.error.bind(console)
);
}
else {
xfer.skip();
}
//END
}
if (_auto_zmodem()) {
on_form_submit();
}
else {
offer_form.onsubmit = on_form_submit;
offer_form.style.display = "";
}
} );
var promise = new Promise( (res) => {
zsession.on("session_end", () => {
_hide_file_info();
_hide_progress();
res();
} );
} );
zsession.start();
return promise;
}
function _handle_send_session(zsession) {
var choose_form = document.getElementById("zm_choose");
choose_form.style.display = "";
var file_el = document.getElementById("zm_files");
var promise = new Promise( (res) => {
file_el.onchange = function(e) {
choose_form.style.display = "none";
var files_obj = file_el.files;
Zmodem.Browser.send_files(
zsession,
files_obj,
{
on_offer_response(obj, xfer) {
if (xfer) _show_progress();
//console.log("offer", xfer ? "accepted" : "skipped");
},
on_progress(obj, xfer) {
_update_progress(xfer);
},
on_file_complete(obj) {
//console.log("COMPLETE", obj);
_hide_progress();
},
}
).then(_hide_progress).then(
zsession.close.bind(zsession),
console.error.bind(console)
).then( () => {
_hide_file_info();
_hide_progress();
res();
} );
};
} );
return promise;
}
//This is here to allow canceling of an in-progress ZMODEM transfer.
var current_receive_xfer;
//Called from HTML directly.
function skip_current_file() {
current_receive_xfer.skip();
skipper_button.disabled = true;
skipper_button.textContent = "Waiting for server to acknowledge skip …";
}
function runRealTerminal() {
term.attach(socket);
term._initialized = true;
}
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
var shellprompt = '$ ';
term.prompt = function () {
term.write('\r\n' + shellprompt);
};
term.writeln('Welcome to xterm.js');
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
term.writeln('Type some keys and commands to play around.');
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
if (ev.keyCode == 13) {
term.prompt();
} else if (ev.keyCode == 8) {
// Do not delete the prompt
if (term.x > 2) {
term.write('\b \b');
}
} else if (printable) {
term.write(key);
}
});
term.on('paste', function (data, ev) {
term.write(data);
});
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.zmodem",
"main": "zmodem.js",
"private": true
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
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);
assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object');
});
});
});
+90
View File
@@ -0,0 +1,90 @@
/**
*
* Allow xterm.js to handle ZMODEM uploads and downloads.
*
* This addon is a wrapper around zmodem.js. It adds the following to the
* Terminal class:
*
* - function `zmodemAttach(<WebSocket>, <Object>)` - creates a Zmodem.Sentry
* on the passed WebSocket object. The Object passed is optional and
* can contain:
* - noTerminalWriteOutsideSession: Suppress writes from the Sentry
* object to the Terminal while there is no active Session. This
* is necessary for compatibility with, for example, the
* `attach.js` addon.
*
* - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback.
* Passes the zmodem.js Detection object.
*
* - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback.
*
* You’ll need to provide logic to handle uploads and downloads.
* See zmodem.js’s documentation for more details.
*
* **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have
* used the `attach` or `terminado` addons, you’ll need to suspend their
* operation for the duration of the ZMODEM session. (The demo does this
* via `detach()` and a re-`attach()`.)
*/
let Zmodem;
export function zmodemAttach(term, ws, opts) {
if (!opts) opts = {};
var senderFunc = function _ws_sender_func(octets) {
ws.send(new Uint8Array(octets));
};
var zsentry;
function _shouldWrite() {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new Zmodem.Sentry( {
to_terminal: function _to_terminal(octets) {
if (_shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
}
},
sender: senderFunc,
on_retract: function _on_retract() {
term.emit('zmodemRetract');
},
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.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
term.write(evt.data);
}
}
else {
zsentry.consume(evt.data);
}
}
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', handleWSMessage);
}
export function apply(terminalConstructor) {
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
}
+8
View File
@@ -16,4 +16,12 @@ describe('evaluatePastedTextProcessing', () => {
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\rbar\r');
});
it('should bracket pasted text in bracketedPasteMode', () => {
const pastedText = 'foo bar';
const unbracketedText = Clipboard.bracketTextForPaste(pastedText, false);
const bracketedText = Clipboard.bracketTextForPaste(pastedText, true);
assert.equal(unbracketedText, 'foo bar');
assert.equal(bracketedText, '\x1b[200~foo bar\x1b[201~');
});
});
+12
View File
@@ -25,6 +25,17 @@ export function prepareTextForTerminal(text: string, isMSWindows: boolean): stri
return text;
}
/**
* Bracket text for paste, if necessary, as per https://cirw.in/blog/bracketed-paste
* @param text The pasted text to bracket
*/
export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string {
if (bracketedPasteMode) {
return '\x1b[200~' + text + '\x1b[201~';
}
return text;
}
/**
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
@@ -52,6 +63,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
let dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
text = bracketTextForPaste(text, term.bracketedPasteMode);
term.handler(text);
term.textarea.value = '';
term.emit('paste', text);
+65 -53
View File
@@ -52,6 +52,7 @@ export class Renderer extends EventEmitter implements IRenderer {
actualCellHeight: null
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
}
public onWindowResize(devicePixelRatio: number): void {
@@ -78,59 +79,8 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
@@ -218,4 +168,66 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
// Perform a new measure if the CharMeasure dimensions are not yet available
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
}
}
+5 -28
View File
@@ -25,41 +25,18 @@ describe('CharMeasure', () => {
});
describe('measure', () => {
it('should set _measureElement on first call', () => {
charMeasure.measure({});
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
it('should have _measureElement', () => {
assert.isDefined((<any>charMeasure)._measureElement, 'new CharMeasure() should have created _measureElement');
});
it('should be performed async on first call', done => {
assert.equal(charMeasure.width, null);
charMeasure.measure({});
it('should be performed sync', () => {
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
};
assert.equal(charMeasure.width, null);
setTimeout(() => {
assert.equal(charMeasure.width, 1);
done();
}, 0);
});
it('should be performed sync on successive calls', done => {
charMeasure.measure({});
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
};
setTimeout(() => {
const firstWidth = charMeasure.width;
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 2, height: 2 };
};
charMeasure.measure({});
assert.equal(charMeasure.width, firstWidth * 2);
done();
}, 0);
assert.equal(charMeasure.height, 1);
assert.equal(charMeasure.width, 1);
});
it('should NOT do a measure when the parent is hidden', done => {
+9 -18
View File
@@ -22,6 +22,14 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
super();
this._document = document;
this._parentElement = parentElement;
this._measureElement = this._document.createElement('span');
this._measureElement.style.position = 'absolute';
this._measureElement.style.top = '0';
this._measureElement.style.left = '-9999em';
this._measureElement.style.lineHeight = 'normal';
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
}
public get width(): number {
@@ -33,24 +41,6 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
}
public measure(options: ITerminalOptions): void {
if (!this._measureElement) {
this._measureElement = this._document.createElement('span');
this._measureElement.style.position = 'absolute';
this._measureElement.style.top = '0';
this._measureElement.style.left = '-9999em';
this._measureElement.style.lineHeight = 'normal';
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
// Perform _doMeasure async if the element was just attached as sometimes
// getBoundingClientRect does not return accurate values without this.
setTimeout(() => this._doMeasure(options), 0);
} else {
this._doMeasure(options);
}
}
private _doMeasure(options: ITerminalOptions): void {
this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontSize = `${options.fontSize}px`;
const geometry = this._measureElement.getBoundingClientRect();
@@ -65,4 +55,5 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
this.emit('charsizechanged');
}
}
}
+2
View File
@@ -10,6 +10,7 @@ import * as Browser from './Browser';
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces';
export class MockTerminal implements ITerminal {
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
linkifier: ILinkifier;
@@ -94,6 +95,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
+1
View File
@@ -95,6 +95,7 @@
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
}
.xterm canvas {