Skip to content

Commit 2edc65b

Browse files
authored
Merge pull request #4220 from aymanbagabas/osc52
Add support to ANSI OSC52
2 parents 826c057 + 6ed1f42 commit 2edc65b

22 files changed

+578
-23
lines changed

.eslintrc.json

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"addons/addon-attach/test/tsconfig.json",
1919
"addons/addon-canvas/src/tsconfig.json",
2020
"addons/addon-canvas/test/tsconfig.json",
21+
"addons/addon-clipboard/src/tsconfig.json",
22+
"addons/addon-clipboard/test/tsconfig.json",
2123
"addons/addon-fit/src/tsconfig.json",
2224
"addons/addon-fit/test/tsconfig.json",
2325
"addons/addon-image/src/tsconfig.json",

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
./addons/addon-attach/out-test/* \
3131
./addons/addon-canvas/out/* \
3232
./addons/addon-canvas/out-test/* \
33+
./addons/addon-clipboard/out/* \
34+
./addons/addon-clipboard/out-test/* \
3335
./addons/addon-fit/out/* \
3436
./addons/addon-fit/out-test/* \
3537
./addons/addon-image/out/* \

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The xterm.js team maintains the following addons, but anyone can build them:
7878

7979
- [`@xterm/addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-attach): Attaches to a server running a process via a websocket
8080
- [`@xterm/addon-canvas`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-canvas): Renders xterm.js using a `canvas` element's 2d context
81+
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
8182
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
8283
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
8384
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality

addons/addon-clipboard/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib
2+
node_modules

addons/addon-clipboard/.npmignore

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
2+
*
3+
!*/
4+
5+
# Whitelist - lib/
6+
!lib/**/*.d.ts
7+
8+
!lib/**/*.js
9+
!lib/**/*.js.map
10+
11+
!lib/**/*.css
12+
13+
# Whitelist - src/
14+
!src/**/*.ts
15+
!src/**/*.d.ts
16+
17+
!src/**/*.js
18+
!src/**/*.js.map
19+
20+
!src/**/*.css
21+
22+
# Blacklist - src/ test files
23+
src/**/*.test.ts
24+
src/**/*.test.d.ts
25+
src/**/*.test.js
26+
src/**/*.test.js.map
27+
28+
# Whitelist - typings/
29+
!typings/*.d.ts

addons/addon-clipboard/LICENSE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2023, The xterm.js authors (https://github.com/xtermjs/xterm.js)
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

addons/addon-clipboard/README.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## @xterm/addon-clipboard
2+
3+
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables
4+
accessing the system clipboard. This addon requires xterm.js v4+.
5+
6+
### Install
7+
8+
```bash
9+
npm install --save @xterm/addon-clipboard
10+
```
11+
12+
### Usage
13+
14+
```ts
15+
import { Terminal } from 'xterm';
16+
import { ClipboardAddon } from '@xterm/addon-clipboard';
17+
18+
const terminal = new Terminal();
19+
const clipboardAddon = new ClipboardAddon();
20+
terminal.loadAddon(clipboardAddon);
21+
```
22+
23+
To use a custom clipboard provider
24+
25+
```ts
26+
import { Terminal } from '@xterm/xterm';
27+
import { ClipboardAddon, IClipboardProvider, ClipboardSelectionType } from '@xterm/addon-clipboard';
28+
29+
function b64Encode(data: string): string {
30+
// Base64 encode impl
31+
}
32+
33+
function b64Decode(data: string): string {
34+
// Base64 decode impl
35+
}
36+
37+
class MyCustomClipboardProvider implements IClipboardProvider {
38+
private _data: string
39+
public readText(selection: ClipboardSelectionType): Promise<string> {
40+
return Promise.resolve(b64Encode(this._data));
41+
}
42+
public writeText(selection: ClipboardSelectionType, data: string): Promise<void> {
43+
this._data = b64Decode(data);
44+
return Promise.resolve();
45+
}
46+
}
47+
48+
const terminal = new Terminal();
49+
const clipboardAddon = new ClipboardAddon(new MyCustomClipboardProvider());
50+
terminal.loadAddon(clipboardAddon);
51+
```
52+
53+
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-clipboard/typings/addon-clipboard.d.ts) for more advanced usage.

addons/addon-clipboard/package.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@xterm/addon-clipboard",
3+
"version": "0.1.0",
4+
"author": {
5+
"name": "The xterm.js authors",
6+
"url": "https://xtermjs.org/"
7+
},
8+
"main": "lib/addon-clipboard.js",
9+
"types": "typings/addon-clipboard.d.ts",
10+
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard",
11+
"license": "MIT",
12+
"keywords": [
13+
"terminal",
14+
"xterm",
15+
"xterm.js"
16+
],
17+
"scripts": {
18+
"build": "../../node_modules/.bin/tsc -p .",
19+
"prepackage": "npm run build",
20+
"package": "../../node_modules/.bin/webpack",
21+
"prepublishOnly": "npm run package"
22+
},
23+
"peerDependencies": {
24+
"@xterm/xterm": "^5.4.0"
25+
},
26+
"dependencies": {
27+
"js-base64": "^3.7.5"
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import type { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
7+
import { type IClipboardProvider, ClipboardSelectionType, type IBase64 } from '@xterm/addon-clipboard';
8+
import { Base64 as JSBase64 } from 'js-base64';
9+
10+
export class ClipboardAddon implements ITerminalAddon {
11+
private _terminal?: Terminal;
12+
private _disposable?: IDisposable;
13+
14+
constructor(
15+
private _base64: IBase64 = new Base64(),
16+
private _provider: IClipboardProvider = new BrowserClipboardProvider()
17+
) {}
18+
19+
public activate(terminal: Terminal): void {
20+
this._terminal = terminal;
21+
this._disposable = terminal.parser.registerOscHandler(52, data => this._setOrReportClipboard(data));
22+
}
23+
24+
public dispose(): void {
25+
return this._disposable?.dispose();
26+
}
27+
28+
private _readText(sel: ClipboardSelectionType, data: string): void {
29+
const b64 = this._base64.encodeText(data);
30+
this._terminal?.input(`\x1b]52;${sel};${b64}\x07`, false);
31+
}
32+
33+
private _setOrReportClipboard(data: string): boolean | Promise<boolean> {
34+
const args = data.split(';');
35+
if (args.length < 2) {
36+
return true;
37+
}
38+
39+
const pc = args[0] as ClipboardSelectionType;
40+
const pd = args[1];
41+
if (pd === '?') {
42+
const text = this._provider.readText(pc);
43+
44+
// Report clipboard
45+
if (text instanceof Promise) {
46+
return text.then((data) => {
47+
this._readText(pc, data);
48+
return true;
49+
});
50+
}
51+
52+
this._readText(pc, text);
53+
return true;
54+
}
55+
56+
// Clear clipboard if text is not a base64 encoded string.
57+
let text = '';
58+
try {
59+
text = this._base64.decodeText(pd);
60+
} catch {}
61+
62+
63+
const result = this._provider.writeText(pc, text);
64+
if (result instanceof Promise) {
65+
return result.then(() => true);
66+
}
67+
68+
return true;
69+
}
70+
}
71+
72+
export class BrowserClipboardProvider implements IClipboardProvider {
73+
public async readText(selection: ClipboardSelectionType): Promise<string> {
74+
if (selection !== 'c') {
75+
return Promise.resolve('');
76+
}
77+
return navigator.clipboard.readText();
78+
}
79+
80+
public async writeText(selection: ClipboardSelectionType, text: string): Promise<void> {
81+
if (selection !== 'c') {
82+
return Promise.resolve();
83+
}
84+
return navigator.clipboard.writeText(text);
85+
}
86+
}
87+
88+
export class Base64 implements IBase64 {
89+
public encodeText(data: string): string {
90+
return JSBase64.encode(data);
91+
}
92+
public decodeText(data: string): string {
93+
const text = JSBase64.decode(data);
94+
if (!JSBase64.isValid(data) || JSBase64.encode(text) !== data) {
95+
return '';
96+
}
97+
return text;
98+
}
99+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es2021",
5+
"lib": [
6+
"dom",
7+
"es2015"
8+
],
9+
"rootDir": ".",
10+
"outDir": "../out",
11+
"sourceMap": true,
12+
"removeComments": true,
13+
"strict": true,
14+
"types": [
15+
"../../../node_modules/@types/mocha"
16+
],
17+
"paths": {
18+
"browser/*": [
19+
"../../../src/browser/*"
20+
],
21+
"@xterm/addon-clipboard": [
22+
"../typings/addon-clipboard.d.ts"
23+
]
24+
}
25+
},
26+
"include": [
27+
"./**/*",
28+
"../../../typings/xterm.d.ts"
29+
],
30+
"references": [
31+
{
32+
"path": "../../../src/browser"
33+
}
34+
]
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { assert } from 'chai';
7+
import { openTerminal, launchBrowser, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
8+
import { Browser, BrowserContext, Page } from '@playwright/test';
9+
import { beforeEach } from 'mocha';
10+
11+
const APP = 'http://127.0.0.1:3001/test';
12+
13+
let browser: Browser;
14+
let context: BrowserContext;
15+
let page: Page;
16+
const width = 800;
17+
const height = 600;
18+
19+
describe('ClipboardAddon', () => {
20+
before(async function (): Promise<any> {
21+
browser = await launchBrowser({
22+
// Enable clipboard access in firefox, mainly for readText
23+
firefoxUserPrefs: {
24+
// eslint-disable-next-line @typescript-eslint/naming-convention
25+
'dom.events.testing.asyncClipboard': true,
26+
// eslint-disable-next-line @typescript-eslint/naming-convention
27+
'dom.events.asyncClipboard.readText': true
28+
}
29+
});
30+
context = await browser.newContext();
31+
if (getBrowserType().name() !== 'webkit') {
32+
// Enable clipboard access in chromium without user gesture
33+
context.grantPermissions(['clipboard-read', 'clipboard-write']);
34+
}
35+
page = await context.newPage();
36+
await page.setViewportSize({ width, height });
37+
await page.goto(APP);
38+
await openTerminal(page);
39+
await page.evaluate(`
40+
window.clipboardAddon = new ClipboardAddon();
41+
window.term.loadAddon(window.clipboardAddon);
42+
`);
43+
});
44+
45+
after(() => {
46+
browser.close();
47+
});
48+
49+
beforeEach(async () => {
50+
await page.evaluate(`window.term.reset()`);
51+
});
52+
53+
const testDataEncoded = 'aGVsbG8gd29ybGQ=';
54+
const testDataDecoded = 'hello world';
55+
56+
describe('write data', async function (): Promise<any> {
57+
it('simple string', async () => {
58+
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
59+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
60+
});
61+
it('invalid base64 string', async () => {
62+
await writeSync(page, `\x1b]52;c;${testDataEncoded}invalid\x07`);
63+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
64+
});
65+
it('empty string', async () => {
66+
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
67+
await writeSync(page, `\x1b]52;c;\x07`);
68+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
69+
});
70+
});
71+
72+
describe('read data', async function (): Promise<any> {
73+
it('simple string', async () => {
74+
await page.evaluate(`
75+
window.data = [];
76+
window.term.onData(e => data.push(e));
77+
`);
78+
await page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
79+
await writeSync(page, `\x1b]52;c;?\x07`);
80+
assert.deepEqual(await page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
81+
});
82+
it('clear clipboard', async () => {
83+
await writeSync(page, `\x1b]52;c;!\x07`);
84+
await writeSync(page, `\x1b]52;c;?\x07`);
85+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)