@guanghechen/mini-copy
Version:
Access system clipboard (also support to share clipboard in wsl with windows).
124 lines (118 loc) • 4.6 kB
JavaScript
import { spawn } from 'node:child_process';
import { writeFile, mkdirsIfNotExists } from '@guanghechen/fs';
import invariant from '@guanghechen/invariant';
import { existsSync, statSync } from 'node:fs';
import fs from 'node:fs/promises';
import { safeExec } from '@guanghechen/exec';
const DEFAULT_LINE_END = process.platform === 'win32' ? '\r\n' : '\n';
async function copy(content, options = {}) {
const { copyCommandPath, copyCommandArgs = [], fakeClipboard, reporter } = options;
try {
reporter?.debug('[copy] try: clipboardy');
const clipboardy = await import('clipboardy').then(md => md.default);
await clipboardy.write(content);
return;
}
catch (error) {
reporter?.debug('[copy] Failed to write clipboard through clipboardy:', error);
}
if (copyCommandPath != null) {
reporter?.debug(`[copy] try: ${copyCommandPath} ${copyCommandArgs.join(' ')}`);
try {
await new Promise((resolve, reject) => {
const command = spawn(copyCommandPath);
command.stdin.write(content);
command.stdin.end();
command.on('close', code => {
if (code === 0)
resolve();
else
reject('[paste] Failed to copy content to clipboard');
});
});
return;
}
catch (error) {
reporter?.debug(`[copy] Failed to call ${copyCommandPath}`, error);
}
}
if (fakeClipboard != null) {
reporter?.debug('[copy] try: fake clipboard {}.', fakeClipboard.filepath);
await fakeClipboard.write(content);
return;
}
throw '[copy] Cannot find available clipboard or fake-clipboard.';
}
class FakeClipboard {
filepath;
encoding;
reporter = null;
isInitialized = false;
constructor(props) {
const { filepath, encoding, reporter } = props;
this.reporter = reporter ?? null;
this.encoding = encoding ?? 'utf8';
this.filepath = filepath;
}
async read(_options = {}) {
await this.init();
return await fs.readFile(this.filepath, this.encoding);
}
async write(content, _options = {}) {
await this.init();
invariant(this.filepath != null, '[FakeClipboard.write] filepath is null/undefined.');
await writeFile(this.filepath, content, this.encoding);
}
async init() {
if (this.isInitialized)
return;
const { filepath, reporter } = this;
if (existsSync(filepath)) {
invariant(statSync(filepath).isFile(), () => `[FakeClipboard] ${filepath} is not a file`);
return;
}
reporter?.verbose(`[FakeClipboard] init fake-clipboard (${filepath}).`);
mkdirsIfNotExists(filepath, false);
await fs.writeFile(filepath, '', this.encoding);
this.isInitialized = true;
}
}
async function paste(options = {}) {
const { pasteCommandPath, pasteCommandArgs = [], fakeClipboard, reporter, lineEnd = DEFAULT_LINE_END, } = options;
try {
reporter?.debug('[paste] try: clipboardy');
const clipboardy = await import('clipboardy').then(md => md.default);
const content = await clipboardy.read();
return normalizeOutput(content, lineEnd);
}
catch (error) {
reporter?.debug('[paste] Failed to read clipboard through clipboardy:', error);
}
if (pasteCommandPath != null) {
reporter?.debug(`[paste] try: ${pasteCommandPath} ${pasteCommandArgs.join(' ')}`);
try {
const { stdout } = await safeExec({
from: 'paste',
cmd: pasteCommandPath,
args: pasteCommandArgs,
});
let content = stdout.toString();
if (/powershell/.test(pasteCommandPath)) {
content = content.replace(/^([^]*?)(?:\r\n|\n\r|[\n\r])$/, '$1');
}
return normalizeOutput(content, lineEnd);
}
catch (error) {
reporter?.debug(`[paste] Failed to call ${pasteCommandPath}:`, error);
}
}
if (fakeClipboard != null) {
reporter?.debug('[paste] try: fake clipboard {}.', fakeClipboard.filepath);
return await fakeClipboard.read();
}
throw '[paste] Cannot find available clipboard or fake-clipboard.';
}
function normalizeOutput(output, lineEnd) {
return output ? output.replace(/\r\n|\r|\n/g, lineEnd) : '';
}
export { DEFAULT_LINE_END, FakeClipboard, copy, paste };