Skip to content

Instantly share code, notes, and snippets.

@jsejcksn
Last active June 28, 2020 01:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsejcksn/b4b1e86e504f16239aec90df4e9b29a9 to your computer and use it in GitHub Desktop.
Save jsejcksn/b4b1e86e504f16239aec90df4e9b29a9 to your computer and use it in GitHub Desktop.
Deno text clipboard
import {assert, assertEquals} from './deps.ts';
import {readText, writeText} from './mod.ts';
type Test = [string, () => void | Promise<void>];
const tests: Test[] = [
[
'reads/writes without throwing', async () => {
const input = 'hello world';
await writeText(input);
await readText();
},
],
[
'single line data', async () => {
const input = 'single line data';
await writeText(input);
const output = await readText();
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, ''));
},
],
[
'multi line data', async () => {
const input = 'multi\nline\ndata';
await writeText(input);
const output = await readText();
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, ''));
},
],
[
'multi line data dangling newlines', async () => {
const input = '\n\n\nmulti\n\n\n\n\n\nline\ndata\n\n\n\n\n';
await writeText(input);
const output = await readText({trimFinalNewlines: false});
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, ''));
},
],
[
'data with special characters', async () => {
const input = '`~!@#$%^&*()_+-=[]{};\':",./<>?\t\n';
await writeText(input);
const output = await readText({trimFinalNewlines: false});
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, ''));
},
],
[
'data with unicode characters', async () => {
const input = 'Rafał';
await writeText(input);
const output = await readText();
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, ''));
},
],
[
'option: trim', async () => {
const input = 'hello world\n\n';
const inputTrimmed = 'hello world';
await writeText(input);
const output = await readText({trimFinalNewlines: false});
const outputTrimmed = await readText({trimFinalNewlines: true});
const outputDefault = await readText();
assert(output !== inputTrimmed && output.trim() === inputTrimmed);
assertEquals(inputTrimmed, outputTrimmed);
assertEquals(inputTrimmed, outputDefault);
},
],
[
'option: unixNewlines', async () => {
const inputCRLF = 'hello\r\nworld';
const inputLF = 'hello\nworld';
await writeText(inputCRLF);
const output = await readText({unixNewlines: false});
const outputUnix = await readText({unixNewlines: true});
const outputDefault = await readText();
assertEquals(inputCRLF, output);
assertEquals(inputLF, outputUnix);
assertEquals(inputLF, outputDefault);
},
],
];
for (const [name, fn] of tests) Deno.test({fn, name});
const decoder = new TextDecoder();
const encoder = new TextEncoder();
type LinuxBinary = 'wsl' | 'xclip' | 'xsel';
type Config = {
linuxBinary: LinuxBinary;
};
const config: Config = {linuxBinary: 'xsel'};
const errMsg = {
genericRead: 'There was a problem reading from the clipboard',
genericWrite: 'There was a problem writing to the clipboard',
noClipboardUtility: 'No supported clipboard utility. "xsel" or "xclip" must be installed.',
osUnsupported: 'Unsupported operating system',
};
const normalizeNewlines = (str: string) => str.replace(/\r\n/gu, '\n');
const trimNewlines = (str: string) => str.replace(/(?:\r\n|\n)+$/u, '');
/**
* Options to change the parsing behavior when reading the clipboard text
*
* `trimFinalNewlines?` — Trim trailing newlines. Default is `true`.
*
* `unixNewlines?` — Convert all CRLF newlines to LF newlines. Default is `true`.
*/
export type ReadTextOptions = {
trimFinalNewlines?: boolean;
unixNewlines?: boolean;
};
type TextClipboard = {
readText: (readTextOptions?: ReadTextOptions) => Promise<string>;
writeText: (data: string) => Promise<void>;
};
const shared = {
async readText (
cmd: string[],
{trimFinalNewlines = true, unixNewlines = true}: ReadTextOptions = {},
): Promise<string> {
const p = Deno.run({cmd, stdout: 'piped'});
const {success} = await p.status();
const stdout = decoder.decode(await p.output());
p.close();
if (!success) throw new Error(errMsg.genericRead);
let result = stdout;
if (unixNewlines) result = normalizeNewlines(result);
if (trimFinalNewlines) return trimNewlines(result);
return result;
},
async writeText (cmd: string[], data: string): Promise<void> {
const p = Deno.run({cmd, stdin: 'piped'});
if (!p.stdin) throw new Error(errMsg.genericWrite);
await p.stdin.write(encoder.encode(data));
p.stdin.close();
const {success} = await p.status();
if (!success) throw new Error(errMsg.genericWrite);
p.close();
},
};
const darwin: TextClipboard = {
readText (readTextOptions?: ReadTextOptions): Promise<string> {
const cmd: string[] = ['pbpaste'];
return shared.readText(cmd, readTextOptions);
},
writeText (data: string): Promise<void> {
const cmd: string[] = ['pbcopy'];
return shared.writeText(cmd, data);
},
};
const linux: TextClipboard = {
readText (readTextOptions?: ReadTextOptions): Promise<string> {
const cmds: {[key in LinuxBinary]: string[]} = {
wsl: ['powershell.exe', '-NoProfile', '-Command', 'Get-Clipboard'],
xclip: ['xclip', '-selection', 'clipboard', '-o'],
xsel: ['xsel', '-b', '-o'],
};
const cmd = cmds[config.linuxBinary];
return shared.readText(cmd, readTextOptions);
},
writeText (data: string): Promise<void> {
const cmds: {[key in LinuxBinary]: string[]} = {
wsl: ['clip.exe'],
xclip: ['xclip', '-selection', 'clipboard'],
xsel: ['xsel', '-b', '-i'],
};
const cmd = cmds[config.linuxBinary];
return shared.writeText(cmd, data);
},
};
const windows: TextClipboard = {
readText (readTextOptions?: ReadTextOptions): Promise<string> {
const cmd: string[] = ['powershell', '-NoProfile', '-Command', 'Get-Clipboard'];
return shared.readText(cmd, readTextOptions);
},
writeText (data: string): Promise<void> {
const cmd: string[] = ['powershell', '-NoProfile', '-Command', '$input|Set-Clipboard'];
return shared.writeText(cmd, data);
},
};
const getProcessOutput = async (cmd: string[]): Promise<string> => {
try {
const p = Deno.run({cmd, stdout: 'piped'});
const stdout = decoder.decode(await p.output());
p.close();
return stdout.trim();
}
catch (err) {
return '';
}
};
const resolveLinuxBinary = async (): Promise<LinuxBinary> => {
type BinaryEntry = [LinuxBinary, () => boolean | Promise<boolean>];
const binaryEntries: BinaryEntry[] = [
[
'wsl', async () => (
(await getProcessOutput(['uname', '-r', '-v'])).toLowerCase().includes('microsoft')
&& Boolean(await getProcessOutput(['which', 'clip.exe']))
&& Boolean(await getProcessOutput(['which', 'powershell.exe']))
),
],
['xsel', async () => Boolean(await getProcessOutput(['which', 'xsel']))],
['xclip', async () => Boolean(await getProcessOutput(['which', 'xclip']))],
];
for (const [binary, matchFn] of binaryEntries) {
const binaryMatches = await matchFn();
if (binaryMatches) return binary;
}
throw new Error(errMsg.noClipboardUtility);
};
type Clipboards = {[key in typeof Deno.build.os]: TextClipboard};
const clipboards: Clipboards = {
darwin,
linux,
windows,
};
const {build: {os}} = Deno;
if (os === 'linux') config.linuxBinary = await resolveLinuxBinary();
else if (!clipboards[os]) throw new Error(errMsg.osUnsupported);
/**
* Reads the clipboard and returns a string containing the text contents. Requires the `--allow-run` flag.
*/
export const {readText} = clipboards[os];
/**
* Writes a string to the clipboard. Requires the `--allow-run` flag.
*/
export const {writeText} = clipboards[os];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment