slicc-clipboard
Clipboard commands for SLICC: pbcopy, pbpaste, xclip, xsel, wl-copy and wl-paste, as POSIX sh scripts over the kernel's /dev/clipboard character device. Programs that shell out to a clipboard tool, such as gh through atotto/clipboard, find one whichever they look for.
sccopy (stdin to the clipboard) and scpaste (the clipboard to stdout) work the same everywhere: in SLICC they are pbcopy and pbpaste, and on macOS, Linux and Windows they hand off to the platform's own clipboard tool (see On a host).
The kernel answers the device from its embedder's clipboard hook, which in seven is navigator.clipboard in seven's own page. It needs seven's tab in front, and Chrome's clipboard permission for reading. Text only.
Install
In SLICC, the package declares its commands in slicc.commands, so installing it puts them in /usr/bin:
pnpm add -g @ai-ecoverse/slicc-clipboard
It needs slicc-kernel 1.75.0 or later (engines.slicc-kernel). On an older kernel, which has no /dev/clipboard, every command except -h fails with <cmd>: /dev/clipboard not available (needs slicc-kernel >= 1.75.0) and exits 1, before it reads or writes anything.
Commands
| command | copies | pastes |
|---|---|---|
pbcopy | stdin | |
pbpaste | to stdout | |
xclip | -i, -in: stdin, or the files named | -o, -out |
xsel | -i, --input: stdin | -o, --output |
wl-copy | stdin, or the arguments joined by spaces | |
wl-paste | to stdout, then a newline | |
sccopy | stdin | |
scpaste | to stdout |
- xclip:
-selection(or-se,-sel, …) takesclipboard,primaryorsecondary, or any prefix of one (c,clip,p); all three are the one clipboard, andprimaryis the default.-rmlastnl(or-r) drops one newline at the end of what is copied or pasted. A file named-is stdin, and xclip has no--, so a file whose name starts with-needs a path (./-notes.txt). Without-ior-o, xclip copies, unless stdin is a terminal; then it pastes. Of-iand-o, the last one wins, and files are ignored when pasting. - xsel:
-b/--clipboard,-p/--primaryand-s/--secondaryall mean the one clipboard.-c/--clearempties it.-n/--nodetachis accepted and does nothing. Without a mode, xsel copies, unless stdin is a terminal; then it pastes. Of-i,-oand-c, the last one wins. It takes no other arguments. - wl-copy:
-n/--trim-newlinedrops one newline at the end.-c/--clearempties the clipboard. Options may come anywhere, until--. - wl-paste:
-n/--no-newlineleaves out the newline it adds. - sccopy, scpaste: the same as
pbcopyandpbpaste, under their own names. - Help:
-hand--help(-helpfor xclip) print the usage line to stdout and exit 0.
An unknown option (or an argument a command doesn't take) prints the command's error and its usage line to stderr and exits 2. When the device fails, the command prints name: and the error's message to stderr, and exits 1; for example, when the tab isn't in front:
pbcopy: Permission denied
pbcopy: the clipboard needs seven's tab in front, with clipboard access allowed
On a host
On a machine with Node 24 or later:
npm install -g @ai-ecoverse/slicc-clipboard
printf 'hello\n' | sccopy
scpaste
or without installing, npx -p @ai-ecoverse/slicc-clipboard sccopy. Only sccopy and scpaste are installed (the package's bin, small Node scripts in host/); the host's own pbcopy, xclip and the rest are never shadowed. They hand off to the platform's clipboard tool, the first one on PATH:
| platform | sccopy | scpaste |
|---|---|---|
| macOS | pbcopy | pbpaste |
Linux, under Wayland (WAYLAND_DISPLAY set) | wl-copy | wl-paste --no-newline |
| Linux, X11 | xclip -selection clipboard -i, else xsel --clipboard --input | xclip -selection clipboard -o, else xsel --clipboard --output |
| Windows | PowerShell Set-Clipboard | PowerShell Get-Clipboard -Raw |
- Text only, bytes in = bytes out: UTF-8 and a trailing newline survive a round trip unchanged, and
scpasteadds nothing. On macOS,pbcopyandpbpasterun withLANGandLC_ALLset toen_US.UTF-8. On Windows,powershell.exe(elsepwsh.exe) reads stdin and writes stdout as raw UTF-8 bytes, not through the console code page;clip.exeisn't used, as it mangles non-ASCII. Under Wayland,wl-copyandwl-pastecome first, thenxclipandxsel(through XWayland). Other Unix systems are treated as Linux. - Spawning: the tool runs with a fixed argv and no shell.
sccopydoesn't wait for the background process thatxclip,xselandwl-copyleave behind to serve the clipboard. - Errors: with no tool on
PATH,sccopy: no clipboard tool found (tried xclip, xsel)(the names tried on that platform) to stderr, exit 1. A tool that fails has its stderr passed through, exit 1.-hand--helpprintusage: sccopy(orscpaste) to stdout and exit 0; any other argument printssccopy: unknown option: <arg>and the usage line to stderr, and exits 2.
In SLICC, the sh commands win. The package has both bin and slicc.commands, and both name sccopy and scpaste. The kernel ignores a package's bin entry when the same package declares that name in slicc.commands (where it reads bin at all, in its appended command roots), so /usr/bin/sccopy is always bin/sccopy. bios's base set doesn't put bin links on PATH, and after a pnpm add -g, bios removes the $PNPM_HOME/bin link pnpm made for each name a package declares in both bin and slicc.commands, so the Node scripts, which SLICC has no Node to run, never shadow the sh ones.
The device
What the scripts rely on from /dev/clipboard (slicc-kernel 1.75.0 and later):
- Reading gives the clipboard's text as UTF-8, from one snapshot, then EOF.
- Writing buffers per open file; the copy happens at
close(). Coreutilscatreports an error there as a write error, so the scripts always write throughcat, never a shell builtin. - A copy over 1 MiB fails with
EFBIG, invalid UTF-8 withEILSEQ, a refusal (the tab not in front, or no permission) withEACCES, an embedder without a clipboard withENOTSUP, anything else withEIO. Coreutils sees only the errno, so the commands print its message (Permission denied,Not supported,File too large,Illegal byte sequence,I/O error), not the hook's. AfterPermission denied, they add a second line that says what to do.
Copying or pasting NUL bytes isn't supported: the text goes through shell variables when it has to be trimmed.
Development
npm install
npm run lint
npm test
npm run test:unit
npm run lint: Biome, the no-comment check and the other slicc-shared-web checks,shellcheck -s shanddash -non every script.npm test: the integration tests intest/integration/. The kernel tests run on@ai-ecoverse/slicc-kernel/nodewith wasm-bash and wasm-coreutils, and a fakeclipboardhook behind the real/dev/clipboard: gh's command lines, a round trip, UTF-8, an empty clipboard, the options of all eight,EACCES,EIO,ENOTSUP,EFBIG,EILSEQ, an ordinary user underusers: 'enforce', slicc-kernel 1.74.0, which has no/dev/clipboard(as the devDependencyslicc-kernel-without-clipboard), and a kernel that reads npmbinentries, wheresccopyandscpastemust still be theshones.test/integration/host.test.mjsrunshost/against fakepbcopy,pbpaste,wl-copy,wl-paste,xclip,xsel,powershell.exeandpwsh.exeon a temporaryPATH, with the platform forced throughrun({ platform }), so each branch is checked on any machine: the tool picked, its argv, its stdin and stdout, the UTF-8 locale on macOS, the PowerShell script on Windows, the fallbacks, a failing tool's stderr, an emptyPATHand the argument errors. On CI under Linux with an X display, it also round-trips UTF-8, a trailing newline and an empty clipboard through the realxclipandxselwhen they're installed, and skips that, saying why, everywhere else.test/integration/repo.test.mjschecks the workflow, the release config and whatnpm packships.npm run test:unit: the unit tests in the gitignoredtest/unit/. They run every script under dash and bash, against copies of the scripts that use a temporary file as the device (the shipped scripts always use/dev/clipboard), andscripts/cover.mjsturns bash'sxtraceinto line coverage incoverage/unit/lcov.info, next to Node's coverage ofhost/, whose tests use fake tools onPATH. The pre-commit hook wants 100% coverage of the changed lines.
CI is .github/workflows/ci.yml, the only workflow, which runs slicc-shared-web's pipeline at the devDependency's version, on Linux only: npm run lint, then npm test split over two shards, then on a pull request the Claude and Codex reviews. The merge queue requires ci / integration and ci / review. On a push to main, semantic-release releases and publishes to npm through trusted publishing, with provenance from ci.yml.
License
Apache-2.0