Skip to content

Commit 892ad23

Browse files
committed
Add Wayland clipboard support
Fixes #38
1 parent c55839e commit 892ad23

File tree

9 files changed

+193
-107
lines changed

9 files changed

+193
-107
lines changed

index.d.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ declare const clipboard: {
99
import clipboard from 'clipboardy';
1010
1111
await clipboard.write('🦄');
12-
13-
await clipboard.read();
14-
//=> '🦄'
1512
```
1613
*/
1714
write(text: string): Promise<void>;
@@ -23,9 +20,7 @@ declare const clipboard: {
2320
```
2421
import clipboard from 'clipboardy';
2522
26-
await clipboard.write('🦄');
27-
28-
await clipboard.read();
23+
const content = await clipboard.read();
2924
//=> '🦄'
3025
```
3126
*/
@@ -43,9 +38,6 @@ declare const clipboard: {
4338
import clipboard from 'clipboardy';
4439
4540
clipboard.writeSync('🦄');
46-
47-
clipboard.readSync();
48-
//=> '🦄'
4941
```
5042
*/
5143
writeSync(text: string): void;
@@ -59,9 +51,7 @@ declare const clipboard: {
5951
```
6052
import clipboard from 'clipboardy';
6153
62-
clipboard.writeSync('🦄');
63-
64-
clipboard.readSync();
54+
const content = clipboard.readSync();
6555
//=> '🦄'
6656
```
6757
*/

index.js

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import process from 'node:process';
22
import isWSL from 'is-wsl';
3+
import isWayland from 'is-wayland';
34
import termux from './lib/termux.js';
45
import linux from './lib/linux.js';
6+
import wayland from './lib/wayland.js';
57
import macos from './lib/macos.js';
68
import windows from './lib/windows.js';
79

@@ -29,31 +31,40 @@ const platformLib = (() => {
2931
return windows;
3032
}
3133

34+
// Check for Wayland session on Linux
35+
if (isWayland()) {
36+
return wayland;
37+
}
38+
3239
return linux;
3340
}
3441
}
3542
})();
3643

37-
const clipboard = {};
44+
const clipboard = {
45+
async write(text) {
46+
if (typeof text !== 'string') {
47+
throw new TypeError(`Expected a string, got ${typeof text}`);
48+
}
3849

39-
clipboard.write = async text => {
40-
if (typeof text !== 'string') {
41-
throw new TypeError(`Expected a string, got ${typeof text}`);
42-
}
50+
await platformLib.copy({input: text});
51+
},
4352

44-
await platformLib.copy({input: text});
45-
};
53+
async read() {
54+
return platformLib.paste({stripFinalNewline: false});
55+
},
4656

47-
clipboard.read = async () => platformLib.paste({stripFinalNewline: false});
57+
writeSync(text) {
58+
if (typeof text !== 'string') {
59+
throw new TypeError(`Expected a string, got ${typeof text}`);
60+
}
4861

49-
clipboard.writeSync = text => {
50-
if (typeof text !== 'string') {
51-
throw new TypeError(`Expected a string, got ${typeof text}`);
52-
}
62+
platformLib.copySync({input: text});
63+
},
5364

54-
platformLib.copySync({input: text});
65+
readSync() {
66+
return platformLib.pasteSync({stripFinalNewline: false});
67+
},
5568
};
5669

57-
clipboard.readSync = () => platformLib.pasteSync({stripFinalNewline: false});
58-
5970
export default clipboard;

lib/linux.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ const copyArguments = ['--clipboard', '--input'];
1111
const pasteArguments = ['--clipboard', '--output'];
1212

1313
const makeError = (xselError, fallbackError) => {
14-
let error;
15-
if (xselError.code === 'ENOENT') {
16-
error = new Error('Couldn\'t find the `xsel` binary and fallback didn\'t work. On Debian/Ubuntu you can install xsel with: sudo apt install xsel');
17-
} else {
18-
error = new Error('Both xsel and fallback failed');
14+
const message = xselError.code === 'ENOENT'
15+
? 'Couldn\'t find the `xsel` binary and fallback didn\'t work. On Debian/Ubuntu you can install xsel with: sudo apt install xsel'
16+
: 'Both xsel and fallback failed';
17+
18+
const error = new Error(message);
19+
20+
if (xselError.code !== 'ENOENT') {
1921
error.xselError = xselError;
2022
}
2123

lib/termux.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {execa, execaSync} from 'execa';
22

33
const handler = error => {
44
if (error.code === 'ENOENT') {
5-
throw new Error('Couldn\'t find the termux-api scripts. You can install them with: apt install termux-api');
5+
throw new Error('Couldn\'t find the `termux-api` scripts. You can install them with: apt install termux-api');
66
}
77

88
throw error;

lib/wayland.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {execa, execaSync} from 'execa';
2+
import linux from './linux.js';
3+
4+
// Common arguments for text clipboard operations
5+
const textArgs = ['--type', 'text/plain'];
6+
7+
const makeError = (command, error) => {
8+
if (error.code === 'ENOENT') {
9+
return new Error(`Couldn't find the \`${command}\` binary. On Debian/Ubuntu you can install wl-clipboard with: sudo apt install wl-clipboard`);
10+
}
11+
12+
return new Error(`Command \`${command}\` failed: ${error.message}`);
13+
};
14+
15+
const tryWaylandWithFallback = async (command, arguments_, options, fallbackMethod) => {
16+
try {
17+
const result = await execa(command, arguments_, options);
18+
return result.stdout;
19+
} catch (error) {
20+
// Handle empty clipboard on wl-paste
21+
if (command === 'wl-paste' && /nothing is copied|no selection|selection owner/i.test(error.stderr || '')) {
22+
return '';
23+
}
24+
25+
// Fall back to X11 if wl-clipboard not found OR Wayland not available
26+
if (error.code === 'ENOENT'
27+
|| /wayland|wayland_display|failed to connect|display/i.test(error.stderr || '')) {
28+
return fallbackMethod(options);
29+
}
30+
31+
throw makeError(command, error);
32+
}
33+
};
34+
35+
const tryWaylandWithFallbackSync = (command, arguments_, options, fallbackMethod) => {
36+
try {
37+
const result = execaSync(command, arguments_, options);
38+
return result.stdout;
39+
} catch (error) {
40+
// Handle empty clipboard on wl-paste
41+
if (command === 'wl-paste' && /nothing is copied|no selection|selection owner/i.test(error.stderr || '')) {
42+
return '';
43+
}
44+
45+
// Fall back to X11 if wl-clipboard not found OR Wayland not available
46+
if (error.code === 'ENOENT'
47+
|| /wayland|wayland_display|failed to connect|display/i.test(error.stderr || '')) {
48+
return fallbackMethod(options);
49+
}
50+
51+
throw makeError(command, error);
52+
}
53+
};
54+
55+
const clipboard = {
56+
async copy(options) {
57+
await tryWaylandWithFallback('wl-copy', textArgs, options, linux.copy);
58+
},
59+
copySync(options) {
60+
tryWaylandWithFallbackSync('wl-copy', textArgs, options, linux.copySync);
61+
},
62+
async paste(options) {
63+
return tryWaylandWithFallback('wl-paste', [...textArgs, '--no-newline'], options, linux.paste);
64+
},
65+
pasteSync(options) {
66+
return tryWaylandWithFallbackSync('wl-paste', [...textArgs, '--no-newline'], options, linux.pasteSync);
67+
},
68+
};
69+
70+
export default clipboard;

lib/windows.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {Buffer} from 'node:buffer';
22
import path from 'node:path';
3-
import process from 'node:process';
43
import {fileURLToPath} from 'node:url';
54
import {execa, execaSync} from 'execa';
65
import {is64bitSync} from 'is64bit';
@@ -30,6 +29,7 @@ const createEncodedCommand = script => {
3029
// Robust PowerShell commands with error handling
3130
const psCopyScript = `
3231
try {
32+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
3333
$input = [Console]::In.ReadToEnd()
3434
if ($input -eq $null) { $input = '' }
3535
Set-Clipboard -Value $input

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"engines": {
2121
"node": ">=18"
2222
},
23-
"sideEffects": false,
2423
"scripts": {
2524
"test": "xo && ava && tsd"
2625
},
@@ -46,6 +45,7 @@
4645
],
4746
"dependencies": {
4847
"execa": "^8.0.1",
48+
"is-wayland": "^0.1.0",
4949
"is-wsl": "^3.1.0",
5050
"is64bit": "^2.0.0"
5151
},

readme.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Access the system clipboard (copy/paste)
44
5-
Cross-platform. Supports: macOS, Windows, Linux, OpenBSD, FreeBSD, Android with [Termux](https://termux.com/), and [modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#Browser_compatibility).
5+
Cross-platform. Supports: macOS, Windows, Linux (including Wayland), OpenBSD, FreeBSD, Android with [Termux](https://termux.com/), and [modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#Browser_compatibility).
66

77
## Install
88

@@ -15,6 +15,12 @@ npm install clipboardy
1515
```js
1616
import clipboard from 'clipboardy';
1717

18+
await clipboard.write('🦄');
19+
20+
await clipboard.read();
21+
//=> '🦄'
22+
23+
// Or use the synchronous API
1824
clipboard.writeSync('🦄');
1925

2026
clipboard.readSync();
@@ -23,27 +29,36 @@ clipboard.readSync();
2329

2430
## API
2531

26-
In the browser, it requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
32+
**Browser usage:** Requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS). Synchronous methods are not available in browsers.
2733

2834
### clipboard
2935

3036
#### .write(text)
3137

3238
Write (copy) to the clipboard asynchronously.
3339

34-
Returns a `Promise`.
40+
Returns a `Promise<void>`.
3541

3642
##### text
3743

3844
Type: `string`
3945

4046
The text to write to the clipboard.
4147

48+
```js
49+
await clipboard.write('🦄');
50+
```
51+
4252
#### .read()
4353

4454
Read (paste) from the clipboard asynchronously.
4555

46-
Returns a `Promise`.
56+
Returns a `Promise<string>`.
57+
58+
```js
59+
const content = await clipboard.read();
60+
//=> '🦄'
61+
```
4762

4863
#### .writeSync(text)
4964

@@ -57,12 +72,23 @@ Type: `string`
5772

5873
The text to write to the clipboard.
5974

75+
```js
76+
clipboard.writeSync('🦄');
77+
```
78+
6079
#### .readSync()
6180

6281
Read (paste) from the clipboard synchronously.
6382

83+
Returns a `string`.
84+
6485
**Doesn't work in browsers.**
6586

87+
```js
88+
const content = clipboard.readSync();
89+
//=> '🦄'
90+
```
91+
6692
## FAQ
6793

6894
#### Where can I find the source of the bundled binaries?
@@ -71,6 +97,10 @@ The [Linux binary](fallbacks/linux/xsel) is just a bundled version of [`xsel`](h
7197

7298
On Windows, clipboardy first tries the native PowerShell cmdlets (`Set-Clipboard`/`Get-Clipboard`) and falls back to the bundled binary if PowerShell is unavailable or restricted.
7399

100+
#### Does this work on Wayland?
101+
102+
Yes. On Linux, clipboardy automatically detects Wayland sessions and uses [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) when available. If not, it gracefully falls back to X11 tools. Also works with WSLg (Windows Subsystem for Linux GUI). Install `wl-clipboard` using your distribution's package manager.
103+
74104
## Related
75105

76106
- [clipboard-cli](https://github.com/sindresorhus/clipboard-cli) - CLI for this module

0 commit comments

Comments
 (0)