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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559ae17a80..18d34831f0 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) 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..76ddb76c22 --- /dev/null +++ b/addons/addon-progress/README.md @@ -0,0 +1,69 @@ +## @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, IProgressState } from '@xterm/addon-progress'; + +const terminal = new Terminal(); +const progressAddon = new ProgressAddon(); +terminal.loadAddon(progressAddon); +progressAddon.onChange({state, value}: IProgressState) => { + // state: 0-4 integer (see below for meaning) + // value: 0-100 integer (percent value) + + // do your visualisation based on state/value here + ... +}); +``` + +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: + +```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. 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..24e5786068 --- /dev/null +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -0,0 +1,101 @@ +/** + * 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, IProgressState } from '@xterm/addon-progress'; +import type { Emitter, Event } from 'vs/base/common/event'; + + +const enum ProgressType { + 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: ProgressType = ProgressType.REMOVE; + private _pr = 0; + private _onChange: Emitter | undefined; + public onChange: Event | undefined; + + public dispose(): void { + this._seqHandler?.dispose(); + this._onChange?.dispose(); + } + + 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 ProgressType.REMOVE: + this.progress = { state: st, value: 0 }; + break; + case ProgressType.SET: + if (pr < 0) return true; // faulty sequence, just exit + this.progress = { state: st, value: pr }; + break; + 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 ProgressType.INDETERMINATE: + this.progress = { state: st, value: this._pr }; + 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(): IProgressState { + return { state: this._st, value: this._pr }; + } + + public set progress(progress: IProgressState) { + 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 new file mode 100644 index 0000000000..ffc7b01a58 --- /dev/null +++ b/addons/addon-progress/src/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true, + "types": [ + "../../../node_modules/@types/mocha", + "../../../src/vs/typings/thenable" + ], + "paths": { + "browser/*": [ + "../../../src/browser/*" + ], + "vs/*": [ + "../../../src/vs/*" + ], + "@xterm/addon-progress": [ + "../typings/addon-progress.d.ts" + ] + } + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/browser" + }, + { + "path": "../../../src/vs" + } + ] +} diff --git a/addons/addon-progress/test/ProgressAddon.test.ts b/addons/addon-progress/test/ProgressAddon.test.ts new file mode 100644 index 0000000000..792c0445de --- /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.onChange(progress => window.progressStack.push(progress)); + `); + }); + + 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..923a2aa39b --- /dev/null +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { Event } from 'vs/base/common/event'; + + +declare module '@xterm/addon-progress' { + /** + * 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; + + /** + * 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: IProgressState; + } + + /** + * Progress state tracked by the addon. + */ + export interface IProgressState { + state: 0 | 1 | 2 | 3 | 4; + value: number; + } +} diff --git a/addons/addon-progress/webpack.config.js b/addons/addon-progress/webpack.config.js new file mode 100644 index 0000000000..1bf29bfd2c --- /dev/null +++ b/addons/addon-progress/webpack.config.js @@ -0,0 +1,42 @@ +/** + * 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/ + } + ] + }, + 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'), + 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/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", diff --git a/bin/publish.js b/bin/publish.js index 59771cd8f0..395b52bd71 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'), 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 e926eaaa97..f5903aa4d7 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, 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'; @@ -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; @@ -246,6 +252,7 @@ if (document.location.pathname === '/test') { addVtButtons(); initImageAddonExposed(); testEvents(); + progressButtons(); } function createTerminal(): void { @@ -272,6 +279,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) @@ -282,6 +290,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); @@ -1458,3 +1467,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, 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 % + // 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]; + 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.onChange(progressHandler); + + // apply initial state once to make it visible on page load + const initialProgress = progressAddon.progress; + progressHandler(initialProgress); + + 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 96361a88dc..cb63b174a7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -120,6 +120,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" },