From 09a77998baed5f9c52206a3bda07010a52227b05 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 06:27:01 +0000 Subject: [PATCH] improvements for html serialize --- .../src/SerializeAddon.ts | 68 ++++++++++++---- demo/client.ts | 81 +++++++++---------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 8a1c9659..cebd16cc 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -5,7 +5,7 @@ * (EXPERIMENTAL) This Addon is still under development */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm'; import { IColorSet } from 'browser/Types'; function constrain(value: number, low: number, high: number): number { @@ -19,18 +19,25 @@ abstract class BaseSerializeHandler { ) { } - public serialize(startRow: number, endRow: number): string { + public serialize(range: IBufferRange): string { // we need two of them to flip between old and new cell const cell1 = this._buffer.getNullCell(); const cell2 = this._buffer.getNullCell(); let oldCell = cell1; + const startRow = range.start.x; + const endRow = range.end.x; + const startColumn = range.start.y; + const endColumn = range.end.y; + 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); if (line) { - for (let col = 0; col < line.length; col++) { + const startLineColumn = row !== range.start.x ? 0 : startColumn; + const endLineColumn = row !== range.end.x ? line.length : endColumn; + for (let col = startLineColumn; col < endLineColumn; col++) { const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); if (!c) { console.warn(`Can't get cell at row=${row}, col=${col}`); @@ -40,7 +47,7 @@ abstract class BaseSerializeHandler { oldCell = c; } } - this._rowEnd(row, row === endRow - 1); + this._rowEnd(row, row === endRow); } this._afterSerialize(); @@ -400,17 +407,37 @@ export class SerializeAddon implements ITerminalAddon { } private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length; + const maxRows = buffer.length - 1; const handler = new StringSerializeHandler(buffer, terminal); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); - return handler.serialize(maxRows - correctRows, maxRows); + return handler.serialize({ + start: { x: maxRows - correctRows, y: 0 }, + end: { x: maxRows, y: 0 } + }); } - private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length; + private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, options: Partial): string { const handler = new HTMLSerializeHandler(buffer, terminal); - const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); - return handler.serialize(maxRows - correctRows, maxRows); + const onlySelection = options.onlySelection ?? true; + if (!onlySelection) { + const maxRows = buffer.length; + const scrollback = options.scrollback; + const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return handler.serialize({ + start: { x: maxRows - correctRows, y: 0 }, + end: { x: maxRows, y: 0 } + }); + } + + const selection = this._terminal?.getSelectionPosition(); + if (selection !== undefined) { + return handler.serialize({ + start: { x: selection.startRow, y: selection.startColumn }, + end: { x: selection.endRow, y: selection.endColumn } + }); + } + + return ''; } private _serializeModes(terminal: Terminal): string { @@ -467,12 +494,12 @@ export class SerializeAddon implements ITerminalAddon { return content; } - public htmlserialize(options?: IHtmlSerializeOptions): string { + public htmlserialize(options?: Partial): 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); + return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options || {}); } public dispose(): void { } @@ -486,7 +513,8 @@ interface ISerializeOptions { } interface IHtmlSerializeOptions { - scrollback?: number; + scrollback: number; + onlySelection: boolean; } export class HTMLSerializeHandler extends BaseSerializeHandler { @@ -532,8 +560,16 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { protected _beforeSerialize(rows: number, start: number, end: number): void { this._htmlContent += '' + '
';
-    // TODO: fetch options and remove hardcoded values
-    this._htmlContent += '
'; + + const foreground = this._terminal.options.theme?.foreground ?? '#ffffff'; + const background = this._terminal.options.theme?.background ?? '#000000'; + + const globalStyleDefinitions = []; + globalStyleDefinitions.push('color: ' + foreground + ';'); + globalStyleDefinitions.push('background-color: ' + background + ';'); + globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';'); + globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;'); + this._htmlContent += '
'; } protected _afterSerialize(): void { diff --git a/demo/client.ts b/demo/client.ts index cc73cfa4..81309c7b 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,7 +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('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); } @@ -187,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/'; @@ -217,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; @@ -262,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'); } @@ -354,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 { @@ -381,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', () => { @@ -448,30 +448,21 @@ function serializeButtonHandler(): void { } } -function htmlserializeButtonHandler(): 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 + // Deprecated, but the most supported for now. + function listener(e) { + e.clipboardData.setData("text/html", output); + e.clipboardData.setData("text/plain", output); + e.preventDefault(); + } + document.addEventListener("copy", listener); + document.execCommand("copy"); + document.removeEventListener("copy", listener); + document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard"; - }, - () => { - document.getElementById("htmlserialize-output-result").innerText - = "Can't copy to clipboard."; - } - ); - } - }); }