Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to ANSI OSC52 #4220

Merged
merged 28 commits into from Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
db5bfc7
Add support to ANSI OSC52
aymanbagabas Sep 20, 2023
b22762b
Fix tsconfigs, IDisposable, and add comments
aymanbagabas Sep 21, 2023
092aeae
Fix rename clipboard selection for clarity
aymanbagabas Sep 21, 2023
d245e90
Add playwright terminal test
aymanbagabas Sep 21, 2023
2a8ddcd
Merge branch 'master' into osc52
aymanbagabas Oct 4, 2023
96fcfbd
Merge branch 'master' into osc52
aymanbagabas Oct 20, 2023
a4e6713
Merge remote-tracking branch 'upstream/master' into pr/aymanbagabas/4220
Tyriar Nov 3, 2023
aed94e5
xterm-addon-clipboard to scoped module
Tyriar Nov 3, 2023
bdcba30
Upload clipboard addon artifacts
Tyriar Nov 3, 2023
805caba
Merge remote-tracking branch 'upstream/master' into osc52
aymanbagabas Apr 7, 2024
94648eb
Update addon-clipboard
aymanbagabas Apr 7, 2024
fad97bc
Clean up
aymanbagabas Apr 7, 2024
a709187
Update addon-clipboard readme
aymanbagabas Apr 8, 2024
ce9e92f
Fix OSC52 sequence response
aymanbagabas Apr 8, 2024
00514d4
Undo format
aymanbagabas Apr 8, 2024
6ac75d8
Fix demo tsconfig addon name
aymanbagabas Apr 8, 2024
acc0a2e
Support passing playwright browser options
aymanbagabas Apr 8, 2024
70dd76f
Merge branch 'master' into osc52
aymanbagabas Apr 8, 2024
8a9cb37
Tidy
aymanbagabas Apr 18, 2024
1f7db05
Merge branch 'master' into osc52
aymanbagabas Apr 18, 2024
5238461
Update ClipboardAddon.ts
aymanbagabas Apr 18, 2024
87169ee
fix: tests
aymanbagabas Apr 19, 2024
98b0e16
Merge branch 'master' into osc52
aymanbagabas Apr 19, 2024
9ebfb11
Fix tests
aymanbagabas Apr 19, 2024
0562f32
Apply suggestions
aymanbagabas Apr 20, 2024
24b04e9
Merge branch 'master' into osc52
aymanbagabas Apr 20, 2024
3cedfbe
Merge branch 'master' into osc52
aymanbagabas Apr 21, 2024
6ed1f42
Merge branch 'master' into osc52
Tyriar Apr 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.json
Expand Up @@ -18,6 +18,8 @@
"addons/addon-attach/test/tsconfig.json",
"addons/addon-canvas/src/tsconfig.json",
"addons/addon-canvas/test/tsconfig.json",
"addons/addon-clipboard/src/tsconfig.json",
"addons/addon-clipboard/test/tsconfig.json",
"addons/addon-fit/src/tsconfig.json",
"addons/addon-fit/test/tsconfig.json",
"addons/addon-image/src/tsconfig.json",
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -30,6 +30,8 @@ jobs:
./addons/addon-attach/out-test/* \
./addons/addon-canvas/out/* \
./addons/addon-canvas/out-test/* \
./addons/addon-clipboard/out/* \
./addons/addon-clipboard/out-test/* \
./addons/addon-fit/out/* \
./addons/addon-fit/out-test/* \
./addons/addon-image/out/* \
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -78,6 +78,7 @@ The xterm.js team maintains the following addons, but anyone can build them:

- [`@xterm/addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-attach): Attaches to a server running a process via a websocket
- [`@xterm/addon-canvas`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-canvas): Renders xterm.js using a `canvas` element's 2d context
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
Expand Down
2 changes: 2 additions & 0 deletions addons/addon-clipboard/.gitignore
@@ -0,0 +1,2 @@
lib
node_modules
29 changes: 29 additions & 0 deletions addons/addon-clipboard/.npmignore
@@ -0,0 +1,29 @@
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/

# Whitelist - lib/
!lib/**/*.d.ts

!lib/**/*.js
!lib/**/*.js.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
19 changes: 19 additions & 0 deletions addons/addon-clipboard/LICENSE
@@ -0,0 +1,19 @@
Copyright (c) 2023, 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.
53 changes: 53 additions & 0 deletions addons/addon-clipboard/README.md
@@ -0,0 +1,53 @@
## @xterm/addon-clipboard

An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables
accessing the system clipboard. This addon requires xterm.js v4+.

### Install

```bash
npm install --save @xterm/addon-clipboard
```

### Usage

```ts
import { Terminal } from 'xterm';
import { ClipboardAddon } from '@xterm/addon-clipboard';

const terminal = new Terminal();
const clipboardAddon = new ClipboardAddon();
terminal.loadAddon(clipboardAddon);
```

To use a custom clipboard provider

```ts
import { Terminal } from '@xterm/xterm';
import { ClipboardAddon, IClipboardProvider, ClipboardSelectionType } from '@xterm/addon-clipboard';

function b64Encode(data: string): string {
// Base64 encode impl
}

function b64Decode(data: string): string {
// Base64 decode impl
}

class MyCustomClipboardProvider implements IClipboardProvider {
private _data: string
public readText(selection: ClipboardSelectionType): Promise<string> {
return Promise.resolve(b64Encode(this._data));
}
public writeText(selection: ClipboardSelectionType, data: string): Promise<void> {
this._data = b64Decode(data);
return Promise.resolve();
}
}

const terminal = new Terminal();
const clipboardAddon = new ClipboardAddon(new MyCustomClipboardProvider());
terminal.loadAddon(clipboardAddon);
```

See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-clipboard/typings/addon-clipboard.d.ts) for more advanced usage.
29 changes: 29 additions & 0 deletions addons/addon-clipboard/package.json
@@ -0,0 +1,29 @@
{
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/addon-clipboard.js",
"types": "typings/addon-clipboard.d.ts",
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard",
"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"
},
"peerDependencies": {
"@xterm/xterm": "^5.4.0"
},
"dependencies": {
"js-base64": "^3.7.5"
}
}
99 changes: 99 additions & 0 deletions addons/addon-clipboard/src/ClipboardAddon.ts
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/

import type { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
import { type IClipboardProvider, ClipboardSelectionType, type IBase64 } from '@xterm/addon-clipboard';
import { Base64 as JSBase64 } from 'js-base64';

export class ClipboardAddon implements ITerminalAddon {
private _terminal?: Terminal;
private _disposable?: IDisposable;

constructor(
private _base64: IBase64 = new Base64(),
private _provider: IClipboardProvider = new BrowserClipboardProvider()
) {}

public activate(terminal: Terminal): void {
this._terminal = terminal;
this._disposable = terminal.parser.registerOscHandler(52, data => this._setOrReportClipboard(data));
}

public dispose(): void {
return this._disposable?.dispose();
}

private _readText(sel: ClipboardSelectionType, data: string): void {
const b64 = this._base64.encodeText(data);
this._terminal?.input(`\x1b]52;${sel};${b64}\x07`, false);
}

private _setOrReportClipboard(data: string): boolean | Promise<boolean> {
const args = data.split(';');
if (args.length < 2) {
return true;
}

const pc = args[0] as ClipboardSelectionType;
const pd = args[1];
if (pd === '?') {
const text = this._provider.readText(pc);

// Report clipboard
if (text instanceof Promise) {
return text.then((data) => {
this._readText(pc, data);
return true;
});
}

this._readText(pc, text);
return true;
}

// Clear clipboard if text is not a base64 encoded string.
let text = '';
try {
text = this._base64.decodeText(pd);
} catch {}


const result = this._provider.writeText(pc, text);
if (result instanceof Promise) {
return result.then(() => true);
}

return true;
}
}

export class BrowserClipboardProvider implements IClipboardProvider {
public async readText(selection: ClipboardSelectionType): Promise<string> {
if (selection !== 'c') {
return Promise.resolve('');
}
return navigator.clipboard.readText();
}

public async writeText(selection: ClipboardSelectionType, text: string): Promise<void> {
if (selection !== 'c') {
return Promise.resolve();
}
return navigator.clipboard.writeText(text);
}
}

export class Base64 implements IBase64 {
public encodeText(data: string): string {
return JSBase64.encode(data);
}
public decodeText(data: string): string {
const text = JSBase64.decode(data);
if (!JSBase64.isValid(data) || JSBase64.encode(text) !== data) {
return '';
}
return text;
}
}
35 changes: 35 additions & 0 deletions addons/addon-clipboard/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-clipboard": [
"../typings/addon-clipboard.d.ts"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/browser"
}
]
}
88 changes: 88 additions & 0 deletions addons/addon-clipboard/test/ClipboardAddon.api.ts
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { assert } from 'chai';
import { openTerminal, launchBrowser, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, BrowserContext, Page } from '@playwright/test';
import { beforeEach } from 'mocha';

const APP = 'http://127.0.0.1:3001/test';

let browser: Browser;
let context: BrowserContext;
let page: Page;
const width = 800;
const height = 600;

describe('ClipboardAddon', () => {
before(async function (): Promise<any> {
browser = await launchBrowser({
// Enable clipboard access in firefox, mainly for readText
firefoxUserPrefs: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'dom.events.testing.asyncClipboard': true,
// eslint-disable-next-line @typescript-eslint/naming-convention
'dom.events.asyncClipboard.readText': true
}
});
context = await browser.newContext();
if (getBrowserType().name() !== 'webkit') {
// Enable clipboard access in chromium without user gesture
context.grantPermissions(['clipboard-read', 'clipboard-write']);
}
page = await context.newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page);
await page.evaluate(`
window.clipboardAddon = new ClipboardAddon();
window.term.loadAddon(window.clipboardAddon);
`);
});

after(() => {
browser.close();
});

beforeEach(async () => {
await page.evaluate(`window.term.reset()`);
});

const testDataEncoded = 'aGVsbG8gd29ybGQ=';
const testDataDecoded = 'hello world';

describe('write data', async function (): Promise<any> {
it('simple string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
it('invalid base64 string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}invalid\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
it('empty string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
await writeSync(page, `\x1b]52;c;\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});

describe('read data', async function (): Promise<any> {
it('simple string', async () => {
await page.evaluate(`
window.data = [];
window.term.onData(e => data.push(e));
`);
await page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await writeSync(page, `\x1b]52;c;?\x07`);
assert.deepEqual(await page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
it('clear clipboard', async () => {
await writeSync(page, `\x1b]52;c;!\x07`);
await writeSync(page, `\x1b]52;c;?\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
});