Merge branch 'master' into osc52

This commit is contained in:
Daniel Imms
2024-04-22 02:04:33 -07:00
committed by GitHub
27 changed files with 162 additions and 62 deletions
+5 -3
View File
@@ -1,13 +1,15 @@
{
"name": "xterm.js",
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {} // yarn
"ghcr.io/devcontainers/features/node:1": {
"version": 18
} // yarn
},
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install",
"postCreateCommand": "yarn install && yarn setup",
"customizations": {
"vscode": {
"extensions": [
+1 -1
View File
@@ -1 +1 @@
16
18
+1 -1
View File
@@ -61,7 +61,7 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"stopOnEntry": true,
"runtimeVersion": "16",
"runtimeVersion": "18",
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
+1
View File
@@ -223,6 +223,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
- [**Wave Terminal**](https://waveterm.dev): An open-source, ai-native, terminal built for seamless workflows.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
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 on our list. Note: Please add any new contributions to the end of the list only.
+3 -1
View File
@@ -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'
};
+3 -1
View File
@@ -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'
};
+3 -1
View File
@@ -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'
};
+3 -1
View File
@@ -33,7 +33,9 @@ const addon = {
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'
};
+3 -1
View File
@@ -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',
externals: {
+3 -3
View File
@@ -86,9 +86,9 @@ fd-slicer@~1.1.0:
pend "~1.2.0"
follow-redirects@^1.15.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
font-finder@^1.0.3:
version "1.0.4"
+3 -1
View File
@@ -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'
};
@@ -138,6 +138,16 @@ describe('SerializeAddon', () => {
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
});
it('basic terminal with html unsafe chars', async () => {
await writeP(terminal, ' <a>&pi; ');
terminal.select(1, 0, 7);
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(/<div><span>&lt;a>&amp;pi;<\/span><\/div>/g) || []).length, 1, output);
});
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
+9 -1
View File
@@ -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 '&amp;';
case '<': return '&lt;';
}
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());
}
}
+2 -1
View File
@@ -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'
};
@@ -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'
};
+2 -1
View File
@@ -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'
};
+3 -1
View File
@@ -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'
};
+3 -1
View File
@@ -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'
};
+7 -1
View File
@@ -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);
}
}
+3 -5
View File
@@ -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();
+7 -3
View File
@@ -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<number>());
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 {
+31 -2
View File
@@ -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));
+10 -6
View File
@@ -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();
+14
View File
@@ -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]);
});
});
}
+5 -3
View File
@@ -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;
+4 -2
View File
@@ -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;
+20 -20
View File
@@ -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"