From f9cf9b9782296ef6d25b3a925b3e19268a4bbc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 16:55:13 +0200 Subject: [PATCH 001/402] fix #5181 --- addons/addon-webgl/src/WebglAddon.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 32d6bc7b..49a796de 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -88,6 +88,9 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi renderService.setRenderer(this._renderer); this._register(toDisposable(() => { + if ((this._terminal as any)._core._store._isDisposed) { + return; + } const renderService: IRenderService = (this._terminal as any)._core._renderService; renderService.setRenderer((this._terminal as any)._core._createRenderer()); renderService.handleResize(terminal.cols, terminal.rows); From 8faf097e8399568bcd0d0fe8a0e40fbf26af6c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 17:12:00 +0200 Subject: [PATCH 002/402] fix flaky tests --- addons/addon-image/test/ImageAddon.test.ts | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index 758a19a4..d3ea7251 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -6,7 +6,7 @@ import test from '@playwright/test'; import { readFileSync } from 'fs'; import { FINALIZER, introducer, sixelEncode } from 'sixel'; -import { ITestContext, createTestContext, openTerminal, pollFor } from '../../../test/playwright/TestUtils'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; import { deepStrictEqual, ok, strictEqual } from 'assert'; /** @@ -163,28 +163,35 @@ test.describe('ImageAddon', () => { test('testdata default (scrolling with VT240 cursor pos)', async () => { const dim = await getDimensions(); await ctx.proxy.write(SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); // moved to right by 10 cells await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); }); test('write testdata noScrolling', async () => { await ctx.proxy.write('\x1b[?80h' + SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [0, 0]); // second draw does not change anything await ctx.proxy.write(SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [0, 0]); }); test('testdata cursor always at VT240 pos', async () => { const dim = await getDimensions(); // offset 0 await ctx.proxy.write(SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); // moved to right by 10 cells await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); // moved by 30 cells (+10 prev) await ctx.proxy.write('#'.repeat(30) + SIXEL_SEQ_0); + await timeout(50); deepStrictEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]); }); }); @@ -192,6 +199,7 @@ test.describe('ImageAddon', () => { test.describe('image lifecycle & eviction', () => { test('delete image once scrolled off', async () => { await ctx.proxy.write(SIXEL_SEQ_0); + await timeout(50); pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); // scroll to scrollback + rows - 1 await ctx.page.evaluate( @@ -199,7 +207,7 @@ test.describe('ImageAddon', () => { (await getScrollbackPlusRows() - 1) ); // wait here, as we have to make sure, that eviction did not yet occur - await new Promise(r => setTimeout(r, 100)); + await timeout(100); pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); // scroll one further should delete the image await ctx.page.evaluate(() => new Promise(res => (window as any).term.write('\n', res))); @@ -208,6 +216,7 @@ test.describe('ImageAddon', () => { test('get storageUsage', async () => { strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); await ctx.proxy.write(SIXEL_SEQ_0); + await timeout(50); ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); }); test('get/set storageLimit', async () => { @@ -222,18 +231,19 @@ test.describe('ImageAddon', () => { await ctx.proxy.write(SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); + await timeout(100); const usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); await ctx.proxy.write(SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); + await timeout(100); strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage); strictEqual(usage as number < 1, true); }); test('set storageLimit removes images synchronously', async () => { await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); + await timeout(100); const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageLimit = 0.5; window.imageAddon.storageUsage'); strictEqual(newUsage < usage, true); @@ -242,30 +252,32 @@ test.describe('ImageAddon', () => { test('clear alternate images on buffer change', async () => { strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); await ctx.proxy.write('\x1b[?1049h' + SIXEL_SEQ_0); + await timeout(50); ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); await ctx.proxy.write('\x1b[?1049l'); strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); }); test('evict tiles by in-place overwrites (only full overwrite tested)', async () => { - await new Promise(r => setTimeout(r, 50)); + await timeout(50); await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); + await timeout(50); let usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); while (usage === 0) { - await new Promise(r => setTimeout(r, 50)); + await timeout(50); usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); } await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); - await new Promise(r => setTimeout(r, 200)); // wait some time and re-check + await timeout(200); // wait some time and re-check strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage); }); test('manual eviction on alternate buffer must not miss images', async () => { await ctx.proxy.write('\x1b[?1049h'); await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); + await timeout(100); const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); + await timeout(100); const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); strictEqual(newUsage, usage); }); @@ -274,22 +286,27 @@ test.describe('ImageAddon', () => { test.describe('IIP support - testimages', () => { test('palette.png', async () => { await ctx.proxy.write(TESTDATA_IIP[0][0]); + await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]); }); test('spinfox.png', async () => { await ctx.proxy.write(TESTDATA_IIP[1][0]); + await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]); }); test('w3c gif', async () => { await ctx.proxy.write(TESTDATA_IIP[2][0]); + await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]); }); test('w3c jpeg', async () => { await ctx.proxy.write(TESTDATA_IIP[3][0]); + await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]); }); test('w3c png', async () => { await ctx.proxy.write(TESTDATA_IIP[4][0]); + await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]); }); }); From 39bd42983548a3f3f7cbb6416f94ba07ab8f89c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 5 Oct 2024 14:07:49 +0200 Subject: [PATCH 003/402] partially revert last commit --- addons/addon-image/test/ImageAddon.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index d3ea7251..25d47c82 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -163,35 +163,28 @@ test.describe('ImageAddon', () => { test('testdata default (scrolling with VT240 cursor pos)', async () => { const dim = await getDimensions(); await ctx.proxy.write(SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); // moved to right by 10 cells await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); }); test('write testdata noScrolling', async () => { await ctx.proxy.write('\x1b[?80h' + SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [0, 0]); // second draw does not change anything await ctx.proxy.write(SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [0, 0]); }); test('testdata cursor always at VT240 pos', async () => { const dim = await getDimensions(); // offset 0 await ctx.proxy.write(SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); // moved to right by 10 cells await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); // moved by 30 cells (+10 prev) await ctx.proxy.write('#'.repeat(30) + SIXEL_SEQ_0); - await timeout(50); deepStrictEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]); }); }); @@ -199,7 +192,6 @@ test.describe('ImageAddon', () => { test.describe('image lifecycle & eviction', () => { test('delete image once scrolled off', async () => { await ctx.proxy.write(SIXEL_SEQ_0); - await timeout(50); pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); // scroll to scrollback + rows - 1 await ctx.page.evaluate( @@ -216,7 +208,6 @@ test.describe('ImageAddon', () => { test('get storageUsage', async () => { strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); await ctx.proxy.write(SIXEL_SEQ_0); - await timeout(50); ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); }); test('get/set storageLimit', async () => { @@ -243,7 +234,6 @@ test.describe('ImageAddon', () => { }); test('set storageLimit removes images synchronously', async () => { await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await timeout(100); const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageLimit = 0.5; window.imageAddon.storageUsage'); strictEqual(newUsage < usage, true); @@ -252,7 +242,6 @@ test.describe('ImageAddon', () => { test('clear alternate images on buffer change', async () => { strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); await ctx.proxy.write('\x1b[?1049h' + SIXEL_SEQ_0); - await timeout(50); ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); await ctx.proxy.write('\x1b[?1049l'); strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); @@ -286,27 +275,22 @@ test.describe('ImageAddon', () => { test.describe('IIP support - testimages', () => { test('palette.png', async () => { await ctx.proxy.write(TESTDATA_IIP[0][0]); - await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]); }); test('spinfox.png', async () => { await ctx.proxy.write(TESTDATA_IIP[1][0]); - await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]); }); test('w3c gif', async () => { await ctx.proxy.write(TESTDATA_IIP[2][0]); - await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]); }); test('w3c jpeg', async () => { await ctx.proxy.write(TESTDATA_IIP[3][0]); - await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]); }); test('w3c png', async () => { await ctx.proxy.write(TESTDATA_IIP[4][0]); - await timeout(50); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]); }); }); From 687126970d75029059fff05b4b3408637e44e3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 5 Oct 2024 14:24:25 +0200 Subject: [PATCH 004/402] revert 5623ba6 --- addons/addon-web-links/test/WebLinksAddon.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/addons/addon-web-links/test/WebLinksAddon.test.ts b/addons/addon-web-links/test/WebLinksAddon.test.ts index 5763044f..f81d5c0b 100644 --- a/addons/addon-web-links/test/WebLinksAddon.test.ts +++ b/addons/addon-web-links/test/WebLinksAddon.test.ts @@ -35,9 +35,6 @@ test.describe('WebLinksAddon', () => { test.beforeEach(async () => { await ctx.page.evaluate(` window.term.reset(); - `); - await timeout(50); - await ctx.page.evaluate(` window._linkaddon?.dispose(); window._linkaddon = new WebLinksAddon(); window.term.loadAddon(window._linkaddon); From 9c74c81608e79385c5a2a49cfc15d5247a99281b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 5 Oct 2024 14:36:22 +0200 Subject: [PATCH 005/402] modified version of 5623ba6 --- addons/addon-web-links/test/WebLinksAddon.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/addon-web-links/test/WebLinksAddon.test.ts b/addons/addon-web-links/test/WebLinksAddon.test.ts index f81d5c0b..e4d5f5f2 100644 --- a/addons/addon-web-links/test/WebLinksAddon.test.ts +++ b/addons/addon-web-links/test/WebLinksAddon.test.ts @@ -36,6 +36,9 @@ test.describe('WebLinksAddon', () => { await ctx.page.evaluate(` window.term.reset(); window._linkaddon?.dispose(); + `); + await timeout(10); + await ctx.page.evaluate(` window._linkaddon = new WebLinksAddon(); window.term.loadAddon(window._linkaddon); `); From 932b65ef6779bf818f13d33a24f291da61feb02f Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 22 Oct 2024 14:46:43 +0100 Subject: [PATCH 006/402] Add pyTermTk HTML5 Exporter project to Real-world uses --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 39b7b52e..ede1b97e 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**OpenSFTP**](https://opensftp.com): Super beautiful SSH and SFTP integrated workspace client. - [**balena**](https://www.balena.io/): Balena is a full-stack solution for developing, deploying, updating, and troubleshooting IoT Edge devices. We use xterm.js to manage & debug devices on [balenaCloud](https://www.balena.io/cloud). - [**Filet Cloud**](https://github.com/fuglaro/filet-cloud): The lean and powerful personal cloud ⛅. +- [**pyTermTk**](https://github.com/ceccopierangiolieugenio/pyTermTk): Python Terminal Toolkit - a Spiced Up Cross Compatible TUI Library 🌶️, use xterm.js for the [HTML5 exporter](https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html). - [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. From a1b438cea6aec91726c526e263ea5d32079356b5 Mon Sep 17 00:00:00 2001 From: Asem Dreibati Date: Tue, 29 Oct 2024 10:31:57 +0300 Subject: [PATCH 007/402] Store the DI decorator's id in specific property (#5131). --- src/common/services/InstantiationService.ts | 2 +- src/common/services/ServiceRegistry.ts | 2 +- src/common/services/Services.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts index 375e442d..7e769548 100644 --- a/src/common/services/InstantiationService.ts +++ b/src/common/services/InstantiationService.ts @@ -67,7 +67,7 @@ export class InstantiationService implements IInstantiationService { for (const dependency of serviceDependencies) { const service = this._services.get(dependency.id); if (!service) { - throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`); + throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`); } serviceArgs.push(service); } diff --git a/src/common/services/ServiceRegistry.ts b/src/common/services/ServiceRegistry.ts index 6510fb8e..7d887bc6 100644 --- a/src/common/services/ServiceRegistry.ts +++ b/src/common/services/ServiceRegistry.ts @@ -33,7 +33,7 @@ export function createDecorator(id: string): IServiceIdentifier { storeServiceDependency(decorator, target, index); }; - decorator.toString = () => id; + decorator._id = id; serviceRegistry.set(id, decorator); return decorator; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0ceff36c..9d5ca64b 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -124,6 +124,7 @@ export interface ICharsetService { export interface IServiceIdentifier { (...args: any[]): void; type: T; + _id: string; } export interface IBrandedService { From 952ac617617678a0b2314e261fbe3e78995e2eb7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Nov 2024 07:18:27 -0700 Subject: [PATCH 008/402] Only resolve ligatures externals in commonjs environment Part of microsoft/vscode#34103 --- addons/addon-ligatures/webpack.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/addon-ligatures/webpack.config.js b/addons/addon-ligatures/webpack.config.js index f9e9f347..c43d0a17 100644 --- a/addons/addon-ligatures/webpack.config.js +++ b/addons/addon-ligatures/webpack.config.js @@ -31,10 +31,10 @@ module.exports = { }, mode: 'production', externals: { - 'fs': 'fs', - 'path': 'path', - 'stream': 'stream', - 'util': 'util' + 'fs': 'commonjs fs', + 'path': 'commonjs path', + 'stream': 'commonjs stream', + 'util': 'commonjs util' }, resolve: { // The ligature modules contains fallbacks for node environments, we never want to browserify them From 6684c406e89abd9b422177995880f030d73b536c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Nov 2024 07:19:33 -0700 Subject: [PATCH 009/402] Don't hide build folders This was getting too annoying needing to comment out these lines --- .vscode/settings.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bf1c691..9a9182c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,11 +4,7 @@ }, // Hide output files from the file explorer, comment this out to see the build output "files.exclude": { - "**/.nyc_output": true, - "**/lib": true, - "**/dist": true, - "**/out": true, - "**/out-*": true, + "**/.nyc_output": true }, "typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.quoteStyle": "single", From 3948482dc74518697a8c8c1df8b2f33341d32277 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Nov 2024 07:20:10 -0700 Subject: [PATCH 010/402] Also remove associated comment --- .vscode/settings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a9182c9..cf683d8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,6 @@ "files.associations": { ".eslintrc.json.typings": "jsonc" }, - // Hide output files from the file explorer, comment this out to see the build output "files.exclude": { "**/.nyc_output": true }, From 77885ed63d2b88004e0272845f9a92a4e08308e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:47:26 -0800 Subject: [PATCH 011/402] Set liga font feature when ligatures is enabled Fixes #5207 --- addons/addon-ligatures/src/LigaturesAddon.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts index cb589303..47fbc323 100644 --- a/addons/addon-ligatures/src/LigaturesAddon.ts +++ b/addons/addon-ligatures/src/LigaturesAddon.ts @@ -31,8 +31,12 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi { } public activate(terminal: Terminal): void { + if (!terminal.element) { + throw new Error('Cannot activate LigaturesAddon before open is called'); + } this._terminal = terminal; this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures); + terminal.element.style.fontFeatureSettings = '"liga" on, "calt" on'; } public dispose(): void { @@ -40,5 +44,8 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi { this._terminal?.deregisterCharacterJoiner(this._characterJoinerId); this._characterJoinerId = undefined; } + if (this._terminal?.element) { + this._terminal.element.style.fontFeatureSettings = ''; + } } } From 7fab293cd6f90d60d9ec7ff47cb514bbc2138850 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:12:20 -0800 Subject: [PATCH 012/402] Ensure last ligature cell is updated Fixes #3288 Fixes #5206 Part of microsoft/vscode#233005 --- addons/addon-webgl/src/WebglRenderer.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 48023eae..216b8404 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -314,14 +314,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateCursorBlink(); } - public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { - return -1; - } - - public deregisterCharacterJoiner(joinerId: number): boolean { - return false; - } - public renderRows(start: number, end: number): void { if (!this._isAttached) { if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) { @@ -510,14 +502,17 @@ export class WebglRenderer extends Disposable implements IRenderer { cell = this._workCell; // Null out non-first cells - for (x++; x < lastCharX; x++) { + for (x++; x <= lastCharX; x++) { j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0); this._model.cells[j] = NULL_CELL_CODE; + // Don't re-resolve the cell color since multi-colored ligature backgrounds are not + // supported this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; } + x--; // Go back to the previous update cell for next iteration } } } From 9e1d5ba7db595e24db28b9e4d8a362f44ed53a5d Mon Sep 17 00:00:00 2001 From: "Tristan F.-R." Date: Mon, 11 Nov 2024 18:57:03 -0800 Subject: [PATCH 013/402] chore(addons/fit): fix typo Reprepresents -> Represents --- addons/addon-fit/typings/addon-fit.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-fit/typings/addon-fit.d.ts b/addons/addon-fit/typings/addon-fit.d.ts index e3d20e29..784b55d1 100644 --- a/addons/addon-fit/typings/addon-fit.d.ts +++ b/addons/addon-fit/typings/addon-fit.d.ts @@ -39,7 +39,7 @@ declare module '@xterm/addon-fit' { } /** - * Reprepresents the dimensions of a terminal. + * Represents the dimensions of a terminal. */ export interface ITerminalDimensions { /** From 8d23cbc05196be32017c103171deb0e77400ccc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:41:31 +0000 Subject: [PATCH 014/402] Bump cross-spawn from 7.0.3 to 7.0.6 Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 777abc0a..5ed534e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1669,9 +1669,9 @@ cross-env@^7.0.3: cross-spawn "^7.0.1" cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 9c66ad11399cc362dcc94e7f131bbb0f9555ba72 Mon Sep 17 00:00:00 2001 From: JackieL Date: Tue, 19 Nov 2024 14:58:32 +0800 Subject: [PATCH 015/402] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 39b7b52e..5102c69d 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**OpenSFTP**](https://opensftp.com): Super beautiful SSH and SFTP integrated workspace client. - [**balena**](https://www.balena.io/): Balena is a full-stack solution for developing, deploying, updating, and troubleshooting IoT Edge devices. We use xterm.js to manage & debug devices on [balenaCloud](https://www.balena.io/cloud). - [**Filet Cloud**](https://github.com/fuglaro/filet-cloud): The lean and powerful personal cloud ⛅. +- [**LabEx**](https://labex.io): LabEx: Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach. - [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. From db621d7294866ed75fe0066b7bdab1df61c11f9a Mon Sep 17 00:00:00 2001 From: JackieL Date: Tue, 19 Nov 2024 14:59:53 +0800 Subject: [PATCH 016/402] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5102c69d..7883f870 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**OpenSFTP**](https://opensftp.com): Super beautiful SSH and SFTP integrated workspace client. - [**balena**](https://www.balena.io/): Balena is a full-stack solution for developing, deploying, updating, and troubleshooting IoT Edge devices. We use xterm.js to manage & debug devices on [balenaCloud](https://www.balena.io/cloud). - [**Filet Cloud**](https://github.com/fuglaro/filet-cloud): The lean and powerful personal cloud ⛅. -- [**LabEx**](https://labex.io): LabEx: Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach. +- [**LabEx**](https://labex.io): Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach. - [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. From 9aba07f8f83bac4173a0e9de54da97d4b3592629 Mon Sep 17 00:00:00 2001 From: blashaq Date: Thu, 21 Nov 2024 18:16:19 +0000 Subject: [PATCH 017/402] putty-style ED2 sequence handling as terminal option --- src/common/InputHandler.test.ts | 31 +++++++++++++++++++++++++++ src/common/InputHandler.ts | 28 +++++++++++++++++++----- src/common/services/OptionsService.ts | 3 ++- typings/xterm-headless.d.ts | 6 ++++++ typings/xterm.d.ts | 6 ++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index baae735c..c2198b82 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -438,6 +438,37 @@ describe('InputHandler', () => { inputHandler.eraseInLine(Params.fromArray([2])); assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false); }); + it('ED2 with scrollOnDisplayErase turned on', async () => { + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockLogService(), + new MockOptionsService({ scrollOnDisplayErase: true }), + new MockOscLinkService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); + const aLine = Array(bufferService.cols + 1).join('a'); + // add 2 full lines of text. + await inputHandler.parseP(aLine); + await inputHandler.parseP(aLine); + + inputHandler.eraseInDisplay(Params.fromArray([2])); + // those 2 lines should have been pushed to scrollback. + assert.equal(bufferService.rows + 2, bufferService.buffer.lines.length); + assert.equal(bufferService.buffer.ybase, 2); + assert.equal(bufferService.buffer.lines.get(0)?.translateToString(), aLine); + assert.equal(bufferService.buffer.lines.get(1)?.translateToString(), aLine); + + // Move to last line and add more text. + bufferService.buffer.y = bufferService.rows - 1; + bufferService.buffer.x = 0; + await inputHandler.parseP(aLine); + inputHandler.eraseInDisplay(Params.fromArray([2])); + // Screen should have been scrolled by a full screen size. + assert.equal(bufferService.rows * 2 + 2, bufferService.buffer.lines.length); + }); it('eraseInDisplay', async () => { const bufferService = new MockBufferService(80, 7); const inputHandler = new TestInputHandler( diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b94d7855..48973691 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1220,12 +1220,30 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowTracker.markDirty(0); break; case 2: - j = this._bufferService.rows; - this._dirtyRowTracker.markDirty(j - 1); - while (j--) { - this._resetBufferLine(j, respectProtect); + if (this._optionsService.rawOptions.scrollOnDisplayErase) { + let fouldLastLineToKeep = false; + j = this._bufferService.rows; + const x = this._activeBuffer.getBlankLine(this._eraseAttrData()); + while (j > 0 && !fouldLastLineToKeep) { + j--; + const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j); + if (currentLine?.translateToString() !== x.translateToString()) { + fouldLastLineToKeep = true; + this._dirtyRowTracker.markRangeDirty(0, j); + } + } + for (; j >= 0; j--) { + this._bufferService.scroll(this._eraseAttrData()); + } + } + else { + j = this._bufferService.rows; + this._dirtyRowTracker.markDirty(j - 1); + while (j--) { + this._resetBufferLine(j, respectProtect); + } + this._dirtyRowTracker.markDirty(0); } - this._dirtyRowTracker.markDirty(0); break; case 3: // Clear scrollback (everything not in viewport) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index a757c179..772b0a0a 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -54,7 +54,8 @@ export const DEFAULT_OPTIONS: Readonly> = { convertEol: false, termName: 'xterm', cancelEvents: false, - overviewRuler: {} + overviewRuler: {}, + scrollOnDisplayErase: false }; const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 3cbde44b..fdaf8fed 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -248,6 +248,12 @@ declare module '@xterm/headless' { * All features are disabled by default for security reasons. */ windowOptions?: IWindowOptions; + + /** + * If enabled ED2 (clear screen) escape sequence will push erased text to scrollback. + * This emulates PuTTY default clear screen behaviour. + */ + scrollOnDisplayErase?: boolean } /** diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f9cf14f9..6514261e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -331,6 +331,12 @@ declare module '@xterm/xterm' { * decorations underneath the scroll bar. */ overviewRuler?: IOverviewRulerOptions; + + /** + * If enabled ED2 (clear screen) escape sequence will push erased text to scrollback. + * This emulates PuTTY default clear screen behaviour. + */ + scrollOnDisplayErase?: boolean } /** From 780f9a31ff8320b97ee1bfd3c388cb2bd07784cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20B=C5=82asiak?= Date: Thu, 21 Nov 2024 22:46:06 +0100 Subject: [PATCH 018/402] Update src/common/InputHandler.ts Co-authored-by: jerch --- src/common/InputHandler.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 48973691..81cf086c 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1221,15 +1221,12 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 2: if (this._optionsService.rawOptions.scrollOnDisplayErase) { - let fouldLastLineToKeep = false; j = this._bufferService.rows; - const x = this._activeBuffer.getBlankLine(this._eraseAttrData()); - while (j > 0 && !fouldLastLineToKeep) { - j--; + this._dirtyRowTracker.markRangeDirty(0, j - 1); + while (j--) { const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j); - if (currentLine?.translateToString() !== x.translateToString()) { - fouldLastLineToKeep = true; - this._dirtyRowTracker.markRangeDirty(0, j); + if (currentLine?.getTrimmedLength()) { + break; } } for (; j >= 0; j--) { From 92c21a695057403f41ad54f8f9864f03b7ed28d9 Mon Sep 17 00:00:00 2001 From: blashaq Date: Sat, 23 Nov 2024 10:58:19 +0000 Subject: [PATCH 019/402] + scrollOnDisplayErase option --- src/common/services/Services.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0ceff36c..080f4a8e 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -252,6 +252,7 @@ export interface ITerminalOptions { windowOptions?: IWindowOptions; wordSeparator?: string; overviewRuler?: IOverviewRulerOptions; + scrollOnDisplayErase?: boolean; [key: string]: any; cancelEvents: boolean; From 904ddbc884437d82b2e6b1eb7ca0588302243763 Mon Sep 17 00:00:00 2001 From: blashaq Date: Sat, 23 Nov 2024 16:59:30 +0000 Subject: [PATCH 020/402] code style fix --- typings/xterm-headless.d.ts | 9 +++++---- typings/xterm.d.ts | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index fdaf8fed..b7aa1b59 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -249,11 +249,12 @@ declare module '@xterm/headless' { */ windowOptions?: IWindowOptions; - /** - * If enabled ED2 (clear screen) escape sequence will push erased text to scrollback. - * This emulates PuTTY default clear screen behaviour. + /** + * If enabled ED2 (clear screen) escape sequence will push + * erased text to scrollback. + * This emulates PuTTY default clear screen behavior. */ - scrollOnDisplayErase?: boolean + scrollOnDisplayErase?: boolean; } /** diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6514261e..9becb0b3 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -333,10 +333,11 @@ declare module '@xterm/xterm' { overviewRuler?: IOverviewRulerOptions; /** - * If enabled ED2 (clear screen) escape sequence will push erased text to scrollback. - * This emulates PuTTY default clear screen behaviour. + * If enabled ED2 (clear screen) escape sequence will push + * erased text to scrollback. + * This emulates PuTTY default clear screen behavior. */ - scrollOnDisplayErase?: boolean + scrollOnDisplayErase?: boolean; } /** From ae2622e5ce864a784b1faf43425106625a118a8e Mon Sep 17 00:00:00 2001 From: Jacob Bandes-Storch Date: Thu, 5 Dec 2024 18:00:18 -0800 Subject: [PATCH 021/402] Add reflowCursorLine option --- demo/client.ts | 21 +++++++++++++-------- demo/index.html | 1 + src/common/buffer/Buffer.ts | 15 +++++++++------ src/common/buffer/BufferReflow.ts | 15 +++++++++------ src/common/services/OptionsService.ts | 1 + src/common/services/Services.ts | 1 + typings/xterm-headless.d.ts | 6 ++++++ typings/xterm.d.ts | 6 ++++++ 8 files changed, 46 insertions(+), 20 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 49ba6743..6e0be22f 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -366,14 +366,19 @@ function createTerminal(): void { // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); - const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); - const processId = await res.text(); - pid = processId; - socketURL += processId; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; + const useRealTerminal = document.getElementById('use-real-terminal'); + if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) { + runFakeTerminal(); + } else { + const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); + const processId = await res.text(); + pid = processId; + socketURL += processId; + socket = new WebSocket(socketURL); + socket.onopen = runRealTerminal; + socket.onclose = runFakeTerminal; + socket.onerror = runFakeTerminal; + } }, 0); } diff --git a/demo/index.html b/demo/index.html index 06451847..3d46f61c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -81,6 +81,7 @@
Lifecycle
+
diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index d5e05731..dfdaefd3 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -315,7 +315,7 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number, newRows: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA)); + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), this._optionsService.rawOptions.reflowCursorLine); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); @@ -347,6 +347,7 @@ export class Buffer implements IBuffer { } private _reflowSmaller(newCols: number, newRows: number): void { + const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine; const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Gather all BufferLines that need to be inserted into the Buffer here so that they can be // batched up and only committed once @@ -367,11 +368,13 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } - // If these lines contain the cursor don't touch them, the program will handle fixing up - // wrapped lines with the cursor - const absoluteY = this.ybase + this.y; - if (absoluteY >= y && absoluteY < y + wrappedLines.length) { - continue; + if (!reflowCursorLine) { + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + const absoluteY = this.ybase + this.y; + if (absoluteY >= y && absoluteY < y + wrappedLines.length) { + continue; + } } const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts index af1c6473..c127f3b0 100644 --- a/src/common/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -20,8 +20,9 @@ export interface INewLayoutResult { * @param newCols The columns after resize. * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY). * @param nullCell The cell data to use when filling in empty cells. + * @param reflowCursorLine Whether to reflow the line containing the cursor. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData,reflowCursorLine: boolean): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -41,11 +42,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o nextLine = lines.get(++i) as BufferLine; } - // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped - // lines with the cursor - if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { - y += wrappedLines.length - 1; - continue; + if (!reflowCursorLine) { + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.length - 1; + continue; + } } // Copy buffer data to new locations diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index a757c179..4b8ca822 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly> = { allowTransparency: false, tabStopWidth: 8, theme: {}, + reflowCursorLine: false, rescaleOverlappingGlyphs: false, rightClickSelectsWord: isMac, windowOptions: {}, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0ceff36c..0e5c5184 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -237,6 +237,7 @@ export interface ITerminalOptions { macOptionIsMeta?: boolean; macOptionClickForcesSelection?: boolean; minimumContrastRatio?: number; + reflowCursorLine?: boolean; rescaleOverlappingGlyphs?: boolean; rightClickSelectsWord?: boolean; rows?: number; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 3cbde44b..621b450b 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -142,6 +142,12 @@ declare module '@xterm/headless' { */ minimumContrastRatio?: number; + /** + * Whether to reflow the line containing the cursor when the terminal is resized. Defaults to + * false, because shells usually handle this themselves. + */ + reflowCursorLine?: boolean; + /** * Whether to rescale glyphs horizontally that are a single cell wide but * have glyphs that would overlap following cell(s). This typically happens diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f9cf14f9..a3ebadb0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -213,6 +213,12 @@ declare module '@xterm/xterm' { */ minimumContrastRatio?: number; + /** + * Whether to reflow the line containing the cursor when the terminal is resized. Defaults to + * false, because shells usually handle this themselves. + */ + reflowCursorLine?: boolean; + /** * Whether to rescale glyphs horizontally that are a single cell wide but * have glyphs that would overlap following cell(s). This typically happens From d7364faeee18768908df3fbc2664f8922e5ff7e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:25:31 -0800 Subject: [PATCH 022/402] Update src/common/buffer/BufferReflow.ts --- src/common/buffer/BufferReflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts index c127f3b0..44aa0976 100644 --- a/src/common/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -22,7 +22,7 @@ export interface INewLayoutResult { * @param nullCell The cell data to use when filling in empty cells. * @param reflowCursorLine Whether to reflow the line containing the cursor. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData,reflowCursorLine: boolean): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; From 65899a9b50ced99098798b11354f0b5d3a4d8b25 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:25:36 -0800 Subject: [PATCH 023/402] Update src/common/buffer/Buffer.ts --- src/common/buffer/Buffer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index dfdaefd3..81ab156b 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -315,7 +315,8 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number, newRows: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), this._optionsService.rawOptions.reflowCursorLine); + const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine; + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); From 0d673930721848418163df6f6da956d287d2b676 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:30:17 -0800 Subject: [PATCH 024/402] Fix lint --- typings/xterm-headless.d.ts | 5 +++-- typings/xterm.d.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 621b450b..1085db9b 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -143,8 +143,9 @@ declare module '@xterm/headless' { minimumContrastRatio?: number; /** - * Whether to reflow the line containing the cursor when the terminal is resized. Defaults to - * false, because shells usually handle this themselves. + * Whether to reflow the line containing the cursor when the terminal is + * resized. Defaults to false, because shells usually handle this + * themselves. */ reflowCursorLine?: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a3ebadb0..15a03327 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -214,8 +214,9 @@ declare module '@xterm/xterm' { minimumContrastRatio?: number; /** - * Whether to reflow the line containing the cursor when the terminal is resized. Defaults to - * false, because shells usually handle this themselves. + * Whether to reflow the line containing the cursor when the terminal is + * resized. Defaults to false, because shells usually handle this + * themselves. */ reflowCursorLine?: boolean; From bc7288a38908231a2f1642706d84307abe511c9c Mon Sep 17 00:00:00 2001 From: Jay Mathis Date: Thu, 12 Dec 2024 21:40:46 -0600 Subject: [PATCH 025/402] Update README.md - add ecmaOS as Use Case --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 39b7b52e..fc2a6cea 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**OpenSFTP**](https://opensftp.com): Super beautiful SSH and SFTP integrated workspace client. - [**balena**](https://www.balena.io/): Balena is a full-stack solution for developing, deploying, updating, and troubleshooting IoT Edge devices. We use xterm.js to manage & debug devices on [balenaCloud](https://www.balena.io/cloud). - [**Filet Cloud**](https://github.com/fuglaro/filet-cloud): The lean and powerful personal cloud ⛅. +- [**ecmaOS**](https://ecmaos.sh): A kernel and suite of applications tying modern web technologies into a browser-based operating system. - [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. From 1978399259365a3d7e65b575068c58e0751e09f4 Mon Sep 17 00:00:00 2001 From: Jacob Bandes-Storch Date: Fri, 13 Dec 2024 13:57:17 -0800 Subject: [PATCH 026/402] Fix click event bug caused by DomRenderer replaceChildren behavior --- src/browser/renderer/dom/DomRenderer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index f6fda22e..2274d043 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -161,6 +161,10 @@ export class DomRenderer extends Disposable implements IRenderer { // Base CSS let styles = `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + + // Disabling pointer events circumvents a browser behavior that prevents `click` events from + // being delivered if the target element is replaced during the click. This happened due to + // refresh() being called during the mousedown handler to start a selection. + ` pointer-events: none;` + ` color: ${colors.foreground.css};` + ` font-family: ${this._optionsService.rawOptions.fontFamily};` + ` font-size: ${this._optionsService.rawOptions.fontSize}px;` + From 6bb2210a91e30eadd6c3872c16b6f26f3f272132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 15 Dec 2024 18:57:03 +0100 Subject: [PATCH 027/402] initial commit --- .eslintrc.json | 2 + addons/addon-progress/.gitignore | 2 + addons/addon-progress/.npmignore | 32 +++++ addons/addon-progress/LICENSE | 19 +++ addons/addon-progress/README.md | 81 +++++++++++ addons/addon-progress/package.json | 28 ++++ addons/addon-progress/src/ProgressAddon.ts | 114 ++++++++++++++++ addons/addon-progress/src/tsconfig.json | 35 +++++ .../addon-progress/test/ProgressAddon.test.ts | 129 ++++++++++++++++++ .../addon-progress/test/playwright.config.ts | 35 +++++ addons/addon-progress/test/tsconfig.json | 41 ++++++ addons/addon-progress/tsconfig.json | 8 ++ .../typings/addon-progress.d.ts | 30 ++++ addons/addon-progress/webpack.config.js | 33 +++++ bin/test_integration.js | 1 + demo/client.ts | 80 +++++++++-- demo/index.html | 41 ++++++ demo/tsconfig.json | 1 + tsconfig.all.json | 1 + 19 files changed, 698 insertions(+), 15 deletions(-) create mode 100644 addons/addon-progress/.gitignore create mode 100644 addons/addon-progress/.npmignore create mode 100644 addons/addon-progress/LICENSE create mode 100644 addons/addon-progress/README.md create mode 100644 addons/addon-progress/package.json create mode 100644 addons/addon-progress/src/ProgressAddon.ts create mode 100644 addons/addon-progress/src/tsconfig.json create mode 100644 addons/addon-progress/test/ProgressAddon.test.ts create mode 100644 addons/addon-progress/test/playwright.config.ts create mode 100644 addons/addon-progress/test/tsconfig.json create mode 100644 addons/addon-progress/tsconfig.json create mode 100644 addons/addon-progress/typings/addon-progress.d.ts create mode 100644 addons/addon-progress/webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index 8c475982..acd85af6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,6 +23,8 @@ "addons/addon-image/src/tsconfig.json", "addons/addon-image/test/tsconfig.json", "addons/addon-ligatures/src/tsconfig.json", + "addons/addon-progress/src/tsconfig.json", + "addons/addon-progress/test/tsconfig.json", "addons/addon-search/src/tsconfig.json", "addons/addon-search/test/tsconfig.json", "addons/addon-serialize/src/tsconfig.json", diff --git a/addons/addon-progress/.gitignore b/addons/addon-progress/.gitignore new file mode 100644 index 00000000..3063f07d --- /dev/null +++ b/addons/addon-progress/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/addon-progress/.npmignore b/addons/addon-progress/.npmignore new file mode 100644 index 00000000..d2fb3bdc --- /dev/null +++ b/addons/addon-progress/.npmignore @@ -0,0 +1,32 @@ +# Blacklist - exclude everything except npm defaults such as LICENSE, etc +* +!*/ + +# Whitelist - lib/ +!lib/**/*.d.ts + +!lib/**/*.js +!lib/**/*.js.map + +!lib/**/*.mjs +!lib/**/*.mjs.map + +!lib/**/*.css + +# Whitelist - src/ +!src/**/*.ts +!src/**/*.d.ts + +!src/**/*.js +!src/**/*.js.map + +!src/**/*.css + +# Blacklist - src/ test files +src/**/*.test.ts +src/**/*.test.d.ts +src/**/*.test.js +src/**/*.test.js.map + +# Whitelist - typings/ +!typings/*.d.ts diff --git a/addons/addon-progress/LICENSE b/addons/addon-progress/LICENSE new file mode 100644 index 00000000..447eb79f --- /dev/null +++ b/addons/addon-progress/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md new file mode 100644 index 00000000..192e1e5a --- /dev/null +++ b/addons/addon-progress/README.md @@ -0,0 +1,81 @@ +## @xterm/addon-progress + +An xterm.js addon providing an interface for ConEmu's progress sequence. +See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC for sequence details. + + +### Install + +```bash +npm install --save @xterm/addon-progress +``` + + +### Usage + +```ts +import { Terminal } from '@xterm/xterm'; +import { ProgressAddon } from '@xterm/addon-progress'; + +const terminal = new Terminal(); +const progressAddon = new ProgressAddon(); +terminal.loadAddon(progressAddon); +progressAddon.register({state, value} => { + // state: 0-4 integer (see below for meaning) + // value: 0-100 integer (percent value) + + // do your visualisation based on state/progress here + ... +}); +``` + +### Sequence + +The sequence to set progress information has the following format: + +```plain +ESC ] 9 ; 4 ; ; BEL +``` + +where state is a decimal number in 0 to 4 and progress value is a decimal number in 0 to 100. +The states have the following meaning: + +- 0: Remove any progress indication. Also resets progress value to 0. A given progress value will be ignored. +- 1: Normal state to set a progress value. The value should be in 0..100, greater values are clamped to 100. + If the value is omitted, it will be set to 0. +- 2: Error state with an optional progress value. An omitted value will be set to 0, + which has a special meaning using the last active value. +- 3: Actual progress is "indeterminate", any progress value will be ignored. Meant to be used to indicate + a running task without progress information (e.g. by a spinner). A previously set progress value + by any other state sequence will be left untouched. +- 4: Pause or warning state with an optional progress value. An omitted value will be set to 0, + which has a special meaning using the last active value. + +The addon resolves most of those semantic nuances and will provide these ready-to-go values: +- For the remove state (0) any progress value wont be parsed, thus is even allowed to contain garbage. + It will always emit `{state: 0, value: 0}`. +- For the set state (1) an omitted value will be set to 0 emitting `{state: 1, value: 0}`. + If a value was given, it must be decimal digits only, any characters outside will mark the whole sequence + as faulty (no sloppy integer parsing). The value will be clamped to max 100 giving + `{state: 1, value: parsedAndClampedValue}`. +- For the error and pause state (2 & 4) an omitted or zero value will emit `{state: 2|4, value: lastValue}`. + If a value was given, it must be decimal digits only, any characters outside will mark the whole sequence + as faulty (no sloppy integer parsing). The value will be clamped to max 100 giving + `{state: 2|4, value: parsedAndClampedValue}`. +- For the indeterminate state (3) a value notion will be ignored. + It still emits the value as `{state: 3, value: lastValue}`. Keep in mind not use that value while + that state is active, as a task might have entered that state without a proper reset at the beginning. + +### API + +The addon exposes the following API endpoints: +- `public register(handler: ProgressHandler): IDisposable;` \ + Registers your actual progress handler, where you gonna do the visual progress visualisation. + The handler will get called upon valid progress sequences with 2 arguments as `(state, value) => {}`. + Returns a disposable to unregister the handler later on by calling its `dispose()` method. +- `public progress: IProgress;` + A getter/setter for the current progress information. Can be used to read the last seen progress information. + This can also be used to clean up stuck progress indicators by setting the value back to initial, e.g.: + ```typescript + progressAddon.progress = {state: 0, value: 0}; + ``` diff --git a/addons/addon-progress/package.json b/addons/addon-progress/package.json new file mode 100644 index 00000000..57a06d68 --- /dev/null +++ b/addons/addon-progress/package.json @@ -0,0 +1,28 @@ +{ + "name": "@xterm/addon-progress", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/addon-progress.js", + "module": "lib/addon-progress.mjs", + "types": "typings/addon-progress.d.ts", + "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-progress", + "license": "MIT", + "keywords": [ + "terminal", + "xterm", + "xterm.js" + ], + "scripts": { + "build": "../../node_modules/.bin/tsc -p .", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package", + "start": "node ../../demo/start" + }, + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } +} diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts new file mode 100644 index 00000000..6065c5c5 --- /dev/null +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { ProgressAddon as IProgressApi, IProgress, ProgressHandler } from '@xterm/addon-progress'; + + +const enum ProgressState { + REMOVE = 0, + SET = 1, + ERROR = 2, + INDETERMINATE = 3, + PAUSE = 4 +} + + +/** + * Strict integer parsing, only decimal digits allowed. + */ +function toInt(s: string): number { + let v = 0; + for (let i = 0; i < s.length; ++i) { + const c = s.charCodeAt(i); + if (c < 0x30 || 0x39 < c) { + return -1; + } + v = v * 10 + c - 48; + } + return v; +} + + +export class ProgressAddon implements ITerminalAddon, IProgressApi { + private _seqHandler: IDisposable | undefined; + private _st: ProgressState = ProgressState.REMOVE; + private _pr = 0; + private _handlers: ProgressHandler[] = []; + + public dispose(): void { + this._seqHandler?.dispose(); + this._handlers.length = 0; + } + + public activate(terminal: Terminal): void { + this._seqHandler = terminal.parser.registerOscHandler(9, data => { + if (!data.startsWith('4;')) { + return false; + } + const parts = data.split(';'); + + if (parts.length > 3) { + return true; // faulty sequence, just exit + } + if (parts.length === 2) { + parts.push(''); + } + const st = toInt(parts[1]); + const pr = toInt(parts[2]); + + switch (st) { + case ProgressState.REMOVE: + this.progress = { state: st, value: 0 }; + break; + case ProgressState.SET: + if (pr < 0) return true; // faulty sequence, just exit + this.progress = { state: st, value: pr }; + break; + case ProgressState.ERROR: + case ProgressState.PAUSE: + if (pr < 0) return true; // faulty sequence, just exit + this.progress = { state: st, value: pr || this._pr }; + break; + case ProgressState.INDETERMINATE: + this.progress = { state: st, value: this._pr }; + break; + } + return true; + }); + } + + public register(handler: ProgressHandler): IDisposable { + const handlers = this._handlers; + handlers.push(handler); + return { + dispose: () => { + const idx = handlers.indexOf(handler); + if (idx !== -1) { + handlers.splice(idx, 1); + } + } + }; + } + + public get progress(): IProgress { + return { state: this._st, value: this._pr }; + } + + public set progress(progress: IProgress) { + if (0 <= progress.state && progress.state <= 4) + { + this._st = progress.state; + this._pr = Math.min(Math.max(progress.value, 0), 100); + + // call progress handlers + for (let i = 0; i < this._handlers.length; ++i) { + this._handlers[i](this._st, this._pr); + } + } else { + console.warn(`progress state out of bounds, not applied`); + } + } +} diff --git a/addons/addon-progress/src/tsconfig.json b/addons/addon-progress/src/tsconfig.json new file mode 100644 index 00000000..8a90d6e3 --- /dev/null +++ b/addons/addon-progress/src/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true, + "types": [ + "../../../node_modules/@types/mocha" + ], + "paths": { + "browser/*": [ + "../../../src/browser/*" + ], + "@xterm/addon-progress": [ + "../typings/addon-progress.d.ts" + ] + } + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/browser" + } + ] +} diff --git a/addons/addon-progress/test/ProgressAddon.test.ts b/addons/addon-progress/test/ProgressAddon.test.ts new file mode 100644 index 00000000..656b7f95 --- /dev/null +++ b/addons/addon-progress/test/ProgressAddon.test.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import test from '@playwright/test'; +import { deepStrictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils'; + + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + ctx.page.setViewportSize({ width: 1024, height: 768 }); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + + +test.describe('ProgressAddon', () => { + test.beforeEach(async function(): Promise { + await ctx.page.evaluate(` + window.progressStack = []; + window.term.reset(); + window.progressAddon?.dispose(); + window.progressAddon = new ProgressAddon(); + window.term.loadAddon(window.progressAddon); + window.progressAddon.register((state, value) => window.progressStack.push({state, value})); + `); + }); + + test('initial values should be 0;0', async () => { + deepStrictEqual(await ctx.page.evaluate('window.progressAddon.progress'), {state: 0, value: 0}); + }); + test('state 0: remove', async () => { + // no value + await ctx.proxy.write('\x1b]9;4;0\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 0, value: 0}]); + // value ignored + await ctx.proxy.write('\x1b]9;4;0;12\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 0, value: 0}, {state: 0, value: 0}]); + }); + test('state 1: set', async () => { + // set 10% + await ctx.proxy.write('\x1b]9;4;1;10\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 10}]); + // set 50% + await ctx.proxy.write('\x1b]9;4;1;50\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 10}, {state: 1, value: 50}]); + // set 23% + await ctx.proxy.write('\x1b]9;4;1;23\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 10}, {state: 1, value: 50}, {state: 1, value: 23}]); + }); + test('state 1: set - special sequence handling', async () => { + // missing progress value defaults to 0 + await ctx.proxy.write('\x1b]9;4;1\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 0}]); + // malformed progress value get ignored + await ctx.proxy.write('\x1b]9;4;1;12x\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 0}]); + // out of bounds gets clamped to 100 + await ctx.proxy.write('\x1b]9;4;1;123\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), [{state: 1, value: 0}, {state: 1, value: 100}]); + }); + test('state 2: error - preserve previous value on empty/0', async () => { + // set value to 12 + await ctx.proxy.write('\x1b]9;4;1;12\x1b\\'); + // omitted/empty/0 value emits previous value + await ctx.proxy.write('\x1b]9;4;2\x1b\\'); + await ctx.proxy.write('\x1b]9;4;2;\x1b\\'); + await ctx.proxy.write('\x1b]9;4;2;0\x1b\\'); + deepStrictEqual( + await ctx.page.evaluate('window.progressStack'), + [{state: 1, value: 12}, {state: 2, value: 12}, {state: 2, value: 12}, {state: 2, value: 12}] + ); + }); + test('state 2: error - with new value', async () => { + // set value to 12 + await ctx.proxy.write('\x1b]9;4;1;12\x1b\\'); + // new value updates clamped + await ctx.proxy.write('\x1b]9;4;2;25\x1b\\'); + await ctx.proxy.write('\x1b]9;4;2;123\x1b\\'); + deepStrictEqual( + await ctx.page.evaluate('window.progressStack'), + [{state: 1, value: 12}, {state: 2, value: 25}, {state: 2, value: 100}] + ); + }); + test('state 3: indeterminate - keeps value untouched', async () => { + // set value to 12 + await ctx.proxy.write('\x1b]9;4;1;12\x1b\\'); + // new value updates clamped + await ctx.proxy.write('\x1b]9;4;3\x1b\\'); + await ctx.proxy.write('\x1b]9;4;3;123\x1b\\'); + deepStrictEqual( + await ctx.page.evaluate('window.progressStack'), + [{state: 1, value: 12}, {state: 3, value: 12}, {state: 3, value: 12}] + ); + }); + test('state 4: pause - preserve previous value on empty/0', async () => { + // set value to 12 + await ctx.proxy.write('\x1b]9;4;1;12\x1b\\'); + // omitted/empty/0 value emits previous value + await ctx.proxy.write('\x1b]9;4;4\x1b\\'); + await ctx.proxy.write('\x1b]9;4;4;\x1b\\'); + await ctx.proxy.write('\x1b]9;4;4;0\x1b\\'); + deepStrictEqual( + await ctx.page.evaluate('window.progressStack'), + [{state: 1, value: 12}, {state: 4, value: 12}, {state: 4, value: 12}, {state: 4, value: 12}] + ); + }); + test('state 4: pause - with new value', async () => { + // set value to 12 + await ctx.proxy.write('\x1b]9;4;1;12\x1b\\'); + // new value updates clamped + await ctx.proxy.write('\x1b]9;4;4;25\x1b\\'); + await ctx.proxy.write('\x1b]9;4;4;123\x1b\\'); + deepStrictEqual( + await ctx.page.evaluate('window.progressStack'), + [{state: 1, value: 12}, {state: 4, value: 25}, {state: 4, value: 100}] + ); + }); + test('invalid sequences should not emit anything', async () => { + // illegal state + await ctx.proxy.write('\x1b]9;4;5;12\x1b\\'); + // illegal chars in value + await ctx.proxy.write('\x1b]9;4;1; 123xxxx\x1b\\'); + deepStrictEqual(await ctx.page.evaluate('window.progressStack'), []); + }); +}); diff --git a/addons/addon-progress/test/playwright.config.ts b/addons/addon-progress/test/playwright.config.ts new file mode 100644 index 00000000..22834be1 --- /dev/null +++ b/addons/addon-progress/test/playwright.config.ts @@ -0,0 +1,35 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '.', + timeout: 10000, + projects: [ + { + name: 'ChromeStable', + use: { + browserName: 'chromium', + channel: 'chrome' + } + }, + { + name: 'FirefoxStable', + use: { + browserName: 'firefox' + } + }, + { + name: 'WebKit', + use: { + browserName: 'webkit' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm run start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/addon-progress/test/tsconfig.json b/addons/addon-progress/test/tsconfig.json new file mode 100644 index 00000000..cff27705 --- /dev/null +++ b/addons/addon-progress/test/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ESNext", + "lib": [ + "es2021", + ], + "rootDir": ".", + "outDir": "../out-test", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "types": [ + "../../../node_modules/@types/node" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + }, + { + "path": "../../../test/playwright" + } + ] +} diff --git a/addons/addon-progress/tsconfig.json b/addons/addon-progress/tsconfig.json new file mode 100644 index 00000000..2d820dd1 --- /dev/null +++ b/addons/addon-progress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./test" } + ] +} diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts new file mode 100644 index 00000000..f302055c --- /dev/null +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; + +declare module '@xterm/addon-progress' { + /** xterm.js addon providing an interface for ConEmu's progress sequence */ + export class ProgressAddon implements ITerminalAddon { + constructor(); + public activate(terminal: Terminal): void; + public dispose(): void; + + /** register progress handler */ + public register(handler: ProgressHandler): IDisposable; + + /** getter / setter for current progress */ + public progress: IProgress; + } + + /** progress object interface */ + export interface IProgress { + state: 0 | 1 | 2 | 3 | 4; + value: number; + } + + /** Progress handler type */ + export type ProgressHandler = (state: 0 | 1 | 2 | 3 | 4, value: number) => void; +} diff --git a/addons/addon-progress/webpack.config.js b/addons/addon-progress/webpack.config.js new file mode 100644 index 00000000..a612e29a --- /dev/null +++ b/addons/addon-progress/webpack.config.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'ProgressAddon'; +const mainFile = 'addon-progress.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd', + // Force usage of globalThis instead of global / self. (This is cross-env compatible) + globalObject: 'globalThis', + }, + mode: 'production' +}; diff --git a/bin/test_integration.js b/bin/test_integration.js index 00e5b57c..68f3ffdb 100644 --- a/bin/test_integration.js +++ b/bin/test_integration.js @@ -25,6 +25,7 @@ const addons = [ 'clipboard', 'fit', 'image', + 'progress', 'search', 'serialize', 'unicode-graphemes', diff --git a/demo/client.ts b/demo/client.ts index 6e0be22f..27443110 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,6 +21,7 @@ import { AttachAddon } from '@xterm/addon-attach'; import { ClipboardAddon } from '@xterm/addon-clipboard'; import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; +import { ProgressAddon } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; import { WebLinksAddon } from '@xterm/addon-web-links'; @@ -35,6 +36,7 @@ export interface IWindowWithTerminal extends Window { ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention ImageAddon?: typeof ImageAddon; // eslint-disable-line @typescript-eslint/naming-convention + ProgressAddon?: typeof ProgressAddon; // eslint-disable-line @typescript-eslint/naming-convention SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention @@ -52,7 +54,7 @@ let socket; let pid; let autoResize: boolean = true; -type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; +type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; @@ -63,13 +65,14 @@ interface IDemoAddon { T extends 'fit' ? typeof FitAddon : T extends 'image' ? typeof ImageAddonType : T extends 'ligatures' ? typeof LigaturesAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never + T extends 'progress' ? typeof ProgressAddon : + T extends 'search' ? typeof SearchAddon : + T extends 'serialize' ? typeof SerializeAddon : + T extends 'webLinks' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : + T extends 'webgl' ? typeof WebglAddon : + never ); instance?: ( T extends 'attach' ? AttachAddon : @@ -77,13 +80,14 @@ interface IDemoAddon { T extends 'fit' ? FitAddon : T extends 'image' ? ImageAddonType : T extends 'ligatures' ? LigaturesAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never + T extends 'progress' ? ProgressAddon : + T extends 'search' ? SearchAddon : + T extends 'serialize' ? SerializeAddon : + T extends 'webLinks' ? WebLinksAddon : + T extends 'unicode11' ? Unicode11Addon : + T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : + T extends 'webgl' ? WebglAddon : + never ); } @@ -92,6 +96,7 @@ const addons: { [T in AddonType]: IDemoAddon } = { clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, image: { name: 'image', ctor: ImageAddon, canChange: true }, + progress: { name: 'progress', ctor: ProgressAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, @@ -213,6 +218,7 @@ if (document.location.pathname === '/test') { window.ClipboardAddon = ClipboardAddon; window.FitAddon = FitAddon; window.ImageAddon = ImageAddon; + window.ProgressAddon = ProgressAddon; window.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; window.Unicode11Addon = Unicode11Addon; @@ -245,6 +251,7 @@ if (document.location.pathname === '/test') { addVtButtons(); initImageAddonExposed(); testEvents(); + progressButtons(); } function createTerminal(): void { @@ -271,6 +278,7 @@ function createTerminal(): void { addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.image.instance = new ImageAddon(); + addons.progress.instance = new ProgressAddon(); addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); addons.clipboard.instance = new ClipboardAddon(); try { // try to start with webgl renderer (might throw on older safari/webkit) @@ -281,6 +289,7 @@ function createTerminal(): void { addons.webLinks.instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); + typedTerm.loadAddon(addons.progress.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); @@ -1423,3 +1432,44 @@ function testEvents(): void { document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); } + + +function progressButtons(): void { + const STATES = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' }; + const COLORS = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' }; + + function progressHandler(state: number, value: number) { + // Simulate windows taskbar hack by windows terminal: + // Since the taskbar has no means to indicate error/pause state other than by coloring + // the current progress, we move 0 to 10% and distribute higher values in the remaining 90 % + // NOTE: This most likely not what you want to do for other progress indicators, + // that have a proper visual state for error/paused + value = Math.min(10 + value * 0.9, 100); + document.getElementById('progress-percent').style.width = `${value}%`; + document.getElementById('progress-percent').style.backgroundColor = COLORS[state]; + document.getElementById('progress-state').innerText = `State: ${STATES[state]}`; + + document.getElementById('progress-percent').style.display = state === 3 ? 'none' : 'block'; + document.getElementById('progress-indeterminate').style.display = state === 3 ? 'block' : 'none'; + } + + const progressAddon = addons.progress.instance; + progressAddon.register(progressHandler); + + // apply initial state once to make it visible on page load + const {state, value} = progressAddon.progress; + progressHandler(state, value); + + document.getElementById('progress-run').addEventListener('click', async () => { + term.write('\x1b]9;4;0\x1b\\'); + for (let i = 0; i <= 100; i += 5) { + term.write(`\x1b]9;4;1;${i}\x1b\\`); + await new Promise(res => setTimeout(res, 200)); + } + }); + document.getElementById('progress-0').addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\')); + document.getElementById('progress-1').addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\')); + document.getElementById('progress-2').addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\')); + document.getElementById('progress-3').addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\')); + document.getElementById('progress-4').addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\')); +} diff --git a/demo/index.html b/demo/index.html index 3d46f61c..731f92ec 100644 --- a/demo/index.html +++ b/demo/index.html @@ -117,6 +117,47 @@
Events Test
+ +
Progress Addon
+
+
+
+
+
+
+ +
+
+
+
+
State:
diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 5569bd1d..405dbf0f 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -10,6 +10,7 @@ "@xterm/addon-clipboard": ["../addons/addon-clipboard"], "@xterm/addon-fit": ["../addons/addon-fit"], "@xterm/addon-image": ["../addons/addon-image"], + "@xterm/addon-progress": ["../addons/addon-progress"], "@xterm/addon-search": ["../addons/addon-search"], "@xterm/addon-serialize": ["../addons/addon-serialize"], "@xterm/addon-web-links": ["../addons/addon-web-links"], diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ca3b7a7..493ec6c9 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -11,6 +11,7 @@ { "path": "./addons/addon-fit" }, { "path": "./addons/addon-image" }, { "path": "./addons/addon-ligatures" }, + { "path": "./addons/addon-progress" }, { "path": "./addons/addon-search" }, { "path": "./addons/addon-serialize" }, { "path": "./addons/addon-unicode11" }, From 80c57221ef378cd3682729fc23bd83fe030d784f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 15 Dec 2024 19:18:46 +0100 Subject: [PATCH 028/402] fix esbuild script --- bin/esbuild.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 9ed69eda..1727defe 100644 --- a/bin/esbuild.mjs +++ b/bin/esbuild.mjs @@ -136,6 +136,7 @@ if (config.addon) { "@xterm/addon-clipboard": "./addons/addon-clipboard/lib/addon-clipboard.mjs", "@xterm/addon-fit": "./addons/addon-fit/lib/addon-fit.mjs", "@xterm/addon-image": "./addons/addon-image/lib/addon-image.mjs", + "@xterm/addon-progress": "./addons/addon-progress/lib/addon-progress.mjs", "@xterm/addon-search": "./addons/addon-search/lib/addon-search.mjs", "@xterm/addon-serialize": "./addons/addon-serialize/lib/addon-serialize.mjs", "@xterm/addon-web-links": "./addons/addon-web-links/lib/addon-web-links.mjs", From f0fd3683d537c7cac0991b5e86ad540fc813bdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 15 Dec 2024 19:28:19 +0100 Subject: [PATCH 029/402] add to publish script --- bin/publish.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish.js b/bin/publish.js index 0a43b7c4..e2fcf9ad 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -46,6 +46,7 @@ const addonPackageDirs = [ path.resolve(__dirname, '../addons/addon-fit'), path.resolve(__dirname, '../addons/addon-image'), path.resolve(__dirname, '../addons/addon-ligatures'), + path.resolve(__dirname, '../addons/addon-progress'), path.resolve(__dirname, '../addons/addon-search'), path.resolve(__dirname, '../addons/addon-serialize'), path.resolve(__dirname, '../addons/addon-unicode11'), From 569e854a03d4ba902db53d80117863986e4c0181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 15 Dec 2024 19:32:47 +0100 Subject: [PATCH 030/402] add to workflow bundle --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f39e6b..c2bcf782 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,9 @@ jobs: ./addons/addon-ligatures/lib/* \ ./addons/addon-ligatures/out/* \ ./addons/addon-ligatures/out-*/* \ + ./addons/addon-progress/lib/* \ + ./addons/addon-progress/out/* \ + ./addons/addon-progress/out-*/* \ ./addons/addon-search/lib/* \ ./addons/addon-search/out/* \ ./addons/addon-search/out-*/* \ @@ -212,6 +215,8 @@ jobs: run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-fit - name: Integration tests (addon-image) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-image + - name: Integration tests (addon-progress) + run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-progress - name: Integration tests (addon-search) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-search - name: Integration tests (addon-serialize) From 94ca05c8d631b1ca797d6490f9008fb11deb8c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 15 Dec 2024 20:01:05 +0100 Subject: [PATCH 031/402] fix handler example in readme --- addons/addon-progress/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md index 192e1e5a..4f05e4aa 100644 --- a/addons/addon-progress/README.md +++ b/addons/addon-progress/README.md @@ -20,7 +20,7 @@ import { ProgressAddon } from '@xterm/addon-progress'; const terminal = new Terminal(); const progressAddon = new ProgressAddon(); terminal.loadAddon(progressAddon); -progressAddon.register({state, value} => { +progressAddon.register((state: number, value: number) => { // state: 0-4 integer (see below for meaning) // value: 0-100 integer (percent value) From 9824230cafffe8483ba90d7f6f7969da2d342b11 Mon Sep 17 00:00:00 2001 From: Jacob Bandes-Storch Date: Tue, 17 Dec 2024 12:20:55 -0800 Subject: [PATCH 032/402] Fix ReferenceError in publish.js --- bin/publish.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/publish.js b/bin/publish.js index 0a43b7c4..59771cd8 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -114,8 +114,7 @@ function checkAndPublishPackage(packageDir, repoCommit, peerDependencies) { stdio: 'inherit' }); if (result.status) { - error(`Spawn exited with code ${result.status}`); - process.exit(result.status); + throw new Error(`Spawn exited with code ${result.status}`); } return { isStableRelease, nextVersion }; From 9f9bb3c11c0d6d88c4d9d16ce70e9e1f3be73038 Mon Sep 17 00:00:00 2001 From: An Phi Date: Wed, 18 Dec 2024 01:26:47 -0500 Subject: [PATCH 033/402] bug: properly render the terminal when open() is called again --- src/browser/CoreBrowserTerminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 2e15b5f3..33ee94bc 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -400,7 +400,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } // If the terminal is already opened - if (this.element?.ownerDocument.defaultView && this._coreBrowserService) { + if (this.element?.ownerDocument.defaultView && this._coreBrowserService && this.element?.isConnected) { // Adjust the window if needed if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) { this._coreBrowserService.window = this.element.ownerDocument.defaultView; From b74ec267e2bcfd1c3582958cf7be29fff29cbd80 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:37:12 -0800 Subject: [PATCH 034/402] Blend cursor with background to support alpha in webgl Fixes #5241 --- demo/client.ts | 4 ++-- src/browser/services/ThemeService.ts | 2 +- test/playwright/SharedRendererTests.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 6e0be22f..02b9f7be 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -16,7 +16,7 @@ if ('WebAssembly' in window) { ImageAddon = imageAddon.ImageAddon; } -import { Terminal, ITerminalOptions, type IDisposable } from '@xterm/xterm'; +import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm'; import { AttachAddon } from '@xterm/addon-attach'; import { ClipboardAddon } from '@xterm/addon-clipboard'; import { FitAddon } from '@xterm/addon-fit'; @@ -131,7 +131,7 @@ const xtermjsTheme = { brightCyan: '#72F0FF', white: '#F8F8F8', brightWhite: '#FFFFFF' -}; +} satisfies ITheme; function setPadding(): void { term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; addons.fit.instance.fit(); diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts index 88ffd99d..0dc7fcbf 100644 --- a/src/browser/services/ThemeService.ts +++ b/src/browser/services/ThemeService.ts @@ -82,7 +82,7 @@ export class ThemeService extends Disposable implements IThemeService { const colors = this._colors; colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND); colors.background = parseColor(theme.background, DEFAULT_BACKGROUND); - colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR); + colors.cursor = color.blend(colors.background, parseColor(theme.cursor, DEFAULT_CURSOR)); colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION); colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent); diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 4a1798ce..b0c5c499 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1248,6 +1248,14 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.proxy.scrollLines(-2); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); }); + test('#5241 cursor with alpha should blend color with background color', async () => { + const theme: ITheme = { + cursor: '#FF000080' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); + }); }); } From 3e998387fe61910b75e3e549c5d84e8e322c6978 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:51:17 -0800 Subject: [PATCH 035/402] Blend cursorAccent with background too Fixes #5241 --- src/browser/services/ThemeService.ts | 2 +- test/playwright/SharedRendererTests.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts index 0dc7fcbf..cd85e0ff 100644 --- a/src/browser/services/ThemeService.ts +++ b/src/browser/services/ThemeService.ts @@ -83,7 +83,7 @@ export class ThemeService extends Disposable implements IThemeService { colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND); colors.background = parseColor(theme.background, DEFAULT_BACKGROUND); colors.cursor = color.blend(colors.background, parseColor(theme.cursor, DEFAULT_CURSOR)); - colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + colors.cursorAccent = color.blend(colors.background, parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT)); colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION); colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent); colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent); diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index b0c5c499..998d51a6 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1256,6 +1256,16 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.proxy.focus(); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); }); + test.only('#5241 cursorAccent with alpha should blend color with background color', async () => { + const theme: ITheme = { + cursorAccent: '#FF000080' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + await ctx.value.proxy.write('■'); + await ctx.value.proxy.write('\x1b[1D'); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); + }); }); } From da22b0308ef6f53f62f5eb78b19fce735aa1a15f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:00:05 -0800 Subject: [PATCH 036/402] Make textarea readonly when disableStdin is set Fixes #5256 --- src/browser/CoreBrowserTerminal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 2e15b5f3..58da9344 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -437,7 +437,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this.screenElement.appendChild(this._helperContainer); fragment.appendChild(this.screenElement); - this.textarea = this._document.createElement('textarea'); + const textarea = this.textarea = this._document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); this.textarea.setAttribute('aria-label', Strings.promptLabel.get()); if (!Browser.isChromeOS) { @@ -449,6 +449,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); this.textarea.tabIndex = 0; + this._register(this.optionsService.onSpecificOptionChange('disableStdin', () => textarea.readOnly = this.optionsService.rawOptions.disableStdin)); + this.textarea.readOnly = this.optionsService.rawOptions.disableStdin; // Register the core browser service before the generic textarea handlers are registered so it // handles them first. Otherwise the renderers may use the wrong focus state. From 18c9eb196010ec082ad56adf7040ed2dca5fd391 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:01:30 -0800 Subject: [PATCH 037/402] Remove .only --- test/playwright/SharedRendererTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 998d51a6..1d37b5c1 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1256,7 +1256,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.proxy.focus(); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); }); - test.only('#5241 cursorAccent with alpha should blend color with background color', async () => { + test('#5241 cursorAccent with alpha should blend color with background color', async () => { const theme: ITheme = { cursorAccent: '#FF000080' }; From fb2e96e1b0693809ea6221113506033b4184a41c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:21:06 -0800 Subject: [PATCH 038/402] Add test button for DECSCUSR Part of #3293 --- demo/client.ts | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 02b9f7be..ceb3fab1 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1266,10 +1266,13 @@ function addVtButtons(): void { const element = document.createElement('button'); element.textContent = name; - writeCsi.split(''); - const prefix = writeCsi.length === 2 ? writeCsi[0] : ''; - const suffix = writeCsi[writeCsi.length - 1]; - element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); + const writeCsiSplit = writeCsi.split('|'); + const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : ''; + const suffix = writeCsiSplit[writeCsiSplit.length - 1]; + element.addEventListener(`click`, () => { + debugger; + term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)); + }); const desc = document.createElement('span'); desc.textContent = description; @@ -1281,22 +1284,23 @@ function addVtButtons(): void { } const vtFragment = document.createDocumentFragment(); const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = { - A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, - B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, - C: { label: 'CUF →', description: 'Cursor Forward Ps Times' }, - D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' }, - E: { label: 'CNL', description: 'Cursor Next Line Ps Times' }, - F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' }, - G: { label: 'CHA', description: 'Cursor Character Absolute' }, - H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 }, - I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' }, - J: { label: 'ED', description: 'Erase in Display' }, - '?J': { label: 'DECSED', description: 'Erase in Display' }, - K: { label: 'EL', description: 'Erase in Line' }, - '?K': { label: 'DECSEL', description: 'Erase in Line' }, - L: { label: 'IL', description: 'Insert Ps Line(s)' }, - M: { label: 'DL', description: 'Delete Ps Line(s)' }, - P: { label: 'DCH', description: 'Delete Ps Character(s)' } + A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, + B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, + C: { label: 'CUF →', description: 'Cursor Forward Ps Times' }, + D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' }, + E: { label: 'CNL', description: 'Cursor Next Line Ps Times' }, + F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' }, + G: { label: 'CHA', description: 'Cursor Character Absolute' }, + H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 }, + I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' }, + J: { label: 'ED', description: 'Erase in Display' }, + '?|J': { label: 'DECSED', description: 'Erase in Display' }, + K: { label: 'EL', description: 'Erase in Line' }, + '?|K': { label: 'DECSEL', description: 'Erase in Line' }, + L: { label: 'IL', description: 'Insert Ps Line(s)' }, + M: { label: 'DL', description: 'Delete Ps Line(s)' }, + P: { label: 'DCH', description: 'Delete Ps Character(s)' }, + ' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 } }; for (const s of Object.keys(buttonSpecs)) { const spec = buttonSpecs[s]; From b579649a392d6e178cb08948ef888316b4b91e3a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:34:05 -0800 Subject: [PATCH 039/402] Revert to cursor options after DECSCUSR 0 Fixes #3293 --- addons/addon-webgl/src/WebglRenderer.ts | 11 ++++--- demo/client.ts | 5 +-- src/browser/renderer/dom/DomRenderer.ts | 7 ++-- src/common/InputHandler.ts | 44 +++++++++++++++---------- src/common/TestUtils.test.ts | 2 ++ src/common/Types.ts | 2 ++ src/common/services/CoreService.ts | 2 ++ 7 files changed, 43 insertions(+), 30 deletions(-) diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 216b8404..6c270d12 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -354,7 +354,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } private _updateCursorBlink(): void { - if (this._terminal.options.cursorBlink) { + if (this._coreService.decPrivateModes.cursorBlink ?? this._terminal.options.cursorBlink) { this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => { this._requestRedrawCursor(); }, this._coreBrowserService); @@ -387,6 +387,7 @@ export class WebglRenderer extends Disposable implements IRenderer { let j: number; start = clamp(start, terminal.rows - 1, 0); end = clamp(end, terminal.rows - 1, 0); + const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? terminal.options.cursorStyle ?? 'block'; const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; const viewportRelativeCursorY = cursorY - terminal.buffer.ydisp; @@ -450,8 +451,7 @@ export class WebglRenderer extends Disposable implements IRenderer { x: cursorX, y: viewportRelativeCursorY, width: cell.getWidth(), - style: this._coreBrowserService.isFocused ? - (terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle, + style: this._coreBrowserService.isFocused ? cursorStyle : terminal.options.cursorInactiveStyle, cursorWidth: terminal.options.cursorWidth, dpr: this._devicePixelRatio }; @@ -459,9 +459,10 @@ export class WebglRenderer extends Disposable implements IRenderer { } if (x >= cursorX && x <= lastCursorX && ((this._coreBrowserService.isFocused && - (terminal.options.cursorStyle || 'block') === 'block') || + cursorStyle === 'block') || (this._coreBrowserService.isFocused === false && - terminal.options.cursorInactiveStyle === 'block'))) { + terminal.options.cursorInactiveStyle === 'block')) + ) { this._cellColorResolver.result.fg = Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK); this._cellColorResolver.result.bg = diff --git a/demo/client.ts b/demo/client.ts index ceb3fab1..f7503210 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1269,10 +1269,7 @@ function addVtButtons(): void { const writeCsiSplit = writeCsi.split('|'); const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : ''; const suffix = writeCsiSplit[writeCsiSplit.length - 1]; - element.addEventListener(`click`, () => { - debugger; - term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)); - }); + element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); const desc = document.createElement('span'); desc.textContent = description; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index f6fda22e..1de446c4 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -13,7 +13,7 @@ import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/se import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types'; import { color } from 'common/Color'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services'; +import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services'; import { Emitter } from 'vs/base/common/event'; @@ -59,6 +59,7 @@ export class DomRenderer extends Disposable implements IRenderer { @ICharSizeService private readonly _charSizeService: ICharSizeService, @IOptionsService private readonly _optionsService: IOptionsService, @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @IThemeService private readonly _themeService: IThemeService ) { @@ -437,8 +438,8 @@ export class DomRenderer extends Disposable implements IRenderer { const buffer = this._bufferService.buffer; const cursorAbsoluteY = buffer.ybase + buffer.y; const cursorX = Math.min(buffer.x, this._bufferService.cols - 1); - const cursorBlink = this._optionsService.rawOptions.cursorBlink; - const cursorStyle = this._optionsService.rawOptions.cursorStyle; + const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink; + const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle; const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle; for (let y = start; y <= end; y++) { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b94d7855..e9b99138 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2714,7 +2714,7 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps SP q Set cursor style (DECSCUSR, VT520). - * Ps = 0 -> blinking block. + * Ps = 0 -> reset to option. * Ps = 1 -> blinking block (default). * Ps = 2 -> steady block. * Ps = 3 -> blinking underline. @@ -2724,7 +2724,8 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style." * Supported cursor styles: - * - empty, 0 or 1: steady block + * - empty, 0: reset to option + * - 1: steady block * - 2: blink block * - 3: steady underline * - 4: blink underline @@ -2732,23 +2733,30 @@ export class InputHandler extends Disposable implements IInputHandler { * - 6: blink bar */ public setCursorStyle(params: IParams): boolean { - const param = params.params[0] || 1; - switch (param) { - case 1: - case 2: - this._optionsService.options.cursorStyle = 'block'; - break; - case 3: - case 4: - this._optionsService.options.cursorStyle = 'underline'; - break; - case 5: - case 6: - this._optionsService.options.cursorStyle = 'bar'; - break; + const param = params.params[0] ?? 1; + if (param === 0) { + this._coreService.decPrivateModes.cursorStyle = undefined; + this._coreService.decPrivateModes.cursorBlink = undefined; + } else { + switch (param) { + case 0: + break; + case 1: + case 2: + this._coreService.decPrivateModes.cursorStyle = 'block'; + break; + case 3: + case 4: + this._coreService.decPrivateModes.cursorStyle = 'underline'; + break; + case 5: + case 6: + this._coreService.decPrivateModes.cursorStyle = 'bar'; + break; + } + const isBlinking = param % 2 === 1; + this._coreService.decPrivateModes.cursorBlink = isBlinking; } - const isBlinking = param % 2 === 1; - this._optionsService.options.cursorBlink = isBlinking; return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 8c9634db..127e1f24 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -89,6 +89,8 @@ export class MockCoreService implements ICoreService { applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + cursorBlink: undefined, + cursorStyle: undefined, origin: false, reverseWraparound: false, sendFocus: false, diff --git a/src/common/Types.ts b/src/common/Types.ts index 289aa1f6..c254d330 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -268,6 +268,8 @@ export interface IDecPrivateModes { applicationCursorKeys: boolean; applicationKeypad: boolean; bracketedPasteMode: boolean; + cursorBlink: boolean | undefined; + cursorStyle: CursorStyle | undefined; origin: boolean; reverseWraparound: boolean; sendFocus: boolean; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 9c41fc1a..5bee6356 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -17,6 +17,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + cursorBlink: undefined, + cursorStyle: undefined, origin: false, reverseWraparound: false, sendFocus: false, From 82598e94a9cf766172c24b683a49020e5b749fc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:36:32 -0800 Subject: [PATCH 040/402] Fix default param DECSCUSR handling --- src/common/InputHandler.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index e9b99138..de8664e9 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2733,14 +2733,12 @@ export class InputHandler extends Disposable implements IInputHandler { * - 6: blink bar */ public setCursorStyle(params: IParams): boolean { - const param = params.params[0] ?? 1; + const param = params.length === 0 ? 1 : params.params[0]; if (param === 0) { this._coreService.decPrivateModes.cursorStyle = undefined; this._coreService.decPrivateModes.cursorBlink = undefined; } else { switch (param) { - case 0: - break; case 1: case 2: this._coreService.decPrivateModes.cursorStyle = 'block'; From f824083324052ae0387008da999a1f8c3df404ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:37:48 -0800 Subject: [PATCH 041/402] Correct @vt docs --- src/common/InputHandler.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index de8664e9..8308e6de 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2724,13 +2724,13 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style." * Supported cursor styles: - * - empty, 0: reset to option - * - 1: steady block - * - 2: blink block - * - 3: steady underline - * - 4: blink underline - * - 5: steady bar - * - 6: blink bar + * - 0: reset to option + * - empty, 1: blinking block + * - 2: steady block + * - 3: blinking underline + * - 4: steady underline + * - 5: blinking bar + * - 6: steady bar */ public setCursorStyle(params: IParams): boolean { const param = params.length === 0 ? 1 : params.params[0]; From d44f7818c4bfbcdeb006f4c16174462795052047 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:43:38 -0800 Subject: [PATCH 042/402] Fix test to assert new behavior --- src/common/InputHandler.test.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index baae735c..1280417b 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -202,38 +202,38 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { inputHandler.setCursorStyle(Params.fromArray([0])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], true); + assert.equal(coreService.decPrivateModes.cursorStyle, undefined); + assert.equal(coreService.decPrivateModes.cursorBlink, undefined); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([1])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], true); + assert.equal(coreService.decPrivateModes.cursorStyle, 'block'); + assert.equal(coreService.decPrivateModes.cursorBlink, true); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([2])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], false); + assert.equal(coreService.decPrivateModes.cursorStyle, 'block'); + assert.equal(coreService.decPrivateModes.cursorBlink, false); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([3])); - assert.equal(optionsService.options['cursorStyle'], 'underline'); - assert.equal(optionsService.options['cursorBlink'], true); + assert.equal(coreService.decPrivateModes.cursorStyle, 'underline'); + assert.equal(coreService.decPrivateModes.cursorBlink, true); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([4])); - assert.equal(optionsService.options['cursorStyle'], 'underline'); - assert.equal(optionsService.options['cursorBlink'], false); + assert.equal(coreService.decPrivateModes.cursorStyle, 'underline'); + assert.equal(coreService.decPrivateModes.cursorBlink, false); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([5])); - assert.equal(optionsService.options['cursorStyle'], 'bar'); - assert.equal(optionsService.options['cursorBlink'], true); + assert.equal(coreService.decPrivateModes.cursorStyle, 'bar'); + assert.equal(coreService.decPrivateModes.cursorBlink, true); optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([6])); - assert.equal(optionsService.options['cursorStyle'], 'bar'); - assert.equal(optionsService.options['cursorBlink'], false); + assert.equal(coreService.decPrivateModes.cursorStyle, 'bar'); + assert.equal(coreService.decPrivateModes.cursorBlink, false); }); }); describe('setMode', () => { From ef67b42fbd80a584f3897c5614cda8bf13ffadc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:59:00 -0800 Subject: [PATCH 043/402] Demo test button for common ligatures Part of #5231 --- demo/client.ts | 13 +++++++++++++ demo/index.html | 3 +++ 2 files changed, 16 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index f7503210..7a1ac72b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -240,6 +240,7 @@ if (document.location.pathname === '/test') { document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); + document.getElementById('ligatures-test').addEventListener('click', ligaturesTest); document.getElementById('weblinks-test').addEventListener('click', testWeblinks); document.getElementById('bce').addEventListener('click', coloredErase); addVtButtons(); @@ -1307,6 +1308,18 @@ function addVtButtons(): void { document.querySelector('#vt-container').appendChild(vtFragment); } +function ligaturesTest(): void { + term.write([ + '', + '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-', + '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', + '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', + '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', + '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', + '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <---> <----> <=> <==> <===> <====> :: ::: __', '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', - '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| ', '--->', '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', - '<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->', - '<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==', - ':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>', - '++', '+++', '', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::', + '<~~', '', '/>', '~~>', '==', '!=', '/=', '~=', '<>', '===', '!==', '!===', + '<:', ':=', '*=', '*+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+*', '=*', '=:', ':>', + '/*', '*/', '+++', ' ---> * <== <=== <<= <= => =>> ==> ===> >= >>= - * <-> <--> <---> <----> <=> <==> <===> <====> --------> - * <~~ <~ ~> ~~> :: ::: == != === !== - * := :- :+ <* <*> *> <| <|> |> +: -: =: :> - * ++ +++ <---> <----> <=> <==> <===> <====> :: ::: + * <~~ /> ~~> == != /= ~= <> === !== !=== + * <: := *= *+ <* <*> *> <| <|> |> +* =* =: :> + * /* +++ ---> ->- >- >>-', - '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', - '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', - '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', - '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', - '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| ---> ->- >- >>-', + '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', + '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', + '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', + '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', + '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| - +