Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post text, images, videos, and long-form articles to X (Twitter) via real Chrome browser automation.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/copy-to-clipboard.ts
1import { spawn } from 'node:child_process';2import fs from 'node:fs';3import { mkdtemp, rm, writeFile } from 'node:fs/promises';4import os from 'node:os';5import path from 'node:path';6import process from 'node:process';78const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);910function printUsage(exitCode = 0): never {11console.log(`Copy image or HTML to system clipboard1213Supports:14- Image files (jpg, png, gif, webp) - copies as image data15- HTML content - copies as rich text for paste1617Usage:18# Copy image to clipboard19npx -y bun copy-to-clipboard.ts image /path/to/image.jpg2021# Copy HTML to clipboard22npx -y bun copy-to-clipboard.ts html "<p>Hello</p>"2324# Copy HTML from file25npx -y bun copy-to-clipboard.ts html --file /path/to/content.html26`);27process.exit(exitCode);28}2930function resolvePath(filePath: string): string {31return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);32}3334function inferImageMimeType(imagePath: string): string {35const ext = path.extname(imagePath).toLowerCase();36switch (ext) {37case '.jpg':38case '.jpeg':39return 'image/jpeg';40case '.png':41return 'image/png';42case '.gif':43return 'image/gif';44case '.webp':45return 'image/webp';46default:47return 'application/octet-stream';48}49}5051type RunResult = { stdout: string; stderr: string; exitCode: number };5253async function runCommand(54command: string,55args: string[],56options?: { input?: string | Buffer; allowNonZeroExit?: boolean },57): Promise<RunResult> {58return await new Promise<RunResult>((resolve, reject) => {59const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });60const stdoutChunks: Buffer[] = [];61const stderrChunks: Buffer[] = [];6263child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));64child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));65child.on('error', reject);66child.on('close', (code) => {67resolve({68stdout: Buffer.concat(stdoutChunks).toString('utf8'),69stderr: Buffer.concat(stderrChunks).toString('utf8'),70exitCode: code ?? 0,71});72});7374if (options?.input != null) child.stdin.write(options.input);75child.stdin.end();76}).then((result) => {77if (!options?.allowNonZeroExit && result.exitCode !== 0) {78const details = result.stderr.trim() || result.stdout.trim();79throw new Error(`Command failed (${command}): exit ${result.exitCode}${details ? `\n${details}` : ''}`);80}81return result;82});83}8485async function commandExists(command: string): Promise<boolean> {86if (process.platform === 'win32') {87const result = await runCommand('where', [command], { allowNonZeroExit: true });88return result.exitCode === 0 && result.stdout.trim().length > 0;89}90const result = await runCommand('which', [command], { allowNonZeroExit: true });91return result.exitCode === 0 && result.stdout.trim().length > 0;92}9394async function runCommandWithFileStdin(command: string, args: string[], filePath: string): Promise<void> {95await new Promise<void>((resolve, reject) => {96const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });97const stderrChunks: Buffer[] = [];98const stdoutChunks: Buffer[] = [];99100child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));101child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));102child.on('error', reject);103child.on('close', (code) => {104const exitCode = code ?? 0;105if (exitCode !== 0) {106const details = Buffer.concat(stderrChunks).toString('utf8').trim() || Buffer.concat(stdoutChunks).toString('utf8').trim();107reject(108new Error(`Command failed (${command}): exit ${exitCode}${details ? `\n${details}` : ''}`),109);110return;111}112resolve();113});114115fs.createReadStream(filePath).on('error', reject).pipe(child.stdin);116});117}118119async function withTempDir<T>(prefix: string, fn: (tempDir: string) => Promise<T>): Promise<T> {120const tempDir = await mkdtemp(path.join(os.tmpdir(), prefix));121try {122return await fn(tempDir);123} finally {124await rm(tempDir, { recursive: true, force: true });125}126}127128function getMacSwiftClipboardSource(): string {129return `import AppKit130import Foundation131132func die(_ message: String, _ code: Int32 = 1) -> Never {133FileHandle.standardError.write(message.data(using: .utf8)!)134exit(code)135}136137if CommandLine.arguments.count < 3 {138die("Usage: clipboard.swift <image|html> <path>\\n")139}140141let mode = CommandLine.arguments[1]142let inputPath = CommandLine.arguments[2]143let pasteboard = NSPasteboard.general144pasteboard.clearContents()145146switch mode {147case "image":148guard let image = NSImage(contentsOfFile: inputPath) else {149die("Failed to load image: \\(inputPath)\\n")150}151if !pasteboard.writeObjects([image]) {152die("Failed to write image to clipboard\\n")153}154155case "html":156let url = URL(fileURLWithPath: inputPath)157let data: Data158do {159data = try Data(contentsOf: url)160} catch {161die("Failed to read HTML file: \\(inputPath)\\n")162}163164_ = pasteboard.setData(data, forType: .html)165166let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [167.documentType: NSAttributedString.DocumentType.html,168.characterEncoding: String.Encoding.utf8.rawValue169]170171if let attr = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {172pasteboard.setString(attr.string, forType: .string)173if let rtf = try? attr.data(174from: NSRange(location: 0, length: attr.length),175documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]176) {177_ = pasteboard.setData(rtf, forType: .rtf)178}179} else if let html = String(data: data, encoding: .utf8) {180pasteboard.setString(html, forType: .string)181}182183default:184die("Unknown mode: \\(mode)\\n")185}186`;187}188189async function copyImageMac(imagePath: string): Promise<void> {190await withTempDir('copy-to-clipboard-', async (tempDir) => {191const swiftPath = path.join(tempDir, 'clipboard.swift');192await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');193await runCommand('swift', [swiftPath, 'image', imagePath]);194});195}196197async function copyHtmlMac(htmlFilePath: string): Promise<void> {198await withTempDir('copy-to-clipboard-', async (tempDir) => {199const swiftPath = path.join(tempDir, 'clipboard.swift');200await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');201await runCommand('swift', [swiftPath, 'html', htmlFilePath]);202});203}204205async function copyImageLinux(imagePath: string): Promise<void> {206const mime = inferImageMimeType(imagePath);207if (await commandExists('wl-copy')) {208await runCommandWithFileStdin('wl-copy', ['--type', mime], imagePath);209return;210}211if (await commandExists('xclip')) {212await runCommand('xclip', ['-selection', 'clipboard', '-t', mime, '-i', imagePath]);213return;214}215throw new Error('No clipboard tool found. Install `wl-clipboard` (wl-copy) or `xclip`.');216}217218async function copyHtmlLinux(htmlFilePath: string): Promise<void> {219if (await commandExists('wl-copy')) {220await runCommandWithFileStdin('wl-copy', ['--type', 'text/html'], htmlFilePath);221return;222}223if (await commandExists('xclip')) {224await runCommand('xclip', ['-selection', 'clipboard', '-t', 'text/html', '-i', htmlFilePath]);225return;226}227throw new Error('No clipboard tool found. Install `wl-clipboard` (wl-copy) or `xclip`.');228}229230async function copyImageWindows(imagePath: string): Promise<void> {231const escaped = imagePath.replace(/'/g, "''");232const ps = [233'Add-Type -AssemblyName System.Windows.Forms',234'Add-Type -AssemblyName System.Drawing',235`$img = [System.Drawing.Image]::FromFile('${escaped}')`,236'[System.Windows.Forms.Clipboard]::SetImage($img)',237'$img.Dispose()',238].join('; ');239await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);240}241242async function copyHtmlWindows(htmlFilePath: string): Promise<void> {243const escaped = htmlFilePath.replace(/'/g, "''");244const ps = [245'Add-Type -AssemblyName System.Windows.Forms',246`$html = Get-Content -Raw -LiteralPath '${escaped}'`,247'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',248].join('; ');249await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);250}251252async function copyImageToClipboard(imagePathInput: string): Promise<void> {253const imagePath = resolvePath(imagePathInput);254const ext = path.extname(imagePath).toLowerCase();255if (!SUPPORTED_IMAGE_EXTS.has(ext)) {256throw new Error(257`Unsupported image type: ${ext || '(none)'} (supported: ${Array.from(SUPPORTED_IMAGE_EXTS).join(', ')})`,258);259}260if (!fs.existsSync(imagePath)) throw new Error(`File not found: ${imagePath}`);261262switch (process.platform) {263case 'darwin':264await copyImageMac(imagePath);265return;266case 'linux':267await copyImageLinux(imagePath);268return;269case 'win32':270await copyImageWindows(imagePath);271return;272default:273throw new Error(`Unsupported platform: ${process.platform}`);274}275}276277async function copyHtmlFileToClipboard(htmlFilePathInput: string): Promise<void> {278const htmlFilePath = resolvePath(htmlFilePathInput);279if (!fs.existsSync(htmlFilePath)) throw new Error(`File not found: ${htmlFilePath}`);280281switch (process.platform) {282case 'darwin':283await copyHtmlMac(htmlFilePath);284return;285case 'linux':286await copyHtmlLinux(htmlFilePath);287return;288case 'win32':289await copyHtmlWindows(htmlFilePath);290return;291default:292throw new Error(`Unsupported platform: ${process.platform}`);293}294}295296async function readStdinText(): Promise<string | null> {297if (process.stdin.isTTY) return null;298const chunks: Buffer[] = [];299for await (const chunk of process.stdin) {300chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));301}302const text = Buffer.concat(chunks).toString('utf8');303return text.length > 0 ? text : null;304}305306async function copyHtmlToClipboard(args: string[]): Promise<void> {307let htmlFile: string | undefined;308const positional: string[] = [];309310for (let i = 0; i < args.length; i += 1) {311const arg = args[i] ?? '';312if (arg === '--help' || arg === '-h') printUsage(0);313if (arg === '--file') {314htmlFile = args[i + 1];315i += 1;316continue;317}318if (arg.startsWith('--file=')) {319htmlFile = arg.slice('--file='.length);320continue;321}322if (arg === '--') {323positional.push(...args.slice(i + 1));324break;325}326if (arg.startsWith('-')) {327throw new Error(`Unknown option: ${arg}`);328}329positional.push(arg);330}331332if (htmlFile && positional.length > 0) {333throw new Error('Do not pass HTML text when using --file.');334}335336if (htmlFile) {337await copyHtmlFileToClipboard(htmlFile);338return;339}340341const htmlFromArgs = positional.join(' ').trim();342const htmlFromStdin = (await readStdinText())?.trim() ?? '';343const html = htmlFromArgs || htmlFromStdin;344if (!html) throw new Error('Missing HTML input. Provide a string or use --file.');345346await withTempDir('copy-to-clipboard-', async (tempDir) => {347const htmlPath = path.join(tempDir, 'input.html');348await writeFile(htmlPath, html, 'utf8');349await copyHtmlFileToClipboard(htmlPath);350});351}352353async function main(): Promise<void> {354const argv = process.argv.slice(2);355if (argv.length === 0) printUsage(1);356357const command = argv[0];358if (command === '--help' || command === '-h') printUsage(0);359360if (command === 'image') {361const imagePath = argv[1];362if (!imagePath) throw new Error('Missing image path.');363await copyImageToClipboard(imagePath);364return;365}366367if (command === 'html') {368await copyHtmlToClipboard(argv.slice(1));369return;370}371372throw new Error(`Unknown command: ${command}`);373}374375await main().catch((err) => {376const message = err instanceof Error ? err.message : String(err);377console.error(`Error: ${message}`);378process.exit(1);379});380381