mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into ctrl-alt-sequences
This commit is contained in:
@@ -11,6 +11,7 @@ before_install:
|
||||
- npm install -g npm@5.1.0
|
||||
env:
|
||||
matrix:
|
||||
- NPM_COMMAND=tsc
|
||||
- NPM_COMMAND=lint
|
||||
- NPM_COMMAND=test
|
||||
notifications:
|
||||
|
||||
@@ -125,6 +125,7 @@ computational environment for Jupyter, supporting interactive data science and s
|
||||
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
|
||||
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
|
||||
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
|
||||
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"lint": "tslint src/*.ts src/**/*.ts",
|
||||
"test": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"tsc": "tsc",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build",
|
||||
"coveralls": "gulp coveralls",
|
||||
|
||||
+4
-1
@@ -213,7 +213,10 @@ export class Buffer implements IBuffer {
|
||||
// needed here because some chars are 0 characters long (eg. after wide
|
||||
// chars) and some chars are longer than 1 characters long (eg. emojis).
|
||||
let startIndex = startCol;
|
||||
endCol = endCol || line.length;
|
||||
// Only set endCol to the line length when it is null. 0 is a valid column.
|
||||
if (endCol === null) {
|
||||
endCol = line.length;
|
||||
}
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
|
||||
+22
-12
@@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
let min = 0;
|
||||
let max = data.length - 1;
|
||||
let mid;
|
||||
if (ucs < data[0][0] || ucs > data[max][1])
|
||||
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||
return false;
|
||||
}
|
||||
while (max >= min) {
|
||||
mid = (min + max) >> 1;
|
||||
if (ucs > data[mid][1])
|
||||
if (ucs > data[mid][1]) {
|
||||
min = mid + 1;
|
||||
else if (ucs < data[mid][0])
|
||||
} else if (ucs < data[mid][0]) {
|
||||
max = mid - 1;
|
||||
else
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidthBMP(ucs: number): number {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0)
|
||||
if (ucs === 0) {
|
||||
return opts.nul;
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
}
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
||||
return opts.control;
|
||||
}
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs, COMBINING_BMP))
|
||||
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;
|
||||
@@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
||||
}
|
||||
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
||||
if (bisearch(ucs, COMBINING_HIGH))
|
||||
if (bisearch(ucs, COMBINING_HIGH)) {
|
||||
return 0;
|
||||
}
|
||||
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||
return 2;
|
||||
}
|
||||
@@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
||||
let num = 0;
|
||||
let pos = CODEPOINTS_PER_ITEM;
|
||||
while (pos--)
|
||||
while (pos--) {
|
||||
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
||||
}
|
||||
table[i] = num;
|
||||
}
|
||||
return table;
|
||||
@@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
||||
return function (num: number): number {
|
||||
num = num | 0; // get asm.js like optimization under V8
|
||||
if (num < 32)
|
||||
if (num < 32) {
|
||||
return control | 0;
|
||||
if (num < 127)
|
||||
}
|
||||
if (num < 127) {
|
||||
return 1;
|
||||
}
|
||||
let t = table || init_table();
|
||||
if (num < 65536)
|
||||
if (num < 65536) {
|
||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||
}
|
||||
// do a full search for high codepoints
|
||||
return wcwidthHigh(num);
|
||||
};
|
||||
|
||||
+2
-1
@@ -69,8 +69,9 @@ export class InputHandler implements IInputHandler {
|
||||
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
|
||||
}
|
||||
} else {
|
||||
if (chWidth === 2) // FIXME: check for xterm behavior
|
||||
if (chWidth === 2) { // FIXME: check for xterm behavior
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
+2
-1
@@ -224,8 +224,9 @@ export class Parser {
|
||||
ch += data.charAt(this._position + 1);
|
||||
}
|
||||
// surrogate low - already handled above
|
||||
if (0xDC00 <= code && code <= 0xDFFF)
|
||||
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case ParserState.NORMAL:
|
||||
|
||||
+16
-1
@@ -11,6 +11,7 @@ import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -28,6 +29,12 @@ const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* The maximum amount of time that can have elapsed for an alt click to move the
|
||||
* cursor.
|
||||
*/
|
||||
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
@@ -96,6 +103,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: (...args: any[]) => void;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _charMeasure: CharMeasure
|
||||
@@ -317,6 +326,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
this._mouseDownTimeStamp = event.timeStamp;
|
||||
// If we have selection, we want the context menu on right click even if the
|
||||
// terminal is in mouse mode.
|
||||
if (event.button === 2 && this.hasSelection) {
|
||||
@@ -536,10 +546,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent): void {
|
||||
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
|
||||
if (this.hasSelection)
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
||||
(new AltClickHandler(event, this._terminal)).move();
|
||||
} else if (this.hasSelection) {
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
}
|
||||
|
||||
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
|
||||
|
||||
+8
-6
@@ -1609,21 +1609,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
break;
|
||||
case 36:
|
||||
// home
|
||||
if (modifiers)
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
|
||||
else if (this.applicationCursor)
|
||||
} else if (this.applicationCursor) {
|
||||
result.key = C0.ESC + 'OH';
|
||||
else
|
||||
} else {
|
||||
result.key = C0.ESC + '[H';
|
||||
}
|
||||
break;
|
||||
case 35:
|
||||
// end
|
||||
if (modifiers)
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
|
||||
else if (this.applicationCursor)
|
||||
} else if (this.applicationCursor) {
|
||||
result.key = C0.ESC + 'OF';
|
||||
else
|
||||
} else {
|
||||
result.key = C0.ESC + '[F';
|
||||
}
|
||||
break;
|
||||
case 33:
|
||||
// page up
|
||||
|
||||
@@ -188,12 +188,14 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
isFocused: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
bracketedPasteMode: boolean;
|
||||
applicationCursor: boolean;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
handler(data: string): void;
|
||||
send(data: string): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
log(text: string): void;
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
||||
import { C0 } from '../EscapeSequences';
|
||||
|
||||
enum Direction {
|
||||
Up = 'A',
|
||||
Down = 'B',
|
||||
Right = 'C',
|
||||
Left = 'D'
|
||||
}
|
||||
|
||||
export class AltClickHandler {
|
||||
private _startRow: number;
|
||||
private _startCol: number;
|
||||
private _endRow: number;
|
||||
private _endCol: number;
|
||||
private _lines: ICircularList<LineData>;
|
||||
|
||||
constructor(
|
||||
private _mouseEvent: MouseEvent,
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this._lines = this._terminal.buffer.lines;
|
||||
this._startCol = this._terminal.buffer.x;
|
||||
this._startRow = this._terminal.buffer.y;
|
||||
|
||||
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
|
||||
this._mouseEvent,
|
||||
this._terminal.element,
|
||||
this._terminal.charMeasure,
|
||||
this._terminal.options.lineHeight,
|
||||
this._terminal.cols,
|
||||
this._terminal.rows,
|
||||
false
|
||||
).map((coordinate: number) => {
|
||||
return coordinate - 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the escape sequences of arrows to the terminal
|
||||
*/
|
||||
public move(): void {
|
||||
if (this._mouseEvent.altKey) {
|
||||
this._terminal.send(this._arrowSequences());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates all the arrow sequences together.
|
||||
* Resets the starting row to an unwrapped row, moves to the requested row,
|
||||
* then moves to requested col.
|
||||
*/
|
||||
private _arrowSequences(): string {
|
||||
return this._resetStartingRow() +
|
||||
this._moveToRequestedRow() +
|
||||
this._moveToRequestedCol();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the initial position of the cursor is on a row that is wrapped, move the
|
||||
* cursor up to the first row that is not wrapped to have accurate vertical
|
||||
* positioning.
|
||||
*/
|
||||
private _resetStartingRow(): string {
|
||||
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
let endRow = this._endRow;
|
||||
|
||||
if (this._moveToRequestedRow().length === 0) {
|
||||
return '';
|
||||
} else {
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, this._startRow, this._startCol,
|
||||
this._startRow - this._wrappedRowsForRow(this._startRow), false
|
||||
).length, this._sequence(Direction.Left));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the reset starting and ending row, move to the requested row,
|
||||
* ignoring wrapped rows
|
||||
*/
|
||||
private _moveToRequestedRow(): string {
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
|
||||
|
||||
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the requested col on the ending row
|
||||
*/
|
||||
private _moveToRequestedCol(): string {
|
||||
let startRow;
|
||||
if (this._moveToRequestedRow().length > 0) {
|
||||
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
} else {
|
||||
startRow = this._startRow;
|
||||
}
|
||||
|
||||
let endRow = this._endRow;
|
||||
let direction = this._horizontalDirection();
|
||||
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, startRow, this._endCol, endRow,
|
||||
direction === Direction.Right
|
||||
).length, this._sequence(direction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows between the unwrapped starting and
|
||||
* ending rows. These rows need to ignored since the cursor skips over them.
|
||||
*/
|
||||
private _wrappedRowsCount(): number {
|
||||
let wrappedRows = 0;
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
let direction = this._verticalDirection() === Direction.Up ? -1 : 1;
|
||||
|
||||
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
|
||||
wrappedRows++;
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows that make up a given row.
|
||||
* @param currentRow The row to determine how many wrapped rows make it up
|
||||
*/
|
||||
private _wrappedRowsForRow(currentRow: number): number {
|
||||
let rowCount = 0;
|
||||
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
|
||||
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
|
||||
rowCount++;
|
||||
currentRow--;
|
||||
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direction determiners
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if the right or left arrow is needed
|
||||
*/
|
||||
private _horizontalDirection(): Direction {
|
||||
let startRow;
|
||||
if (this._moveToRequestedRow().length > 0) {
|
||||
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
} else {
|
||||
startRow = this._startRow;
|
||||
}
|
||||
|
||||
if ((this._startCol < this._endCol &&
|
||||
startRow <= this._endRow) || // down/right or same y/right
|
||||
(this._startCol >= this._endCol &&
|
||||
startRow < this._endRow)) { // down/left or same y/left
|
||||
return Direction.Right;
|
||||
} else {
|
||||
return Direction.Left;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the up or down arrow is needed
|
||||
*/
|
||||
private _verticalDirection(): Direction {
|
||||
if (this._startRow > this._endRow) {
|
||||
return Direction.Up;
|
||||
} else {
|
||||
return Direction.Down;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the string of chars in the buffer from a starting row and col
|
||||
* to an ending row and col
|
||||
* @param startCol The starting column position
|
||||
* @param startRow The starting row position
|
||||
* @param endCol The ending column position
|
||||
* @param endRow The ending row position
|
||||
* @param forward Direction to move
|
||||
*/
|
||||
private _bufferLine(
|
||||
startCol: number,
|
||||
startRow: number,
|
||||
endCol: number,
|
||||
endRow: number,
|
||||
forward: boolean
|
||||
): string {
|
||||
let currentCol = startCol;
|
||||
let currentRow = startRow;
|
||||
let bufferStr = '';
|
||||
|
||||
while (currentCol !== endCol || currentRow !== endRow) {
|
||||
currentCol += forward ? 1 : -1;
|
||||
|
||||
if (forward && currentCol > this._terminal.cols - 1) {
|
||||
bufferStr += this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
currentCol = 0;
|
||||
startCol = 0;
|
||||
currentRow++;
|
||||
} else if (!forward && currentCol < 0) {
|
||||
bufferStr += this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, 0, startCol + 1
|
||||
);
|
||||
currentCol = this._terminal.cols - 1;
|
||||
startCol = currentCol;
|
||||
currentRow--;
|
||||
}
|
||||
}
|
||||
|
||||
return bufferStr + this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the escape sequence for clicking an arrow
|
||||
* @param direction The direction to move
|
||||
*/
|
||||
private _sequence(direction: Direction): string {
|
||||
const mod = this._terminal.applicationCursor ? 'O' : '[';
|
||||
return C0.ESC + mod + direction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string repeated a given number of times
|
||||
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
|
||||
* @param count The number of times to repeat the string
|
||||
* @param string The string that is to be repeated
|
||||
*/
|
||||
function repeat(count: number, str: string): string {
|
||||
count = Math.floor(count);
|
||||
let rpt = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
rpt += str;
|
||||
}
|
||||
return rpt;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Closing preserves properties
|
||||
// Cloning preserves properties
|
||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
@@ -70,6 +70,9 @@ export class MockTerminal implements ITerminal {
|
||||
write(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
send(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
bracketedPasteMode: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
renderer: IRenderer;
|
||||
@@ -93,6 +96,7 @@ export class MockTerminal implements ITerminal {
|
||||
scrollback: number;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
applicationCursor: boolean;
|
||||
handler(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": [
|
||||
true,
|
||||
"ignore-same-line"
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
|
||||
Reference in New Issue
Block a user