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 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 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 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 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 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 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 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 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 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 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 6065c5c5..5bee83d6 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 f302055c..bd80eeee 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 5bee83d6..5f661a57 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 8a90d6e3..ffc7b01a 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 bd80eeee..ba3d0da2 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 a612e29a..1bf29bfd 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 08027097..0df2d574 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 656b7f95..792c0445 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 5f661a57..c1468a51 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 4f05e4aa..43134ae8 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 0df2d574..6f448658 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 c1468a51..465cff47 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 ba3d0da2..874e4a13 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 874e4a13..a2cb2241 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 43134ae8..a84a307c 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 a2cb2241..5d96f422 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 a84a307c..bb61c92f 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 465cff47..24e57860 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 5d96f422..923a2aa3 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 6f448658..6c7b53ff 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 bb61c92f..76ddb76c 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: