Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/components/term.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {WebglAddon} from 'xterm-addon-webgl';

import type {TermProps} from '../../typings/hyper';
import terms from '../terms';
import {handleOsc52Clipboard} from '../utils/osc52';
import processClipboard from '../utils/paste';
import {decorate} from '../utils/plugins';

Expand Down Expand Up @@ -170,6 +171,13 @@ export default class Term extends React.PureComponent<

this.termOptions = getTermOptions(props);
this.term = props.term || new Terminal(this.termOptions);
this.disposableListeners.push(
this.term.parser.registerOscHandler(52, (data) =>
handleOsc52Clipboard(data, (text) => {
clipboard.writeText(text);
})
)
);
this.defaultBellSound = new Audio(
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
Expand Down
44 changes: 44 additions & 0 deletions lib/utils/osc52.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;

/**
* Decode an OSC 52 system clipboard write.
*
* Hyper intentionally handles writes only. Clipboard reads would expose the
* host clipboard to processes running in the terminal.
*/
export function decodeOsc52Clipboard(data: string): string | undefined {
const separator = data.indexOf(';');
if (separator < 0 || data.slice(0, separator) !== 'c') {
return undefined;
}

const encoded = data.slice(separator + 1);
if (encoded === '?') {
return undefined;
}

if (encoded === '!') {
return '';
}

if (!BASE64_PATTERN.test(encoded)) {
return undefined;
}

const decoded = Buffer.from(encoded, 'base64');
if (decoded.toString('base64') !== encoded) {
return undefined;
}

return decoded.toString('utf8');
}

export function handleOsc52Clipboard(data: string, writeClipboard: (text: string) => void): boolean {
const text = decodeOsc52Clipboard(data);
if (text === undefined) {
return false;
}

writeClipboard(text);
return true;
}
36 changes: 36 additions & 0 deletions test/unit/osc52.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'ava';

import {decodeOsc52Clipboard, handleOsc52Clipboard} from '../../lib/utils/osc52';

test('decodes OSC 52 system clipboard writes', (t) => {
const encoded = Buffer.from('Olá, Hyper!').toString('base64');

t.is(decodeOsc52Clipboard(`c;${encoded}`), 'Olá, Hyper!');
});

test('ignores OSC 52 clipboard reads', (t) => {
t.is(decodeOsc52Clipboard('c;?'), undefined);
});

test('ignores non-system clipboard selections', (t) => {
const encoded = Buffer.from('primary').toString('base64');

t.is(decodeOsc52Clipboard(`p;${encoded}`), undefined);
});

test('rejects malformed OSC 52 payloads', (t) => {
t.is(decodeOsc52Clipboard('c;not-base64'), undefined);
t.is(decodeOsc52Clipboard('c;'), '');
});

test('writes decoded OSC 52 data through the host clipboard', (t) => {
let copied = '';
const encoded = Buffer.from('from a pane').toString('base64');

t.true(
handleOsc52Clipboard(`c;${encoded}`, (text) => {
copied = text;
})
);
t.is(copied, 'from a pane');
});