UNPKG

@guanghechen/mini-copy

Version:

Access system clipboard (also support to share clipboard in wsl with windows).

124 lines (118 loc) 4.6 kB
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 };