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 01/15] 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 8c47598205..acd85af60a 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 0000000000..3063f07d55 --- /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 0000000000..d2fb3bdcc4 --- /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 0000000000..447eb79f4d --- /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 0000000000..192e1e5af5 --- /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 0000000000..57a06d682b --- /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 0000000000..6065c5c54f --- /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 0000000000..8a90d6e358 --- /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 0000000000..656b7f957e --- /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 0000000000..22834be116 --- /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 0000000000..cff277055b --- /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 0000000000..2d820dd1a6 --- /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 0000000000..f302055ca5 --- /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 0000000000..a612e29a79 --- /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 00e5b57c24..68f3ffdb2e 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 6e0be22f57..2744311001 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 3d46f61cab..731f92ecc6 100644 --- a/demo/index.html +++ b/demo/index.html @@ -117,6 +117,47 @@

Test

Events Test
+ +
Progress Addon
+
+
+
+
+
+
+ +
+
+
+
+
State:
diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 5569bd1d46..405dbf0ff6 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 7ca3b7a791..493ec6c9da 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 02/15] fix esbuild script --- bin/esbuild.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 9ed69edab8..1727defe54 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 03/15] add to publish script --- bin/publish.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish.js b/bin/publish.js index 0a43b7c483..e2fcf9add2 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 04/15] 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 d2f39e6b88..c2bcf782a7 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 05/15] 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 192e1e5af5..4f05e4aa6f 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 5badbd3a6f01e1a7eda5d96da588168233768969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 18:22:27 +0100 Subject: [PATCH 06/15] import ProgressState type to d.ts --- addons/addon-progress/src/ProgressAddon.ts | 2 +- addons/addon-progress/typings/addon-progress.d.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 6065c5c54f..5bee83d61c 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -7,7 +7,7 @@ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { ProgressAddon as IProgressApi, IProgress, ProgressHandler } from '@xterm/addon-progress'; -const enum ProgressState { +export const enum ProgressState { REMOVE = 0, SET = 1, ERROR = 2, diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index f302055ca5..bd80eeee9f 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -4,6 +4,7 @@ */ import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { ProgressState } from '../src/ProgressAddon'; declare module '@xterm/addon-progress' { /** xterm.js addon providing an interface for ConEmu's progress sequence */ @@ -21,10 +22,10 @@ declare module '@xterm/addon-progress' { /** progress object interface */ export interface IProgress { - state: 0 | 1 | 2 | 3 | 4; + state: ProgressState; value: number; } /** Progress handler type */ - export type ProgressHandler = (state: 0 | 1 | 2 | 3 | 4, value: number) => void; + export type ProgressHandler = (state: ProgressState, value: number) => void; } From d7cebc312eb1301983dbe4b35ecb7968c39671b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 19:21:51 +0100 Subject: [PATCH 07/15] use event from xterm.js instead of own impl --- addons/addon-progress/src/ProgressAddon.ts | 47 +++++-------------- addons/addon-progress/src/tsconfig.json | 9 +++- .../typings/addon-progress.d.ts | 8 +--- addons/addon-progress/webpack.config.js | 9 ++++ demo/client.ts | 11 +++-- 5 files changed, 38 insertions(+), 46 deletions(-) diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 5bee83d61c..5f661a576f 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -4,7 +4,9 @@ */ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; -import type { ProgressAddon as IProgressApi, IProgress, ProgressHandler } from '@xterm/addon-progress'; +import type { ProgressAddon as IProgressApi, IProgress } from '@xterm/addon-progress'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; export const enum ProgressState { @@ -32,19 +34,14 @@ function toInt(s: string): number { } -export class ProgressAddon implements ITerminalAddon, IProgressApi { - private _seqHandler: IDisposable | undefined; +export class ProgressAddon extends Disposable implements ITerminalAddon, IProgressApi { private _st: ProgressState = ProgressState.REMOVE; private _pr = 0; - private _handlers: ProgressHandler[] = []; - - public dispose(): void { - this._seqHandler?.dispose(); - this._handlers.length = 0; - } + private readonly _onChange = this._register(new Emitter()); + public readonly onChange = this._onChange.event; public activate(terminal: Terminal): void { - this._seqHandler = terminal.parser.registerOscHandler(9, data => { + this._register(terminal.parser.registerOscHandler(9, data => { if (!data.startsWith('4;')) { return false; } @@ -77,20 +74,7 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { 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 { @@ -98,17 +82,12 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { } 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 { + if (progress.state < 0 || progress.state > 4) { console.warn(`progress state out of bounds, not applied`); + return; } + this._st = progress.state; + this._pr = Math.min(Math.max(progress.value, 0), 100); + this._onChange.fire({ state: this._st, value: this._pr }); } } diff --git a/addons/addon-progress/src/tsconfig.json b/addons/addon-progress/src/tsconfig.json index 8a90d6e358..ffc7b01a58 100644 --- a/addons/addon-progress/src/tsconfig.json +++ b/addons/addon-progress/src/tsconfig.json @@ -12,12 +12,16 @@ "removeComments": true, "strict": true, "types": [ - "../../../node_modules/@types/mocha" + "../../../node_modules/@types/mocha", + "../../../src/vs/typings/thenable" ], "paths": { "browser/*": [ "../../../src/browser/*" ], + "vs/*": [ + "../../../src/vs/*" + ], "@xterm/addon-progress": [ "../typings/addon-progress.d.ts" ] @@ -30,6 +34,9 @@ "references": [ { "path": "../../../src/browser" + }, + { + "path": "../../../src/vs" } ] } diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index bd80eeee9f..ba3d0da27f 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IDisposable, IEvent } from '@xterm/xterm'; import type { ProgressState } from '../src/ProgressAddon'; declare module '@xterm/addon-progress' { @@ -13,8 +13,7 @@ declare module '@xterm/addon-progress' { public activate(terminal: Terminal): void; public dispose(): void; - /** register progress handler */ - public register(handler: ProgressHandler): IDisposable; + public readonly onChange: IEvent; /** getter / setter for current progress */ public progress: IProgress; @@ -25,7 +24,4 @@ declare module '@xterm/addon-progress' { state: ProgressState; value: number; } - - /** Progress handler type */ - export type ProgressHandler = (state: ProgressState, value: number) => void; } diff --git a/addons/addon-progress/webpack.config.js b/addons/addon-progress/webpack.config.js index a612e29a79..1bf29bfd2c 100644 --- a/addons/addon-progress/webpack.config.js +++ b/addons/addon-progress/webpack.config.js @@ -21,6 +21,15 @@ module.exports = { } ] }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('../../out/common'), + browser: path.resolve('../../out/browser'), + vs: path.resolve('../../out/vs') + } + }, output: { filename: mainFile, path: path.resolve('./lib'), diff --git a/demo/client.ts b/demo/client.ts index 080270976e..0df2d57415 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,7 +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 { ProgressAddon, IProgress } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; import { WebLinksAddon } from '@xterm/addon-web-links'; @@ -1454,7 +1454,8 @@ 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) { + function progressHandler(progress: IProgress) { + let {state, value} = progress; // 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 % @@ -1470,11 +1471,11 @@ function progressButtons(): void { } const progressAddon = addons.progress.instance; - progressAddon.register(progressHandler); + progressAddon.onChange(progressHandler); // apply initial state once to make it visible on page load - const {state, value} = progressAddon.progress; - progressHandler(state, value); + const initialProgress = progressAddon.progress; + progressHandler(initialProgress); document.getElementById('progress-run').addEventListener('click', async () => { term.write('\x1b]9;4;0\x1b\\'); From 828603cfab84f1642024fafbfda560bed3b6e9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 19:24:14 +0100 Subject: [PATCH 08/15] fix integration test --- addons/addon-progress/test/ProgressAddon.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-progress/test/ProgressAddon.test.ts b/addons/addon-progress/test/ProgressAddon.test.ts index 656b7f957e..792c0445de 100644 --- a/addons/addon-progress/test/ProgressAddon.test.ts +++ b/addons/addon-progress/test/ProgressAddon.test.ts @@ -25,7 +25,7 @@ test.describe('ProgressAddon', () => { window.progressAddon?.dispose(); window.progressAddon = new ProgressAddon(); window.term.loadAddon(window.progressAddon); - window.progressAddon.register((state, value) => window.progressStack.push({state, value})); + window.progressAddon.onChange(progress => window.progressStack.push(progress)); `); }); From c207edd49cf38c4fed812fd5f13af150db47f9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 19:25:47 +0100 Subject: [PATCH 09/15] make linter happy --- addons/addon-progress/src/ProgressAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 5f661a576f..c1468a5149 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { Terminal, ITerminalAddon } from '@xterm/xterm'; import type { ProgressAddon as IProgressApi, IProgress } from '@xterm/addon-progress'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; From f516346e090efc07092ca7a61467066f0aac7858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 20:36:02 +0100 Subject: [PATCH 10/15] fix docs --- addons/addon-progress/README.md | 11 +++++------ demo/client.ts | 7 +++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md index 4f05e4aa6f..43134ae89a 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: number, value: number) => { +progressAddon.onChange({state, value}: IProgress) => { // state: 0-4 integer (see below for meaning) // value: 0-100 integer (percent value) @@ -69,11 +69,10 @@ The addon resolves most of those semantic nuances and will provide these ready-t ### 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;` +- `public readonly onChange: IEvent` \ + Event to register your actual progress handler, where you gonna do the progress visualisation. + The handler will get called upon valid progress sequences with a progress argument as `({state, value}) => {}`. +- `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 diff --git a/demo/client.ts b/demo/client.ts index 0df2d57415..6f44865890 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1454,13 +1454,12 @@ 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(progress: IProgress) { - let {state, value} = progress; + function progressHandler({state, value}: IProgress) { // 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 + // NOTE: This is 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]; From 6a232765f454149bd82dbc1f235e935b154b4766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 7 Jan 2025 21:22:40 +0100 Subject: [PATCH 11/15] borrow emitter ctor from xterm --- addons/addon-progress/src/ProgressAddon.ts | 26 ++++++++++++------- .../typings/addon-progress.d.ts | 7 ++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index c1468a5149..465cff477b 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -3,10 +3,9 @@ * @license MIT */ -import type { Terminal, ITerminalAddon } from '@xterm/xterm'; +import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { ProgressAddon as IProgressApi, IProgress } from '@xterm/addon-progress'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import type { Emitter, Event } from 'vs/base/common/event'; export const enum ProgressState { @@ -34,14 +33,20 @@ function toInt(s: string): number { } -export class ProgressAddon extends Disposable implements ITerminalAddon, IProgressApi { +export class ProgressAddon implements ITerminalAddon, IProgressApi { + private _seqHandler: IDisposable | undefined; private _st: ProgressState = ProgressState.REMOVE; private _pr = 0; - private readonly _onChange = this._register(new Emitter()); - public readonly onChange = this._onChange.event; + private _onChange: Emitter | undefined; + public onChange: Event | undefined; + + public dispose(): void { + this._seqHandler?.dispose(); + this._onChange?.dispose(); + } public activate(terminal: Terminal): void { - this._register(terminal.parser.registerOscHandler(9, data => { + this._seqHandler = terminal.parser.registerOscHandler(9, data => { if (!data.startsWith('4;')) { return false; } @@ -74,7 +79,10 @@ export class ProgressAddon extends Disposable implements ITerminalAddon, IProgre break; } return true; - })); + }); + // FIXME: borrow emitter ctor from xterm, to be changed once #5283 is resolved + this._onChange = new (terminal as any)._core._onData.constructor(); + this.onChange = this._onChange!.event; } public get progress(): IProgress { @@ -88,6 +96,6 @@ export class ProgressAddon extends Disposable implements ITerminalAddon, IProgre } this._st = progress.state; this._pr = Math.min(Math.max(progress.value, 0), 100); - this._onChange.fire({ state: this._st, value: this._pr }); + this._onChange?.fire({ state: this._st, value: this._pr }); } } diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index ba3d0da27f..874e4a13cf 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -3,17 +3,18 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IDisposable, IEvent } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { Event } from 'vs/base/common/event'; import type { ProgressState } from '../src/ProgressAddon'; declare module '@xterm/addon-progress' { /** xterm.js addon providing an interface for ConEmu's progress sequence */ - export class ProgressAddon implements ITerminalAddon { + export class ProgressAddon implements ITerminalAddon, IDisposable { constructor(); public activate(terminal: Terminal): void; public dispose(): void; - public readonly onChange: IEvent; + public readonly onChange: Event | undefined; /** getter / setter for current progress */ public progress: IProgress; From 75e5cc19e02684ea4311d1db76f9edc8806a4a71 Mon Sep 17 00:00:00 2001 From: jerch Date: Thu, 9 Jan 2025 01:06:18 +0100 Subject: [PATCH 12/15] Update addons/addon-progress/typings/addon-progress.d.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../typings/addon-progress.d.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index 874e4a13cf..a2cb22413c 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -8,19 +8,42 @@ import type { Event } from 'vs/base/common/event'; import type { ProgressState } from '../src/ProgressAddon'; declare module '@xterm/addon-progress' { - /** xterm.js addon providing an interface for ConEmu's progress sequence */ + /** + * An xterm.js addon that provides an interface for ConEmu's progress + * sequence. + */ export class ProgressAddon implements ITerminalAddon, IDisposable { + + /** + * Creates a new progress addon + */ constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ public dispose(): void; + /** + * An event that fires when the tracked progress changes. + */ public readonly onChange: Event | undefined; - /** getter / setter for current progress */ + /** + * Gets or sets the current progress tracked by the addon. + */ public progress: IProgress; } - /** progress object interface */ + /** + * Progress tracked by the addon. + */ export interface IProgress { state: ProgressState; value: number; From 96a3be3ca8b9edbc2cdcc077e17e54529078e790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 9 Jan 2025 01:20:25 +0100 Subject: [PATCH 13/15] polish docs --- addons/addon-progress/README.md | 15 +-------------- addons/addon-progress/typings/addon-progress.d.ts | 3 +++ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md index 43134ae89a..a84a307cee 100644 --- a/addons/addon-progress/README.md +++ b/addons/addon-progress/README.md @@ -24,7 +24,7 @@ progressAddon.onChange({state, value}: IProgress) => { // state: 0-4 integer (see below for meaning) // value: 0-100 integer (percent value) - // do your visualisation based on state/progress here + // do your visualisation based on state/value here ... }); ``` @@ -65,16 +65,3 @@ The addon resolves most of those semantic nuances and will provide these ready-t - 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 readonly onChange: IEvent` \ - Event to register your actual progress handler, where you gonna do the progress visualisation. - The handler will get called upon valid progress sequences with a progress argument as `({state, value}) => {}`. -- `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/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index a2cb22413c..5d96f4226a 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -37,6 +37,9 @@ declare module '@xterm/addon-progress' { /** * Gets or sets the current progress tracked by the addon. + * This can also be used to reset a stuck progress indicator + * back to initial with `{state: 0, value: 0}` + * or to restore an indicator. */ public progress: IProgress; } From 810113235268fdcc27bc3088950dd8ff4bfec337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 9 Jan 2025 02:05:36 +0100 Subject: [PATCH 14/15] rename types, remove rc import in d.ts --- addons/addon-progress/README.md | 4 ++-- addons/addon-progress/src/ProgressAddon.ts | 24 +++++++++---------- .../typings/addon-progress.d.ts | 12 +++++----- demo/client.ts | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md index a84a307cee..bb61c92fce 100644 --- a/addons/addon-progress/README.md +++ b/addons/addon-progress/README.md @@ -15,12 +15,12 @@ npm install --save @xterm/addon-progress ```ts import { Terminal } from '@xterm/xterm'; -import { ProgressAddon } from '@xterm/addon-progress'; +import { ProgressAddon, IProgressState } from '@xterm/addon-progress'; const terminal = new Terminal(); const progressAddon = new ProgressAddon(); terminal.loadAddon(progressAddon); -progressAddon.onChange({state, value}: IProgress) => { +progressAddon.onChange({state, value}: IProgressState) => { // state: 0-4 integer (see below for meaning) // value: 0-100 integer (percent value) diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 465cff477b..24e5786068 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -4,11 +4,11 @@ */ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; -import type { ProgressAddon as IProgressApi, IProgress } from '@xterm/addon-progress'; +import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress'; import type { Emitter, Event } from 'vs/base/common/event'; -export const enum ProgressState { +const enum ProgressType { REMOVE = 0, SET = 1, ERROR = 2, @@ -35,10 +35,10 @@ function toInt(s: string): number { export class ProgressAddon implements ITerminalAddon, IProgressApi { private _seqHandler: IDisposable | undefined; - private _st: ProgressState = ProgressState.REMOVE; + private _st: ProgressType = ProgressType.REMOVE; private _pr = 0; - private _onChange: Emitter | undefined; - public onChange: Event | undefined; + private _onChange: Emitter | undefined; + public onChange: Event | undefined; public dispose(): void { this._seqHandler?.dispose(); @@ -62,19 +62,19 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { const pr = toInt(parts[2]); switch (st) { - case ProgressState.REMOVE: + case ProgressType.REMOVE: this.progress = { state: st, value: 0 }; break; - case ProgressState.SET: + case ProgressType.SET: if (pr < 0) return true; // faulty sequence, just exit this.progress = { state: st, value: pr }; break; - case ProgressState.ERROR: - case ProgressState.PAUSE: + case ProgressType.ERROR: + case ProgressType.PAUSE: if (pr < 0) return true; // faulty sequence, just exit this.progress = { state: st, value: pr || this._pr }; break; - case ProgressState.INDETERMINATE: + case ProgressType.INDETERMINATE: this.progress = { state: st, value: this._pr }; break; } @@ -85,11 +85,11 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { this.onChange = this._onChange!.event; } - public get progress(): IProgress { + public get progress(): IProgressState { return { state: this._st, value: this._pr }; } - public set progress(progress: IProgress) { + public set progress(progress: IProgressState) { if (progress.state < 0 || progress.state > 4) { console.warn(`progress state out of bounds, not applied`); return; diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index 5d96f4226a..923a2aa39b 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -5,7 +5,7 @@ import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { Event } from 'vs/base/common/event'; -import type { ProgressState } from '../src/ProgressAddon'; + declare module '@xterm/addon-progress' { /** @@ -33,7 +33,7 @@ declare module '@xterm/addon-progress' { /** * An event that fires when the tracked progress changes. */ - public readonly onChange: Event | undefined; + public readonly onChange: Event | undefined; /** * Gets or sets the current progress tracked by the addon. @@ -41,14 +41,14 @@ declare module '@xterm/addon-progress' { * back to initial with `{state: 0, value: 0}` * or to restore an indicator. */ - public progress: IProgress; + public progress: IProgressState; } /** - * Progress tracked by the addon. + * Progress state tracked by the addon. */ - export interface IProgress { - state: ProgressState; + export interface IProgressState { + state: 0 | 1 | 2 | 3 | 4; value: number; } } diff --git a/demo/client.ts b/demo/client.ts index 6f44865890..6c7b53ffed 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,7 +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, IProgress } from '@xterm/addon-progress'; +import { ProgressAddon, IProgressState } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; import { WebLinksAddon } from '@xterm/addon-web-links'; @@ -1454,7 +1454,7 @@ 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, value}: IProgress) { + function progressHandler({state, value}: IProgressState) { // 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 % From 9caff8d7785b1ee6d2142132c2aa95d777bfc7b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Jan 2025 04:58:02 -0800 Subject: [PATCH 15/15] Add note in readme about full API --- addons/addon-progress/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/addon-progress/README.md b/addons/addon-progress/README.md index bb61c92fce..76ddb76c22 100644 --- a/addons/addon-progress/README.md +++ b/addons/addon-progress/README.md @@ -29,6 +29,8 @@ progressAddon.onChange({state, value}: IProgressState) => { }); ``` +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-progress/typings/addon-progress.d.ts) for more advanced usage. + ### Sequence The sequence to set progress information has the following format: