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

Update; Add read parsing options #5

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ jobs:
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get install -y xclip xsel xvfb --no-install-recommends
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh -s v0.25.0
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh -s v1.0.5

- name: Install Deno (Mac)
if: startsWith(matrix.os, 'macOS')
run: |
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh -s v0.25.0
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh -s v1.0.5

- name: Install Deno (Windows)
if: startsWith(matrix.os, 'windows')
Expand All @@ -50,4 +50,4 @@ jobs:
- name: Test (Mac and Windows)
if: matrix.kind == 'test' && !startsWith(matrix.os, 'ubuntu')
run: |
deno test --allow-run --allow-net test.ts
deno test --allow-run test.ts
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"deno.enable": true
}
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ Deno clipboard library

[![Build Status][actions-img]][actions-url]<br>(CI tests on Linux, Mac, Windows)

> On Linux, `xsel` or `xclip` must be installed and in your `PATH`.

Usage
-

```ts
import { clipboard } from 'https://deno.land/x/clipboard/mod.ts';
import * as clipboard from 'https://deno.land/x/clipboard/mod.ts';

await clipboard.writeText('some text');

Expand All @@ -33,18 +35,24 @@ then hopefully this will be obsolete. See the relevant issue:

- [denoland/deno#3450 Support of Clipboard API without `--deno-run`](https://github.com/denoland/deno/issues/3450)

Notes
Options
-
On Linux it requires `xsel` to be installed (probably installed by default).
The clipboard on Windows always adds a trailing newline if there was none,
which makes single line strings end with a newline. Newlines in the clipboard are sometimes
problematic (like automatically starting commands when pasted into the terminal), so this module trims trailing newlines by default. It also converts CRLF (Windows) newlines to LF (Unix) newlines by default when reading the clipboard.

Both of these options can be disabled independently:

```ts
import * as clipboard from 'https://deno.land/x/clipboard/mod.ts';

The clipboard on Windows always adds a trailing newline if there was none
which makes single line strings end with a newline and this module removes the
trailing newline on Windows, but it means that if it was there originally then it will still
be removed - to preserve single-line strings being single-line, but maybe this is not the right
way to do it. The other option would be to preserve the trailing newline but also to get one
if it wasn't there. Currently I chose to remove it because newlines in the clipboard sometimes
are problematic (like automatically starting commands when pasted into the terminal).
TODO: think about it.
const options: clipboard.ReadTextOptions = {
trimFinalNewlines: false, // don't trim trailing newlines
unixNewlines: false, // don't convert CRLF to LF
};

const clipboardText = await clipboard.readText(options);
```

Issues
-
Expand All @@ -60,6 +68,10 @@ Author
<br/>
[![Follow on Stack Exchange][stackexchange-img]][stackoverflow-url]

Contributors
-
- [**Jesse Jackson**](https://github.com/jsejcksn)

License
-
MIT License (Expat). See [LICENSE.md](LICENSE.md) for details.
Expand Down
6 changes: 3 additions & 3 deletions example1.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clipboard } from './mod.ts';
import * as clipboard from './mod.ts';

const x = 'abcaaa';
const text = 'abcaaa';

await clipboard.writeText(x);
await clipboard.writeText(text);

console.log(await clipboard.readText());
2 changes: 1 addition & 1 deletion example2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { clipboard } from './mod.ts';
import * as clipboard from './mod.ts';

console.log(await clipboard.readText());
2 changes: 1 addition & 1 deletion example3.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clipboard } from './mod.ts';
import * as clipboard from './mod.ts';

await clipboard.writeText('some text');

Expand Down
219 changes: 157 additions & 62 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,183 @@
// Copyright (c) 2019 Rafał Pocztarski. All rights reserved.
// Copyright (c) 2019 Rafał Pocztarski, Jesse Jackson. All rights reserved.
// MIT License (Expat). See: https://github.com/rsp/deno-clipboard

type Dispatch = {
[key in Deno.OperatingSystem]: Clipboard;
const decoder = new TextDecoder();
const encoder = new TextEncoder();

type LinuxBinary = 'wsl' | 'xclip' | 'xsel';

type Config = {
linuxBinary: LinuxBinary;
};

const encoder = new TextEncoder();
const decoder = new TextDecoder();
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',
noClipboard: 'No supported clipboard utility. "xsel" or "xclip" must be installed.',
noClipboardWSL: 'Windows tools not found in $PATH. See https://docs.microsoft.com/en-us/windows/wsl/interop#run-windows-tools-from-linux',
osUnsupported: 'Unsupported operating system',
};

export const encode = (x: string) => encoder.encode(x);
export const decode = (x: Uint8Array) => decoder.decode(x);
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;
};

const opt: Deno.RunOptions = {
args: [],
stdin: 'piped',
stdout: 'piped',
stderr: 'piped',
type TextClipboard = {
readText: (readTextOptions?: ReadTextOptions) => Promise<string>;
writeText: (data: string) => Promise<void>;
};

async function read(args: string[]): Promise<string> {
const p = Deno.run({ ...opt, args });
return decode(await p.output());
}

async function write(args: string[], data: string): Promise<void> {
const p = Deno.run({ ...opt, args });
await p.stdin.write(encode(data));
p.stdin.close();
await p.status();
}

const linux: Clipboard = {
os: 'linux',
async readText() {
// return read(['xclip', '-selection', 'clipboard', '-o']);
return read(['xsel', '-b', '-o']);
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(data) {
// return write(['xclip', '-selection', 'clipboard'], data);
return write(['xsel', '-b', '-i'], data);

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 mac: Clipboard = {
os: 'mac',
async readText() {
return read(['pbpaste']);
const darwin: TextClipboard = {
readText (readTextOptions?: ReadTextOptions): Promise<string> {
const cmd: string[] = ['pbpaste'];
return shared.readText(cmd, readTextOptions);
},
async writeText(data) {
return write(['pbcopy'], data);

writeText (data: string): Promise<void> {
const cmd: string[] = ['pbcopy'];
return shared.writeText(cmd, data);
},
};

const win: Clipboard = {
os: 'win',
async readText() {
const data = await read(['powershell', '-noprofile', '-command', 'Get-Clipboard']);
return data.replace(/\r/g, '').replace(/\n$/, '');
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);
},
async writeText(data) {
return write(['powershell', '-noprofile', '-command', '$input|Set-Clipboard'], data);

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 dispatch: Dispatch = {
linux,
mac,
win,
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);
},
};

class Clipboard {
os: Deno.OperatingSystem;
constructor(os: Deno.OperatingSystem) {
if (!dispatch[os]) {
throw new Error(`Clipboard: unsupported OS: ${os}`);
}
this.os = os;
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();
}
async readText(): Promise<string> {
return dispatch[this.os].readText();
catch (err) {
return '';
}
async writeText(data: string): Promise<void> {
return dispatch[this.os].writeText(data);
};

const resolveLinuxBinary = async (): Promise<LinuxBinary> => {
type BinaryEntry = [LinuxBinary, () => boolean | Promise<boolean>];

const binaryEntries: BinaryEntry[] = [
['wsl', async () => {
const isWSL = (await getProcessOutput(['uname', '-r', '-v'])).toLowerCase().includes('microsoft');
if (!isWSL) return false;
const hasWindowsUtils = (
Boolean(await getProcessOutput(['which', 'clip.exe']))
&& Boolean(await getProcessOutput(['which', 'powershell.exe']))
);
if (hasWindowsUtils) return true;
throw new Error(errMsg.noClipboardWSL);
}],
['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;
}
}

export const clipboard = new Clipboard(Deno.build.os);
throw new Error(errMsg.noClipboard);
};

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: (readTextOptions?: ReadTextOptions) => Promise<string> = clipboards[os].readText;

/**
* Writes a string to the clipboard. Requires the `--allow-run` flag.
*/
export const writeText: (data: string) => Promise<void> = clipboards[os].writeText;
2 changes: 1 addition & 1 deletion test.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/sh

deno test --allow-run --allow-net test.ts
deno test --allow-run test.ts
Loading