diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts
index 24020b74..8a1c9659 100644
--- a/addons/xterm-addon-serialize/src/SerializeAddon.ts
+++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts
@@ -6,7 +6,7 @@
*/
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
-
+import { IColorSet } from 'browser/Types';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -406,6 +406,13 @@ export class SerializeAddon implements ITerminalAddon {
return handler.serialize(maxRows - correctRows, maxRows);
}
+ private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
+ const maxRows = buffer.length;
+ const handler = new HTMLSerializeHandler(buffer, terminal);
+ const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
+ return handler.serialize(maxRows - correctRows, maxRows);
+ }
+
private _serializeModes(terminal: Terminal): string {
let content = '';
const modes = terminal.modes;
@@ -460,6 +467,14 @@ export class SerializeAddon implements ITerminalAddon {
return content;
}
+ public htmlserialize(options?: IHtmlSerializeOptions): string {
+ if (!this._terminal) {
+ throw new Error('Cannot use addon until it has been loaded');
+ }
+
+ return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
+ }
+
public dispose(): void { }
}
@@ -469,3 +484,145 @@ interface ISerializeOptions {
excludeModes?: boolean;
excludeAltBuffer?: boolean;
}
+
+interface IHtmlSerializeOptions {
+ scrollback?: number;
+}
+
+export class HTMLSerializeHandler extends BaseSerializeHandler {
+ private _currentRow: string = '';
+
+ private _htmlContent = '';
+
+ private _inverseStyle = 'color: #000000; background-color: #BFBFBF;';
+ private _blinkStyle = 'text-decoration: blink;';
+ private _boldStyle = 'font-weight: bold;';
+ private _italicStyle = 'font-style: italic;';
+ private _underlineStyle = 'text-decoration: underline;';
+ private _strikethroughStyle = 'text-decoration: line-through;';
+ private _invisibleStyle = 'visibility: hidden;';
+ private _dimStyle = 'opacity: 0.5;';
+
+ private _colors: IColorSet;
+
+ constructor(
+ buffer: IBuffer,
+ private readonly _terminal: Terminal
+ ) {
+ super(buffer);
+
+ // https://github.com/xtermjs/xterm.js/issues/3601
+ this._colors = (_terminal as any)._core._colorManager.colors;
+ }
+
+ private _padStart(target: string, targetLength: number, padString: string): string {
+ targetLength = targetLength >> 0;
+ padString = String(typeof padString !== 'undefined' ? padString : ' ');
+ if (target.length > targetLength) {
+ return target;
+ }
+
+ targetLength = targetLength - target.length;
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length);
+ }
+ return padString.slice(0, targetLength) + target;
+ }
+
+ protected _beforeSerialize(rows: number, start: number, end: number): void {
+ this._htmlContent += '
'
+ + '';
+ // TODO: fetch options and remove hardcoded values
+ this._htmlContent += '';
+ }
+
+ protected _afterSerialize(): void {
+ this._htmlContent += '
';
+ this._htmlContent += '
';
+ }
+
+ protected _rowEnd(row: number, isLastRow: boolean): void {
+ this._htmlContent += '' + this._currentRow + '
';
+ this._currentRow = '';
+ }
+
+ private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {
+ const color = isFg ? cell.getFgColor() : cell.getBgColor();
+ if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
+ const rgb = [
+ color >>> 0xFF0000 & 255,
+ color >>> 0xFF00 & 255,
+ color & 255
+ ];
+ return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
+ }
+ if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
+ return this._colors.ansi[color].css;
+ }
+ return undefined;
+ }
+
+ private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
+ const content: string[] = [];
+
+ const fgChanged = !equalFg(cell, oldCell);
+ const bgChanged = !equalBg(cell, oldCell);
+ const flagsChanged = !equalFlags(cell, oldCell);
+
+ if (fgChanged || bgChanged || flagsChanged) {
+ const fgHexColor = this._getHexColor(cell, true);
+ if (fgHexColor) {
+ content.push('color: ' + fgHexColor + ';');
+ }
+
+ const bgHexColor = this._getHexColor(cell, false);
+ if (bgHexColor) {
+ content.push('background-color: ' + bgHexColor + ';');
+ }
+
+ if (cell.isInverse()) { content.push(this._inverseStyle); }
+ if (cell.isBold()) { content.push(this._boldStyle); }
+ if (cell.isUnderline()) { content.push(this._underlineStyle); }
+ if (cell.isBlink()) { content.push(this._blinkStyle); }
+ if (cell.isInvisible()) { content.push(this._invisibleStyle); }
+ if (cell.isItalic()) { content.push(this._italicStyle); }
+ if (cell.isDim()) { content.push(this._dimStyle); }
+ if (cell.isStrikethrough()) { content.push(this._strikethroughStyle); }
+
+ return content;
+ }
+
+ return undefined;
+ }
+
+ 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 styleDefinitions = this._diffStyle(cell, oldCell);
+
+ // handles style change
+ if (styleDefinitions) {
+ this._currentRow += styleDefinitions.length === 0 ?
+ `` :
+ ``;
+ }
+
+ // handles actual content
+ if (isEmptyCell) {
+ this._currentRow += ' ';
+ } else {
+ this._currentRow += cell.getChars();
+ }
+ }
+
+ protected _serializeString(): string {
+ return this._htmlContent;
+ }
+}
diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json
index ef4a14ab..38ef6a7f 100644
--- a/addons/xterm-addon-serialize/src/tsconfig.json
+++ b/addons/xterm-addon-serialize/src/tsconfig.json
@@ -14,6 +14,9 @@
"paths": {
"common/*": [
"../../../src/common/*"
+ ],
+ "browser/*": [
+ "../../../src/browser/*"
]
},
"strict": true,
@@ -28,6 +31,9 @@
"references": [
{
"path": "../../../src/common"
+ },
+ {
+ "path": "../../../src/browser"
}
]
}
diff --git a/demo/client.ts b/demo/client.ts
index 58d719a1..cc73cfa4 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -59,27 +59,27 @@ interface IDemoAddon {
name: T;
canChange: boolean;
ctor:
- T extends 'attach' ? typeof AttachAddon :
- T extends 'fit' ? typeof FitAddon :
- T extends 'search' ? typeof SearchAddon :
- T extends 'serialize' ? typeof SerializeAddon :
- T extends 'web-links' ? typeof WebLinksAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'ligatures' ? typeof LigaturesAddon :
- typeof WebglAddon;
- instance?:
- T extends 'attach' ? AttachAddon :
- T extends 'fit' ? FitAddon :
- T extends 'search' ? SearchAddon :
- T extends 'serialize' ? SerializeAddon :
- T extends 'web-links' ? WebLinksAddon :
- T extends 'webgl' ? WebglAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'ligatures' ? typeof LigaturesAddon :
- never;
+ T extends 'attach' ? typeof AttachAddon :
+ T extends 'fit' ? typeof FitAddon :
+ T extends 'search' ? typeof SearchAddon :
+ T extends 'serialize' ? typeof SerializeAddon :
+ T extends 'web-links' ? typeof WebLinksAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
+ typeof WebglAddon;
+ instance?:
+ T extends 'attach' ? AttachAddon :
+ T extends 'fit' ? FitAddon :
+ T extends 'search' ? SearchAddon :
+ T extends 'serialize' ? SerializeAddon :
+ T extends 'web-links' ? WebLinksAddon :
+ T extends 'webgl' ? WebglAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
+ never;
}
-const addons: { [T in AddonType]: IDemoAddon} = {
+const addons: { [T in AddonType]: IDemoAddon } = {
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
fit: { name: 'fit', ctor: FitAddon, canChange: false },
search: { name: 'search', ctor: SearchAddon, canChange: true },
@@ -147,6 +147,7 @@ if (document.location.pathname === '/test') {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
+ document.getElementById('htmlserialize').addEventListener('click', htmlserializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
}
@@ -186,7 +187,7 @@ function createTerminal(): void {
const rows = size.rows;
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
- fetch(url, {method: 'POST'});
+ fetch(url, { method: 'POST' });
});
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
@@ -216,7 +217,7 @@ function createTerminal(): void {
// Set terminal size again to set the specific dimensions on the demo
updateTerminalSize();
- fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
+ fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }).then((res) => {
res.text().then((processId) => {
pid = processId;
socketURL += processId;
@@ -261,7 +262,7 @@ function runFakeTerminal(): void {
if (ev.keyCode === 13) {
term.prompt();
} else if (ev.keyCode === 8) {
- // Do not delete the prompt
+ // Do not delete the prompt
if (term._core.buffer.x > 2) {
term.write('\b \b');
}
@@ -353,7 +354,7 @@ function initOptions(term: TerminalType): void {
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
updateTerminalSize();
- } else if(o === 'scrollback') {
+ } else if (o === 'scrollback') {
term.options.scrollback = parseInt(input.value);
setTimeout(() => updateTerminalSize(), 5);
} else {
@@ -380,7 +381,7 @@ function initAddons(term: TerminalType): void {
if (!addon.canChange) {
checkbox.disabled = true;
}
- if(name === 'unicode11' && checkbox.checked) {
+ if (name === 'unicode11' && checkbox.checked) {
term.unicode.activeVersion = '11';
}
addDomListener(checkbox, 'change', () => {
@@ -447,6 +448,32 @@ function serializeButtonHandler(): void {
}
}
+function htmlserializeButtonHandler(): void {
+ const output = addons.serialize.instance.htmlserialize();
+ document.getElementById('htmlserialize-output').innerText = output;
+
+ var type = "text/html";
+ var blob = new Blob([output], { type });
+ // @ts-ignore
+ var data = [new ClipboardItem({ [type]: blob })];
+
+ const permissionName = "clipboard-write" as PermissionName;
+ navigator.permissions.query({name: permissionName }).then(result => {
+ if (result.state == "granted" || result.state == "prompt") {
+ navigator.clipboard.write(data).then(
+ () => {
+ document.getElementById("htmlserialize-output-result").innerText
+ = "Copied to clipboard";
+ },
+ () => {
+ document.getElementById("htmlserialize-output-result").innerText
+ = "Can't copy to clipboard.";
+ }
+ );
+ }
+ });
+}
+
function writeCustomGlyphHandler() {
term.write('\n\r');
diff --git a/demo/index.html b/demo/index.html
index 9c86783b..1ddce948 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -49,6 +49,10 @@
+
+
+
+