Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee88fca8c0 | ||
|
|
9b3fbc9b84 | ||
|
|
187f072f43 | ||
|
|
3ab2376517 | ||
|
|
a73fe62b7a | ||
|
|
350f6cfea1 | ||
|
|
2936f80013 | ||
|
|
dec9f6d254 | ||
|
|
bd6676d3b6 | ||
|
|
d9309d33af | ||
|
|
bb9802e754 | ||
|
|
eec804dd7c | ||
|
|
ff6d3a0768 | ||
|
|
94d37bbe4c | ||
|
|
36136a4c29 | ||
|
|
8b273e6939 | ||
|
|
2340fa1c91 | ||
|
|
70051da2ab | ||
|
|
5659bf3283 | ||
|
|
8de8ac10d4 | ||
|
|
46c93f1b63 | ||
|
|
e3a9d0d14f | ||
|
|
0a4b403f6a | ||
|
|
b94b4f5353 | ||
|
|
d84576e160 | ||
|
|
a0903fbf9b | ||
|
|
0b09f0bafd | ||
|
|
54bb0ee189 | ||
|
|
079702282c | ||
|
|
a2480bae51 | ||
|
|
cd501f9dcc | ||
|
|
354f3e03a2 | ||
|
|
00ef60ebaf | ||
|
|
5200766617 | ||
|
|
ee2c5969fa | ||
|
|
eb5f964bcb | ||
|
|
47be9a2df4 | ||
|
|
aee1629c25 | ||
|
|
8587eea54c | ||
|
|
d7ed109f9d | ||
|
|
84dfe8fccc | ||
|
|
6904dc5009 | ||
|
|
b89227d365 | ||
|
|
6c01630090 | ||
|
|
10282e4cb1 | ||
|
|
8518b354a7 | ||
|
|
00a8b80ef7 | ||
|
|
881a4a0dbb | ||
|
|
edee1a1067 |
@@ -171,6 +171,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
|||||||
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services.
|
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services.
|
||||||
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
|
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
|
||||||
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
|
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
|
||||||
|
- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH.
|
||||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||||
|
|
||||||
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. Note: Please add any new contributions to the end of the list only.
|
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. Note: Please add any new contributions to the end of the list only.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-serialize",
|
"name": "xterm-addon-serialize",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ abstract class BaseSerializeHandler {
|
|||||||
const cell2 = this._buffer.getNullCell();
|
const cell2 = this._buffer.getNullCell();
|
||||||
let oldCell = cell1;
|
let oldCell = cell1;
|
||||||
|
|
||||||
this._beforeSerialize(endRow - startRow);
|
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||||
|
|
||||||
for (let row = startRow; row < endRow; row++) {
|
for (let row = startRow; row < endRow; row++) {
|
||||||
const line = this._buffer.getLine(row);
|
const line = this._buffer.getLine(row);
|
||||||
@@ -36,7 +36,7 @@ abstract class BaseSerializeHandler {
|
|||||||
oldCell = c;
|
oldCell = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._rowEnd(row);
|
this._rowEnd(row, row === endRow - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._afterSerialize();
|
this._afterSerialize();
|
||||||
@@ -45,8 +45,8 @@ abstract class BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||||
protected _rowEnd(row: number): void { }
|
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||||
protected _beforeSerialize(rows: number): void { }
|
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||||
protected _afterSerialize(): void { }
|
protected _afterSerialize(): void { }
|
||||||
protected _serializeString(): string { return ''; }
|
protected _serializeString(): string { return ''; }
|
||||||
}
|
}
|
||||||
@@ -71,27 +71,152 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
|||||||
&& cell1.isDim() === cell2.isDim();
|
&& cell1.isDim() === cell2.isDim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class StringSerializeHandler extends BaseSerializeHandler {
|
class StringSerializeHandler extends BaseSerializeHandler {
|
||||||
private _rowIndex: number = 0;
|
private _rowIndex: number = 0;
|
||||||
private _allRows: string[] = new Array<string>();
|
private _allRows: string[] = new Array<string>();
|
||||||
|
private _allRowSeparators: string[] = new Array<string>();
|
||||||
private _currentRow: string = '';
|
private _currentRow: string = '';
|
||||||
private _nullCellCount: number = 0;
|
private _nullCellCount: number = 0;
|
||||||
|
|
||||||
constructor(buffer: IBuffer) {
|
// we can see a full colored cell and a null cell that only have background the same style
|
||||||
super(buffer);
|
// but the information isn't preserved by null cell itself
|
||||||
|
// so wee need to record it when required.
|
||||||
|
private _cursorStyle: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
|
||||||
|
// where exact the cursor styles comes from
|
||||||
|
// because we can't copy the cell directly
|
||||||
|
// so we remember where the content comes from instead
|
||||||
|
private _cursorStyleRow: number = 0;
|
||||||
|
private _cursorStyleCol: number = 0;
|
||||||
|
|
||||||
|
// this is a null cell for reference for checking whether background is empty or not
|
||||||
|
private _backgroundCell: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
|
||||||
|
private _firstRow: number = 0;
|
||||||
|
private _lastCursorRow: number = 0;
|
||||||
|
private _lastCursorCol: number = 0;
|
||||||
|
private _lastContentCursorRow: number = 0;
|
||||||
|
private _lastContentCursorCol: number = 0;
|
||||||
|
|
||||||
|
constructor(private _buffer1: IBuffer, private _terminal: Terminal) {
|
||||||
|
super(_buffer1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _beforeSerialize(rows: number): void {
|
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||||
this._allRows = new Array<string>(rows);
|
this._allRows = new Array<string>(rows);
|
||||||
|
this._lastContentCursorRow = start;
|
||||||
|
this._lastCursorRow = start;
|
||||||
|
this._firstRow = start;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _rowEnd(row: number): void {
|
private _thisRowLastChar: IBufferCell = this._buffer1.getNullCell();
|
||||||
this._allRows[this._rowIndex++] = this._currentRow;
|
private _thisRowLastSecondChar: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
private _nextRowFirstChar: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||||
|
// if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing
|
||||||
|
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||||
|
// use clear right to set background.
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowSeparator = '';
|
||||||
|
|
||||||
|
// handle row separator
|
||||||
|
if (!isLastRow) {
|
||||||
|
// Enable BCE
|
||||||
|
if (row - this._firstRow >= this._terminal.rows) {
|
||||||
|
this._buffer1.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current line
|
||||||
|
const currentLine = this._buffer1.getLine(row)!;
|
||||||
|
// Fetch next line
|
||||||
|
const nextLine = this._buffer1.getLine(row + 1)!;
|
||||||
|
|
||||||
|
if (!nextLine.isWrapped) {
|
||||||
|
// just insert the line break
|
||||||
|
rowSeparator = '\r\n';
|
||||||
|
// we sended the enter
|
||||||
|
this._lastCursorRow = row + 1;
|
||||||
|
this._lastCursorCol = 0;
|
||||||
|
} else {
|
||||||
|
rowSeparator = '';
|
||||||
|
const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;
|
||||||
|
const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;
|
||||||
|
const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;
|
||||||
|
const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;
|
||||||
|
|
||||||
|
// validate whether this line wrap is ever possible
|
||||||
|
// which mean whether cursor can placed at a overflow position (x === row) naturally
|
||||||
|
let isValid = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
// you must output character to cause overflow, control sequence can't do this
|
||||||
|
nextRowFirstChar.getChars() &&
|
||||||
|
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
// the last character can't be null,
|
||||||
|
// you can't use control sequence to move cursor to (x === row)
|
||||||
|
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||||
|
// change background of the first wrapped cell also affects BCE
|
||||||
|
// so we mark it as invalid to simply the process to determine line separator
|
||||||
|
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||||
|
) {
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// the second to last character can't be null if the next line starts with CJK,
|
||||||
|
// you can't use control sequence to move cursor to (x === row)
|
||||||
|
isNextRowFirstCharDoubleWidth &&
|
||||||
|
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||||
|
// change background of the first wrapped cell also affects BCE
|
||||||
|
// so we mark it as invalid to simply the process to determine line separator
|
||||||
|
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||||
|
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||||
|
) {
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
// force the wrap with magic
|
||||||
|
// insert enough character to force the wrap
|
||||||
|
rowSeparator = '-'.repeat(this._nullCellCount + 1);
|
||||||
|
// move back and erase next line head
|
||||||
|
rowSeparator += '\x1b[1D\x1b[1X';
|
||||||
|
|
||||||
|
if (this._nullCellCount > 0) {
|
||||||
|
// do these because we filled the last several null slot, which we shouldn't
|
||||||
|
rowSeparator += '\x1b[A';
|
||||||
|
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}C`;
|
||||||
|
rowSeparator += `\x1b[${this._nullCellCount}X`;
|
||||||
|
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`;
|
||||||
|
rowSeparator += '\x1b[B';
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is content and need the be serialized even it is invisible.
|
||||||
|
// without this, wrap will be missing from outputs.
|
||||||
|
this._lastContentCursorRow = row + 1;
|
||||||
|
this._lastContentCursorCol = 0;
|
||||||
|
|
||||||
|
// force commit the cursor position
|
||||||
|
this._lastCursorRow = row + 1;
|
||||||
|
this._lastCursorCol = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._allRows[this._rowIndex] = this._currentRow;
|
||||||
|
this._allRowSeparators[this._rowIndex++] = rowSeparator;
|
||||||
this._currentRow = '';
|
this._currentRow = '';
|
||||||
this._nullCellCount = 0;
|
this._nullCellCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
|
||||||
const sgrSeq: number[] = [];
|
const sgrSeq: number[] = [];
|
||||||
const fgChanged = !equalFg(cell, oldCell);
|
const fgChanged = !equalFg(cell, oldCell);
|
||||||
const bgChanged = !equalBg(cell, oldCell);
|
const bgChanged = !equalBg(cell, oldCell);
|
||||||
@@ -99,7 +224,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
|
|
||||||
if (fgChanged || bgChanged || flagsChanged) {
|
if (fgChanged || bgChanged || flagsChanged) {
|
||||||
if (cell.isAttributeDefault()) {
|
if (cell.isAttributeDefault()) {
|
||||||
this._currentRow += '\x1b[0m';
|
if (!oldCell.isAttributeDefault()) {
|
||||||
|
sgrSeq.push(0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (fgChanged) {
|
if (fgChanged) {
|
||||||
const color = cell.getFgColor();
|
const color = cell.getFgColor();
|
||||||
@@ -131,30 +258,129 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sgrSeq.length) {
|
return sgrSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||||
|
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||||
|
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||||
|
|
||||||
|
if (isPlaceHolderCell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this cell don't have content
|
||||||
|
const isEmptyCell = cell.getChars() === '';
|
||||||
|
|
||||||
|
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
|
||||||
|
|
||||||
|
// the empty cell style is only assumed to be changed when background changed, because foreground is always 0.
|
||||||
|
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles style change
|
||||||
|
*/
|
||||||
|
if (styleChanged) {
|
||||||
|
// before update the style, we need to fill empty cell back
|
||||||
|
if (this._nullCellCount > 0) {
|
||||||
|
// use clear right to set background.
|
||||||
|
if (!equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||||
|
}
|
||||||
|
// use move right to move cursor.
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||||
|
this._nullCellCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||||
|
this._lastContentCursorCol = this._lastCursorCol = col;
|
||||||
|
|
||||||
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
|
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
|
||||||
|
|
||||||
|
// update the last cursor style
|
||||||
|
const line = this._buffer1.getLine(row);
|
||||||
|
if (line !== undefined) {
|
||||||
|
line.getCell(col, this._cursorStyle);
|
||||||
|
this._cursorStyleRow = row;
|
||||||
|
this._cursorStyleCol = col;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count number of null cells encountered after the last non-null cell and move the cursor
|
/**
|
||||||
// if a non-null cell is found (eg. \t or cursor move)
|
* handles actual content
|
||||||
if (cell.getChars() === '') {
|
*/
|
||||||
|
if (isEmptyCell) {
|
||||||
this._nullCellCount += cell.getWidth();
|
this._nullCellCount += cell.getWidth();
|
||||||
} else if (this._nullCellCount > 0) {
|
} else {
|
||||||
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
if (this._nullCellCount > 0) {
|
||||||
this._nullCellCount = 0;
|
// we can just assume we have same style with previous one here
|
||||||
}
|
// because style change is handled by previous stage
|
||||||
|
// use move right when background is empty, use clear right when there is background.
|
||||||
|
if (equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||||
|
} else {
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||||
|
}
|
||||||
|
this._nullCellCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
this._currentRow += cell.getChars();
|
this._currentRow += cell.getChars();
|
||||||
|
|
||||||
|
// update cursor
|
||||||
|
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||||
|
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _serializeString(): string {
|
protected _serializeString(): string {
|
||||||
let rowEnd = this._allRows.length;
|
let rowEnd = this._allRows.length;
|
||||||
for (; rowEnd > 0; rowEnd--) {
|
|
||||||
if (this._allRows[rowEnd - 1]) {
|
// the fixup is only required for data without scrollback
|
||||||
break;
|
// because it will always be placed at last line otherwise
|
||||||
|
if (this._buffer1.length - this._firstRow <= this._terminal.rows) {
|
||||||
|
rowEnd = this._lastContentCursorRow + 1 - this._firstRow;
|
||||||
|
this._lastCursorCol = this._lastContentCursorCol;
|
||||||
|
this._lastCursorRow = this._lastContentCursorRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < rowEnd; i++) {
|
||||||
|
content += this._allRows[i];
|
||||||
|
if (i + 1 < rowEnd) {
|
||||||
|
content += this._allRowSeparators[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this._allRows.slice(0, rowEnd).join('\r\n');
|
|
||||||
|
// restore the cursor
|
||||||
|
const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY;
|
||||||
|
const realCursorCol = this._buffer1.cursorX;
|
||||||
|
|
||||||
|
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||||
|
|
||||||
|
const moveRight = (offset: number): void => {
|
||||||
|
if (offset > 0) {
|
||||||
|
content += `\u001b[${offset}C`;
|
||||||
|
} else if (offset < 0) {
|
||||||
|
content += `\u001b[${-offset}D`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const moveDown = (offset: number): void => {
|
||||||
|
if (offset > 0) {
|
||||||
|
content += `\u001b[${offset}B`;
|
||||||
|
} else if (offset < 0) {
|
||||||
|
content += `\u001b[${-offset}A`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cursorMoved) {
|
||||||
|
moveDown(realCursorRow - this._lastCursorRow);
|
||||||
|
moveRight(realCursorCol - this._lastCursorCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,20 +393,33 @@ export class SerializeAddon implements ITerminalAddon {
|
|||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public serialize(rows?: number): string {
|
private _getString(buffer: IBuffer, scrollback?: number): string {
|
||||||
// TODO: Add re-position cursor support
|
const maxRows = buffer.length;
|
||||||
// TODO: Add word wrap mode support
|
const handler = new StringSerializeHandler(buffer, this._terminal!);
|
||||||
|
|
||||||
|
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + this!._terminal!.rows, 0, maxRows);
|
||||||
|
const result = handler.serialize(maxRows - correctRows, maxRows);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public serialize(scrollback?: number): string {
|
||||||
// TODO: Add combinedData support
|
// TODO: Add combinedData support
|
||||||
if (!this._terminal) {
|
if (!this._terminal) {
|
||||||
throw new Error('Cannot use addon until it has been loaded');
|
throw new Error('Cannot use addon until it has been loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxRows = this._terminal.buffer.active.length;
|
if (this._terminal.buffer.active.type === 'normal') {
|
||||||
const handler = new StringSerializeHandler(this._terminal.buffer.active);
|
return this._getString(this._terminal.buffer.active, scrollback);
|
||||||
|
}
|
||||||
|
|
||||||
rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows);
|
const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback);
|
||||||
|
// alt screen don't have scrollback
|
||||||
|
const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined);
|
||||||
|
|
||||||
return handler.serialize(maxRows - rows, maxRows);
|
return normalScreenContent
|
||||||
|
+ '\u001b[?1049h\u001b[H'
|
||||||
|
+ alternativeScreenContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void { }
|
public dispose(): void { }
|
||||||
|
|||||||
@@ -14,6 +14,22 @@ let page: Page;
|
|||||||
const width = 800;
|
const width = 800;
|
||||||
const height = 600;
|
const height = 600;
|
||||||
|
|
||||||
|
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, '\' +' + JSON.stringify(str) + '+ \'');
|
||||||
|
|
||||||
|
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
|
||||||
|
await writeRawSync(page, str);
|
||||||
|
const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
|
||||||
|
await page.evaluate(`term.reset();`);
|
||||||
|
await writeRawSync(page, result);
|
||||||
|
const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
// chai decides -0 and 0 are different number...
|
||||||
|
// and firefox have a bug that output -0 for unknown reason
|
||||||
|
assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer));
|
||||||
|
};
|
||||||
|
|
||||||
describe('SerializeAddon', () => {
|
describe('SerializeAddon', () => {
|
||||||
before(async function(): Promise<any> {
|
before(async function(): Promise<any> {
|
||||||
const browserType = getBrowserType();
|
const browserType = getBrowserType();
|
||||||
@@ -27,19 +43,74 @@ describe('SerializeAddon', () => {
|
|||||||
await page.evaluate(`
|
await page.evaluate(`
|
||||||
window.serializeAddon = new SerializeAddon();
|
window.serializeAddon = new SerializeAddon();
|
||||||
window.term.loadAddon(window.serializeAddon);
|
window.term.loadAddon(window.serializeAddon);
|
||||||
|
window.inspectBuffer = (buffer) => {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
// Do this intentionally to get content of underlining source
|
||||||
|
const bufferLine = buffer.getLine(i)._line;
|
||||||
|
lines.push(JSON.stringify(bufferLine));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: buffer.cursorX,
|
||||||
|
y: buffer.cursorY,
|
||||||
|
data: lines
|
||||||
|
};
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => await browser.close());
|
after(async () => await browser.close());
|
||||||
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
||||||
|
|
||||||
|
it('produce different output when we call test util with different text', async function(): Promise<any> {
|
||||||
|
await writeRawSync(page, '12345');
|
||||||
|
const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
await page.evaluate(`term.reset();`);
|
||||||
|
await writeRawSync(page, '67890');
|
||||||
|
const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
assert.throw(() => {
|
||||||
|
assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produce different output when we call test util with different line wrap', async function(): Promise<any> {
|
||||||
|
await writeRawSync(page, '1234567890\r\n12345');
|
||||||
|
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
await page.evaluate(`term.reset();`);
|
||||||
|
await writeRawSync(page, '1234567890n12345');
|
||||||
|
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
assert.throw(() => {
|
||||||
|
assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('empty content', async function(): Promise<any> {
|
it('empty content', async function(): Promise<any> {
|
||||||
const rows = 10;
|
const rows = 10;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('trim last empty lines', async function(): Promise<any> {
|
it('unwrap wrapped line', async function(): Promise<any> {
|
||||||
|
const lines = ['123456789123456789'];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not unwrap non-wrapped line', async function(): Promise<any> {
|
||||||
|
const lines = [
|
||||||
|
'123456789',
|
||||||
|
'123456789'
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('preserve last empty lines', async function(): Promise<any> {
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
const lines = [
|
const lines = [
|
||||||
'',
|
'',
|
||||||
@@ -55,7 +126,7 @@ describe('SerializeAddon', () => {
|
|||||||
''
|
''
|
||||||
];
|
];
|
||||||
await writeSync(page, lines.join('\\r\\n'));
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n'));
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('digits content', async function(): Promise<any> {
|
it('digits content', async function(): Promise<any> {
|
||||||
@@ -67,21 +138,22 @@ describe('SerializeAddon', () => {
|
|||||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serialize half rows of content', async function(): Promise<any> {
|
it('serialize with half of scrollback', async function(): Promise<any> {
|
||||||
const rows = 10;
|
const rows = 20;
|
||||||
const halfRows = rows >> 1;
|
const scrollback = rows - 10;
|
||||||
|
const halfScrollback = scrollback / 2;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||||
await writeSync(page, lines.join('\\r\\n'));
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n'));
|
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfScrollback});`), lines.slice(halfScrollback, rows).join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serialize 0 rows of content', async function(): Promise<any> {
|
it('serialize 0 rows of scrollback', async function(): Promise<any> {
|
||||||
const rows = 10;
|
const rows = 20;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||||
await writeSync(page, lines.join('\\r\\n'));
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), '');
|
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), lines.slice(rows - 10, rows).join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serialize all rows of content with color16', async function(): Promise<any> {
|
it('serialize all rows of content with color16', async function(): Promise<any> {
|
||||||
@@ -276,17 +348,15 @@ describe('SerializeAddon', () => {
|
|||||||
'中文中文',
|
'中文中文',
|
||||||
'12中文',
|
'12中文',
|
||||||
'中文12',
|
'中文12',
|
||||||
'1中文中文中' // this line is going to be wrapped at last character because it has line length of 11 (1+2*5)
|
// This line is going to be wrapped at last character
|
||||||
];
|
// because it has line length of 11 (1+2*5).
|
||||||
const expected = [
|
// We concat it back without the null cell currently.
|
||||||
'中文中文',
|
// But this may be incorrect.
|
||||||
'12中文',
|
// see also #3097
|
||||||
'中文12',
|
'1中文中文中'
|
||||||
'1中文中文',
|
|
||||||
'中'
|
|
||||||
];
|
];
|
||||||
await writeSync(page, lines.join('\\r\\n'));
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serialize CJK Mixed with tab correctly', async () => {
|
it('serialize CJK Mixed with tab correctly', async () => {
|
||||||
@@ -299,6 +369,118 @@ describe('SerializeAddon', () => {
|
|||||||
await writeSync(page, lines.join('\\r\\n'));
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('serialize with alt screen correctly', async () => {
|
||||||
|
const SMCUP = '\u001b[?1049h';
|
||||||
|
const CUP = '\u001b[H';
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1${SMCUP}${CUP}2`
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
`1${SMCUP}${CUP}2`
|
||||||
|
];
|
||||||
|
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate');
|
||||||
|
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize without alt screen correctly', async () => {
|
||||||
|
const SMCUP = '\u001b[?1049h';
|
||||||
|
const RMCUP = '\u001b[?1049l';
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1${SMCUP}2${RMCUP}`
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
`1`
|
||||||
|
];
|
||||||
|
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal');
|
||||||
|
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize with background', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1\u001b[44m${CLEAR_RIGHT(5)}`,
|
||||||
|
`2${CLEAR_RIGHT(9)}`
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cause the BCE on scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
|
||||||
|
const padLines = newArray<string>(
|
||||||
|
(index: number) => digitsString(10, index),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
...padLines,
|
||||||
|
`\u001b[44m${CLEAR_RIGHT(5)}1111111111111111`
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle invalid wrap before scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||||
|
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||||
|
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||||
|
|
||||||
|
// A line wrap happened after current line.
|
||||||
|
// But there is no content.
|
||||||
|
// so wrap shouldn't even be able to happen.
|
||||||
|
const segments = [
|
||||||
|
`123456789012345`,
|
||||||
|
MOVE_UP(1),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_DOWN(1),
|
||||||
|
MOVE_LEFT(5),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_UP(1),
|
||||||
|
'1'
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, segments.join(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle invalid wrap after scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||||
|
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||||
|
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||||
|
|
||||||
|
const padLines = newArray<string>(
|
||||||
|
(index: number) => digitsString(10, index),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
// A line wrap happened after current line.
|
||||||
|
// But there is no content.
|
||||||
|
// so wrap shouldn't even be able to happen.
|
||||||
|
const lines = [
|
||||||
|
padLines.join('\r\n'),
|
||||||
|
'\r\n',
|
||||||
|
`123456789012345`,
|
||||||
|
MOVE_UP(1),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_DOWN(1),
|
||||||
|
MOVE_LEFT(5),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_UP(1),
|
||||||
|
'1'
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join(''));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ declare module 'xterm-addon-serialize' {
|
|||||||
* to restore the state. The cursor will also be positioned to the correct cell.
|
* to restore the state. The cursor will also be positioned to the correct cell.
|
||||||
* When restoring a terminal it is best to do before `Terminal.open` is called
|
* When restoring a terminal it is best to do before `Terminal.open` is called
|
||||||
* to avoid wasting CPU cycles rendering incomplete frames.
|
* to avoid wasting CPU cycles rendering incomplete frames.
|
||||||
* @param rows The number of rows to serialize, starting from the bottom of the
|
* @param scrollback The number of rows in scrollback buffer to serialize, starting from the bottom of the
|
||||||
* terminal. This defaults to the number of rows in the viewport.
|
* scrollback buffer. This defaults to the all available rows in the scrollback buffer.
|
||||||
*/
|
*/
|
||||||
public serialize(rows?: number): string;
|
public serialize(scrollback?: number): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the addon.
|
* Disposes the addon.
|
||||||
|
|||||||
@@ -3,20 +3,40 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ILinkProvider, IBufferCellPosition, ILink, Terminal } from 'xterm';
|
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||||
|
|
||||||
|
interface ILinkProviderOptions {
|
||||||
|
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||||
|
leave?(event: MouseEvent, text: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export class WebLinkProvider implements ILinkProvider {
|
export class WebLinkProvider implements ILinkProvider {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _terminal: Terminal,
|
private readonly _terminal: Terminal,
|
||||||
private readonly _regex: RegExp,
|
private readonly _regex: RegExp,
|
||||||
private readonly _handler: (event: MouseEvent, uri: string) => void
|
private readonly _handler: (event: MouseEvent, uri: string) => void,
|
||||||
|
private readonly _options: ILinkProviderOptions = {}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
||||||
callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler));
|
const links = LinkComputer.computeLink(y, this._regex, this._terminal, this._handler);
|
||||||
|
callback(this._addCallbacks(links));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addCallbacks(links: ILink[]): ILink[] {
|
||||||
|
return links.map(link => {
|
||||||
|
link.leave = this._options.leave;
|
||||||
|
link.hover = (event: MouseEvent, uri: string): void => {
|
||||||
|
if (this._options.hover) {
|
||||||
|
const { range } = link;
|
||||||
|
this._options.hover(event, uri, range);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return link;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
|
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
|
||||||
import { WebLinkProvider } from './WebLinkProvider';
|
import { WebLinkProvider } from './WebLinkProvider';
|
||||||
|
|
||||||
const protocolClause = '(https?:\\/\\/)';
|
const protocolClause = '(https?:\\/\\/)';
|
||||||
@@ -36,6 +36,11 @@ function handleLink(event: MouseEvent, uri: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ILinkProviderOptions {
|
||||||
|
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||||
|
leave?(event: MouseEvent, text: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export class WebLinksAddon implements ITerminalAddon {
|
export class WebLinksAddon implements ITerminalAddon {
|
||||||
private _linkMatcherId: number | undefined;
|
private _linkMatcherId: number | undefined;
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
@@ -43,20 +48,22 @@ export class WebLinksAddon implements ITerminalAddon {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
|
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
|
||||||
private _options: ILinkMatcherOptions = {},
|
private _options: ILinkMatcherOptions | ILinkProviderOptions = {},
|
||||||
private _useLinkProvider: boolean = false
|
private _useLinkProvider: boolean = false
|
||||||
) {
|
) {
|
||||||
this._options.matchIndex = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
|
|
||||||
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
|
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
|
||||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler));
|
const options = this._options as ILinkProviderOptions;
|
||||||
|
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler, options));
|
||||||
} else {
|
} else {
|
||||||
// TODO: This should be removed eventually
|
// TODO: This should be removed eventually
|
||||||
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options);
|
const options = this._options as ILinkMatcherOptions;
|
||||||
|
options.matchIndex = 1;
|
||||||
|
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm';
|
||||||
|
|
||||||
declare module 'xterm-addon-web-links' {
|
declare module 'xterm-addon-web-links' {
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +20,7 @@ declare module 'xterm-addon-web-links' {
|
|||||||
* link provider (new) may cause issues. Link provider will eventually be
|
* link provider (new) may cause issues. Link provider will eventually be
|
||||||
* the default and only option.
|
* the default and only option.
|
||||||
*/
|
*/
|
||||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions, useLinkProvider?: boolean);
|
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the addon
|
* Activates the addon
|
||||||
@@ -33,4 +33,21 @@ declare module 'xterm-addon-web-links' {
|
|||||||
*/
|
*/
|
||||||
public dispose(): void;
|
public dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing options for a link provider.
|
||||||
|
*/
|
||||||
|
export interface ILinkProviderOptions {
|
||||||
|
/**
|
||||||
|
* A callback that fires when the mouse hovers over a link for a period of
|
||||||
|
* time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}).
|
||||||
|
*/
|
||||||
|
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that fires when the mouse leaves a link. Note that this can
|
||||||
|
* happen even when tooltipCallback hasn't fired for the link yet.
|
||||||
|
*/
|
||||||
|
leave?(event: MouseEvent, text: string): void;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-webgl",
|
"name": "xterm-addon-webgl",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
|
|||||||
@@ -116,12 +116,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
this._refreshCharAtlas();
|
this._refreshCharAtlas();
|
||||||
|
|
||||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
|
||||||
this._glyphRenderer.updateSelection(this._model);
|
|
||||||
|
|
||||||
// Force a full refresh
|
// Force a full refresh
|
||||||
this._model.clear();
|
this._model.clear();
|
||||||
this._model.clearSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDevicePixelRatioChange(): void {
|
public onDevicePixelRatioChange(): void {
|
||||||
@@ -138,7 +134,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
|
|
||||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||||
this._rectangleRenderer.onResize();
|
|
||||||
|
|
||||||
// Resize all render layers
|
// Resize all render layers
|
||||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||||
@@ -152,6 +147,13 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// Resize the screen
|
// Resize the screen
|
||||||
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||||
|
|
||||||
|
this._rectangleRenderer.onResize();
|
||||||
|
if (this._model.selection.hasSelection) {
|
||||||
|
// Update selection as dimensions have changed
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
|
}
|
||||||
|
|
||||||
this._glyphRenderer.setDimensions(this.dimensions);
|
this._glyphRenderer.setDimensions(this.dimensions);
|
||||||
this._glyphRenderer.onResize();
|
this._glyphRenderer.onResize();
|
||||||
|
|
||||||
@@ -159,7 +161,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
// Force a full refresh
|
// Force a full refresh
|
||||||
this._model.clear();
|
this._model.clear();
|
||||||
this._model.clearSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCharSizeChanged(): void {
|
public onCharSizeChanged(): void {
|
||||||
@@ -179,9 +180,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
this._updateSelectionModel(start, end, columnSelectMode);
|
this._updateSelectionModel(start, end, columnSelectMode);
|
||||||
|
|
||||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
|
||||||
this._glyphRenderer.updateSelection(this._model);
|
|
||||||
|
|
||||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +218,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._charAtlas?.clearTexture();
|
this._charAtlas?.clearTexture();
|
||||||
this._model.clear();
|
this._model.clear();
|
||||||
this._updateModel(0, this._terminal.rows - 1);
|
this._updateModel(0, this._terminal.rows - 1);
|
||||||
this._glyphRenderer.updateSelection(this._model);
|
|
||||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +250,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// Tell renderer the frame is beginning
|
// Tell renderer the frame is beginning
|
||||||
if (this._glyphRenderer.beginFrame()) {
|
if (this._glyphRenderer.beginFrame()) {
|
||||||
this._model.clear();
|
this._model.clear();
|
||||||
this._model.clearSelection();
|
this._updateSelectionModel(undefined, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update model to reflect what's drawn
|
// Update model to reflect what's drawn
|
||||||
@@ -303,14 +300,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||||
|
if (this._model.selection.hasSelection) {
|
||||||
|
// Model could be updated but the selection is unchanged
|
||||||
|
this._glyphRenderer.updateSelection(this._model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
|
|
||||||
// Selection does not exist
|
// Selection does not exist
|
||||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||||
this._model.clearSelection();
|
this._model.clearSelection();
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +325,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// No need to draw the selection
|
// No need to draw the selection
|
||||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||||
this._model.clearSelection();
|
this._model.clearSelection();
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +337,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||||
this._model.selection.startCol = start[0];
|
this._model.selection.startCol = start[0];
|
||||||
this._model.selection.endCol = end[0];
|
this._model.selection.endCol = end[0];
|
||||||
|
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"description": "Full xterm terminal, in your browser",
|
"description": "Full xterm terminal, in your browser",
|
||||||
"version": "4.10.0",
|
"version": "4.11.0",
|
||||||
"main": "lib/xterm.js",
|
"main": "lib/xterm.js",
|
||||||
"style": "css/xterm.css",
|
"style": "css/xterm.css",
|
||||||
"types": "typings/xterm.d.ts",
|
"types": "typings/xterm.d.ts",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "webpack",
|
"package": "webpack",
|
||||||
"start": "node demo/start",
|
"start": "node demo/start",
|
||||||
|
"start-debug": "node --inspect-brk demo/start",
|
||||||
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
|
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
|
||||||
"test": "npm run test-unit",
|
"test": "npm run test-unit",
|
||||||
"posttest": "npm run lint",
|
"posttest": "npm run lint",
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
|||||||
throw new Error('Terminal requires a parent element.');
|
throw new Error('Terminal requires a parent element.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.body.contains(parent)) {
|
if (!parent.isConnected) {
|
||||||
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
|
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ export class RenderService extends Disposable implements IRenderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDevicePixelRatioChange(): void {
|
public onDevicePixelRatioChange(): void {
|
||||||
|
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
|
||||||
|
// when devicePixelRatio changes
|
||||||
|
this._charSizeService.measure();
|
||||||
|
|
||||||
this._renderer.onDevicePixelRatioChange();
|
this._renderer.onDevicePixelRatioChange();
|
||||||
this.refreshRows(0, this._rowCount - 1);
|
this.refreshRows(0, this._rowCount - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user