Skip to content

Commit

Permalink
Merge branch 'master' into osc52
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyriar committed Apr 22, 2024
2 parents 3cedfbe + 826c057 commit 6ed1f42
Show file tree
Hide file tree
Showing 27 changed files with 162 additions and 62 deletions.
8 changes: 5 additions & 3 deletions .devcontainer/devcontainer.json
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
@@ -1 +1 @@
16
18
2 changes: 1 addition & 1 deletion .vscode/launch.json
Expand Up @@ -61,7 +61,7 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"stopOnEntry": true,
"runtimeVersion": "16",
"runtimeVersion": "18",
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion addons/addon-attach/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-canvas/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-fit/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-image/webpack.config.js
Expand Up @@ -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'
};
Expand Down
4 changes: 3 additions & 1 deletion addons/addon-ligatures/webpack.config.js
Expand Up @@ -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: {
Expand Down
6 changes: 3 additions & 3 deletions addons/addon-ligatures/yarn.lock
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion addons/addon-search/webpack.config.js
Expand Up @@ -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'
};
10 changes: 10 additions & 0 deletions addons/addon-serialize/src/SerializeAddon.test.ts
Expand Up @@ -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') + ' ');

Expand Down
10 changes: 9 additions & 1 deletion addons/addon-serialize/src/SerializeAddon.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -669,7 +677,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (isEmptyCell) {
this._currentRow += ' ';
} else {
this._currentRow += cell.getChars();
this._currentRow += escapeHTMLChar(cell.getChars());
}
}

Expand Down
3 changes: 2 additions & 1 deletion addons/addon-serialize/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-unicode-graphemes/webpack.config.js
Expand Up @@ -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'
};
3 changes: 2 additions & 1 deletion addons/addon-unicode11/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-web-links/webpack.config.js
Expand Up @@ -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'
};
4 changes: 3 additions & 1 deletion addons/addon-webgl/webpack.config.js
Expand Up @@ -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'
};
8 changes: 7 additions & 1 deletion src/browser/Viewport.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
8 changes: 3 additions & 5 deletions src/browser/renderer/dom/DomRenderer.ts
Expand Up @@ -343,18 +343,16 @@ 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;
const viewportEndRow = this._selectionRenderModel.viewportEndRow;
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();

Expand Down
10 changes: 7 additions & 3 deletions src/browser/services/CoreBrowserService.ts
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
33 changes: 31 additions & 2 deletions src/common/InputHandler.test.ts
Expand Up @@ -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[] {
Expand Down Expand Up @@ -59,15 +60,17 @@ describe('InputHandler', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
let oscLinkService: IOscLinkService;
let inputHandler: TestInputHandler;

beforeEach(() => {
optionsService = new MockOptionsService();
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', () => {
Expand Down Expand Up @@ -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));
Expand Down
16 changes: 10 additions & 6 deletions src/common/InputHandler.ts
Expand Up @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions test/playwright/SharedRendererTests.ts
Expand Up @@ -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]);
});
});
}

Expand Down

0 comments on commit 6ed1f42

Please sign in to comment.