Skip to content

Commit 420f758

Browse files
committed
Add clipboard addon
This addon enables xtermjs to access the system clipboard to read/write data. It uses the Clipboard API to do so and leverages xtermjs OSC52 implementation to read/write data from and to the system clipboard. Users must enable the `allowClipboardAccess` options for this addon to work. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
1 parent 3aac12c commit 420f758

File tree

19 files changed

+420
-17
lines changed

19 files changed

+420
-17
lines changed

.eslintrc.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"addons/xterm-addon-attach/src/tsconfig.json",
1717
"addons/xterm-addon-attach/test/tsconfig.json",
1818
"addons/xterm-addon-canvas/src/tsconfig.json",
19+
"addons/xterm-addon-clipboard/src/tsconfig.json",
20+
"addons/xterm-addon-clipboard/test/tsconfig.json",
1921
"addons/xterm-addon-fit/src/tsconfig.json",
2022
"addons/xterm-addon-fit/test/tsconfig.json",
2123
"addons/xterm-addon-ligatures/src/tsconfig.json",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ terminal.loadAddon(new WebLinksAddon());
7979
The xterm.js team maintains the following addons, but anyone can build them:
8080

8181
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
82+
- [`xterm-addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-clipboard): Access the browser's clipboard
8283
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
8384
- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality
8485
- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib
2+
node_modules
Lines changed: 29 additions & 0 deletions
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
Lines changed: 19 additions & 0 deletions
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.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## xterm-addon-clipboard
2+
3+
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables accessing the system clipboard. This addon requires xterm.js v4+.
4+
5+
### Install
6+
7+
```bash
8+
npm install --save xterm-addon-clipboard
9+
```
10+
11+
### Usage
12+
13+
```ts
14+
import { Terminal } from 'xterm';
15+
import { ClipboardAddon } from 'xterm-addon-clipboard';
16+
17+
const terminal = new Terminal();
18+
const clipboardAddon = new ClipboardAddon();
19+
terminal.loadAddon(clipboardAddon);
20+
```
21+
22+
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-clipboard/typings/xterm-addon-clipboard.d.ts) for more advanced usage.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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/xterm-addon-clipboard.js",
9+
"types": "typings/xterm-addon-clipboard.d.ts",
10+
"repository": "https://github.com/xtermjs/xterm.js",
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": "^5.0.0"
25+
}
26+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { Terminal, ITerminalAddon, IClipboardEvent, ClipboardEventType, ClipboardSelectionType } from 'xterm';
7+
import { Disposable } from 'common/Lifecycle';
8+
import { ITerminal } from 'browser/Types';
9+
import * as Base64 from 'common/Base64';
10+
11+
12+
export class ClipboardAddon extends Disposable implements ITerminalAddon {
13+
private _terminal: Terminal | undefined;
14+
15+
constructor() {
16+
super();
17+
}
18+
19+
// eslint-disable-next-line @typescript-eslint/naming-convention
20+
private _handleClipboard(event: IClipboardEvent): void {
21+
if (!this._terminal) {
22+
return;
23+
}
24+
if (event.selection !== ClipboardSelectionType.CLIPBOARD) {
25+
// TODO support primary selection on linux, browser doesn't support
26+
// primary selection.
27+
return;
28+
}
29+
const core = (this._terminal as any)._core as ITerminal;
30+
switch (event.type) {
31+
case ClipboardEventType.QUERY:
32+
this.readText()
33+
// report base64 encoded text back to terminal.
34+
.then(text => Base64.encode(text))
35+
.then(data => core.coreService.triggerDataEvent(data))
36+
.catch(err => console.error(`Failed to read from clipboard: ${err}`));
37+
break;
38+
case ClipboardEventType.SET:
39+
this.writeText(event.payload || '')
40+
.catch(err => console.error(`Failed to write to clipboard: ${err}`));
41+
break;
42+
}
43+
}
44+
45+
public activate(terminal: Terminal): void {
46+
this._terminal = terminal;
47+
this.register(terminal.onClipboard((event) => this._handleClipboard(event)));
48+
}
49+
50+
public dispose(): void {
51+
super.dispose();
52+
}
53+
54+
public writeText(data: string): Promise<void> {
55+
if (navigator.clipboard) {
56+
return navigator.clipboard.writeText(data);
57+
}
58+
return Promise.reject(new Error('Clipboard API not available'));
59+
}
60+
61+
public readText(): Promise<string> {
62+
if (navigator.clipboard) {
63+
return navigator.clipboard.readText();
64+
}
65+
return Promise.reject(new Error('Clipboard API not available'));
66+
}
67+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es2015",
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+
"common/*": [
19+
"../../../src/common/*"
20+
],
21+
"browser/*": [
22+
"../../../src/browser/*"
23+
]
24+
}
25+
},
26+
"include": [
27+
"./**/*",
28+
"../../../typings/xterm.d.ts"
29+
],
30+
"references": [
31+
{
32+
"path": "../../../src/common"
33+
},
34+
{
35+
"path": "../../../src/browser"
36+
}
37+
]
38+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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';
9+
10+
const APP = 'http://127.0.0.1:3001/test';
11+
12+
let browser: Browser;
13+
let context: BrowserContext;
14+
let page: Page;
15+
const width = 800;
16+
const height = 600;
17+
18+
describe('ClipboardAddon', () => {
19+
before(async function (): Promise<any> {
20+
browser = await launchBrowser({
21+
// Enable clipboard access in firefox, mainly for readText
22+
firefoxUserPrefs: {
23+
'dom.events.testing.asyncClipboard': true,
24+
'dom.events.asyncClipboard.readText': true
25+
}
26+
});
27+
context = await browser.newContext();
28+
if (getBrowserType().name() !== 'webkit') {
29+
// Enable clipboard access in chromium without user gesture
30+
context.grantPermissions(['clipboard-read', 'clipboard-write']);
31+
}
32+
page = await context.newPage();
33+
await page.setViewportSize({ width, height });
34+
await page.goto(APP);
35+
await openTerminal(page, { allowClipboardAccess: true });
36+
await page.evaluate(`
37+
window.clipboardAddon = new ClipboardAddon();
38+
window.term.loadAddon(window.clipboardAddon);
39+
`);
40+
});
41+
42+
after(() => {
43+
browser.close();
44+
});
45+
46+
beforeEach(async () => {
47+
await page.evaluate(`window.term.reset()`);
48+
});
49+
50+
const testDataEncoded = 'aGVsbG8gd29ybGQ=';
51+
const testDataDecoded = 'hello world';
52+
53+
describe('write data', async function (): Promise<any> {
54+
it('simple string', async () => {
55+
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
56+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
57+
});
58+
it('invalid base64 string', async () => {
59+
await writeSync(page, `\x1b]52;c;${testDataEncoded}invalid\x07`);
60+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
61+
});
62+
it('empty string', async () => {
63+
await writeSync(page, `\x1b]52;c;\x07`);
64+
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
65+
});
66+
});
67+
68+
describe('read data', async function (): Promise<any> {
69+
it('simple string', async () => {
70+
await page.evaluate(`
71+
window.data = [];
72+
window.term.onData(e => data.push(e));
73+
`);
74+
await page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
75+
await writeSync(page, `\x1b]52;c;?\x07`);
76+
assert.deepEqual(await page.evaluate(`window.data`), [testDataEncoded]);
77+
});
78+
});
79+
});

0 commit comments

Comments
 (0)