π ');
+ terminal.select(1, 0, 7);
+
+ const output = serializeAddon.serializeAsHTML({
+ onlySelection: true
+ });
+ assert.equal((output.match(/<a>π<\/span><\/div>/g) || []).length, 1, output);
+ });
+
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts
index e654eddb..cd15cfc3 100644
--- a/addons/addon-serialize/src/SerializeAddon.ts
+++ b/addons/addon-serialize/src/SerializeAddon.ts
@@ -14,6 +14,14 @@ function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
}
+function escapeHTMLChar(c: string): string {
+ switch (c) {
+ case '&': return '&';
+ case '<': return '<';
+ }
+ return c;
+}
+
// TODO: Refine this template class later
abstract class BaseSerializeHandler {
constructor(
@@ -669,7 +677,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (isEmptyCell) {
this._currentRow += ' ';
} else {
- this._currentRow += cell.getChars();
+ this._currentRow += escapeHTMLChar(cell.getChars());
}
}
diff --git a/addons/addon-serialize/webpack.config.js b/addons/addon-serialize/webpack.config.js
index bd08ca37..837a73a3 100644
--- a/addons/addon-serialize/webpack.config.js
+++ b/addons/addon-serialize/webpack.config.js
@@ -34,7 +34,8 @@ module.exports = {
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd',
- globalObject: 'this'
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-unicode-graphemes/webpack.config.js b/addons/addon-unicode-graphemes/webpack.config.js
index 6a80bdea..1ebaecaa 100644
--- a/addons/addon-unicode-graphemes/webpack.config.js
+++ b/addons/addon-unicode-graphemes/webpack.config.js
@@ -32,7 +32,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-unicode11/webpack.config.js b/addons/addon-unicode11/webpack.config.js
index 1913481d..746d2581 100644
--- a/addons/addon-unicode11/webpack.config.js
+++ b/addons/addon-unicode11/webpack.config.js
@@ -33,7 +33,8 @@ module.exports = {
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd',
- globalObject: 'this'
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-web-links/webpack.config.js b/addons/addon-web-links/webpack.config.js
index 4484dbf6..e8dcecef 100644
--- a/addons/addon-web-links/webpack.config.js
+++ b/addons/addon-web-links/webpack.config.js
@@ -25,7 +25,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-webgl/webpack.config.js b/addons/addon-webgl/webpack.config.js
index f31ffd51..7365acff 100644
--- a/addons/addon-webgl/webpack.config.js
+++ b/addons/addon-webgl/webpack.config.js
@@ -33,7 +33,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index 1b2f7b5b..cb0f35ea 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -51,6 +51,8 @@ export class Viewport extends Disposable implements IViewport {
target: -1
};
+ private _ensureTimeout: number;
+
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
@@ -83,7 +85,7 @@ export class Viewport extends Disposable implements IViewport {
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready.
- setTimeout(() => this.syncScrollArea());
+ this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
}
private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -405,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
+
+ public dispose(): void {
+ clearTimeout(this._ensureTimeout);
+ }
}
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index c89cee62..92d152f0 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -343,6 +343,9 @@ export class DomRenderer extends Disposable implements IRenderer {
}
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
+ if (!this._selectionRenderModel.hasSelection) {
+ return;
+ }
// Translate from buffer position to viewport position
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
@@ -350,11 +353,6 @@ export class DomRenderer extends Disposable implements IRenderer {
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
- // No need to draw the selection
- if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
- return;
- }
-
// Create the selections
const documentFragment = this._document.createDocumentFragment();
diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts
index 575b62b6..a6c066b2 100644
--- a/src/browser/services/CoreBrowserService.ts
+++ b/src/browser/services/CoreBrowserService.ts
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
private _isFocused = false;
private _cachedIsFocused: boolean | undefined = undefined;
- private _screenDprMonitor = new ScreenDprMonitor(this._window);
+ private _screenDprMonitor = this.register(new ScreenDprMonitor(this._window));
private readonly _onDprChange = this.register(new EventEmitter());
public readonly onDprChange = this._onDprChange.event;
@@ -31,8 +31,12 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange));
- this._textarea.addEventListener('focus', () => this._isFocused = true);
- this._textarea.addEventListener('blur', () => this._isFocused = false);
+ this.register(
+ addDisposableDomListener(this._textarea, 'focus', () => (this._isFocused = true))
+ );
+ this.register(
+ addDisposableDomListener(this._textarea, 'blur', () => (this._isFocused = false))
+ );
}
public get window(): Window & typeof globalThis {
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index 8f9a988f..0f17a3e6 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -12,11 +12,12 @@ import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
-import { IBufferService, ICoreService } from 'common/services/Services';
+import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
import { BufferService } from 'common/services/BufferService';
import { CoreService } from 'common/services/CoreService';
+import { OscLinkService } from 'common/services/OscLinkService';
function getCursor(bufferService: IBufferService): number[] {
@@ -59,6 +60,7 @@ describe('InputHandler', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
+ let oscLinkService: IOscLinkService;
let inputHandler: TestInputHandler;
beforeEach(() => {
@@ -66,8 +68,9 @@ describe('InputHandler', () => {
bufferService = new BufferService(optionsService);
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
+ oscLinkService = new OscLinkService(bufferService);
- inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -1982,6 +1985,32 @@ describe('InputHandler', () => {
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
+ it('8: hyperlink with id', async () => {
+ await inputHandler.parseP('\x1b]8;id=100;http://localhost:3000\x07');
+ assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ assert.deepStrictEqual(
+ oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
+ {
+ id: '100',
+ uri: 'http://localhost:3000'
+ }
+ );
+ await inputHandler.parseP('\x1b]8;;\x07');
+ assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ });
+ it('8: hyperlink with semi-colon', async () => {
+ await inputHandler.parseP('\x1b]8;;http://localhost:3000;abc=def\x07');
+ assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ assert.deepStrictEqual(
+ oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
+ {
+ id: undefined,
+ uri: 'http://localhost:3000;abc=def'
+ }
+ );
+ await inputHandler.parseP('\x1b]8;;\x07');
+ assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ });
it('104: restore events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index 9b300993..6db8751e 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -2972,14 +2972,18 @@ export class InputHandler extends Disposable implements IInputHandler {
* feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.
*/
public setHyperlink(data: string): boolean {
- const args = data.split(';');
- if (args.length < 2) {
- return false;
+ // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)
+ const idx = data.indexOf(';');
+ if (idx === -1) {
+ // malformed sequence, just return as handled
+ return true;
}
- if (args[1]) {
- return this._createHyperlink(args[0], args[1]);
+ const id = data.slice(0, idx).trim();
+ const uri = data.slice(idx + 1);
+ if (uri) {
+ return this._createHyperlink(id, uri);
}
- if (args[0].trim()) {
+ if (id.trim()) {
return false;
}
return this._finishHyperlink();
diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts
index c4b66536..68492b3d 100644
--- a/test/playwright/SharedRendererTests.ts
+++ b/test/playwright/SharedRendererTests.ts
@@ -1234,6 +1234,20 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows), [0, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows, CellColorPosition.FIRST), [0, 0, 255, 255]);
});
+ test('#4917 The selection should not be displayed if it is not within the scope of the viewport.', async () => {
+ const theme: ITheme = {
+ selectionBackground: '#FF0000'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ for (let index = 0; index < 160; index++) {
+ await ctx.value.proxy.writeln(``);
+ }
+ await ctx.value.proxy.scrollToBottom();
+ const rows = await ctx.value.proxy.buffer.active.length;
+ await ctx.value.proxy.selectLines(rows - 1, rows - 1);
+ await ctx.value.proxy.scrollLines(-2);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ });
});
}
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 33008289..b6470275 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -47,11 +47,13 @@ declare module '@xterm/xterm' {
/**
* When enabled the cursor will be set to the beginning of the next line
- * with every new line. This is equivalent to sending '\r\n' for each '\n'.
- * Normally the termios settings of the underlying PTY deals with the
- * translation of '\n' to '\r\n' and this setting should not be used. If you
+ * with every new line. This is equivalent to sending `\r\n` for each `\n`.
+ * Normally the settings of the underlying PTY (`termios`) deal with the
+ * translation of `\n` to `\r\n` and this setting should not be used. If you
* deal with data from a non-PTY related source, this settings might be
* useful.
+ *
+ * @see https://pubs.opengroup.org/onlinepubs/007904975/basedefs/termios.h.html
*/
convertEol?: boolean;
diff --git a/webpack.config.headless.js b/webpack.config.headless.js
index 9e9099cd..12e7484d 100644
--- a/webpack.config.headless.js
+++ b/webpack.config.headless.js
@@ -39,8 +39,10 @@ const config = {
path: path.resolve('./headless/lib-headless'),
library: {
type: 'commonjs'
- }
+ },
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
- mode: 'production'
+ mode: 'production',
};
module.exports = config;
diff --git a/yarn.lock b/yarn.lock
index b2788793..e566fff2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1099,13 +1099,13 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-body-parser@1.20.1:
- version "1.20.1"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
- integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+body-parser@1.20.2:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+ integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
dependencies:
bytes "3.1.2"
- content-type "~1.0.4"
+ content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
@@ -1113,7 +1113,7 @@ body-parser@1.20.1:
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
- raw-body "2.5.1"
+ raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
@@ -1402,7 +1402,7 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
-content-type@~1.0.4:
+content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@@ -1417,10 +1417,10 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-cookie@0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
- integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cross-env@^7.0.3:
version "7.0.3"
@@ -1832,16 +1832,16 @@ express-ws@^5.0.2:
ws "^7.4.6"
express@^4.17.1:
- version "4.18.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
- integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
- body-parser "1.20.1"
+ body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
- cookie "0.5.0"
+ cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
@@ -3242,10 +3242,10 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-raw-body@2.5.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
- integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"