"use strict"; const path = require("node:path"); const fs = require("node:fs"); const { fileURLToPath } = require("node:url"); const HDROP_FORMATS = ["CF_HDROP", "FileDrop"]; const URI_LIST_FORMATS = ["text/uri-list", "text/x-moz-url"]; function decodeWindowsFileNameW(buffer) { if (!Buffer.isBuffer(buffer) || buffer.length === 0) return []; return buffer .toString("utf16le") .split("\0") .map((entry) => entry.trim()) .filter(Boolean); } function decodeWindowsFileName(buffer) { if (!Buffer.isBuffer(buffer) || buffer.length === 0) return []; return buffer .toString("utf8") .split("\0") .map((entry) => entry.trim()) .filter(Boolean); } function decodeWindowsHDrop(buffer) { if (!Buffer.isBuffer(buffer) || buffer.length < 20) return []; const filesOffset = buffer.readUInt32LE(0); if (filesOffset <= 0 || filesOffset >= buffer.length) return []; const isWide = buffer.readUInt32LE(16) !== 0; const payload = buffer.subarray(filesOffset); const text = payload.toString(isWide ? "utf16le" : "utf8"); return text .split("\0") .map((entry) => entry.trim()) .filter(Boolean); } function decodeFileUri(value, { windows = process.platform === "win32" } = {}) { if (!value.startsWith("file://")) return value; try { return fileURLToPath(value, { windows }); } catch { return value; } } function toClipboardFile(filePath, { fsImpl = fs, pathImpl = path } = {}) { if (!filePath || !fsImpl.existsSync(filePath)) return null; try { const stat = fsImpl.statSync(filePath); const isDirectory = stat.isDirectory(); if (!isDirectory && !stat.isFile()) return null; const name = filePath.includes("\\") ? path.win32.basename(filePath) : pathImpl.basename(filePath); return { path: filePath, name, isDirectory, size: isDirectory ? 0 : stat.size, }; } catch { return null; } } function collectExistingFiles(paths, options = {}) { const seen = new Set(); const files = []; for (const candidate of paths) { if (!candidate || seen.has(candidate)) continue; seen.add(candidate); const file = toClipboardFile(candidate, options); if (file) files.push(file); } return files; } function parseClipboardTextFilePaths(text, options = {}) { if (!text) return []; const candidates = text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#") && line.startsWith("file://")) .map((line) => decodeFileUri(line, options)); return collectExistingFiles(candidates, options); } function readClipboardFiles({ clipboard, fsImpl = fs, pathImpl = path, } = {}) { if (!clipboard) return []; const options = { fsImpl, pathImpl }; try { const formats = typeof clipboard.availableFormats === "function" ? clipboard.availableFormats() : []; if (typeof clipboard.readBuffer === "function") { for (const format of HDROP_FORMATS) { if (!formats.includes(format)) continue; const files = collectExistingFiles(decodeWindowsHDrop(clipboard.readBuffer(format)), options); if (files.length > 0) return files; } } if (formats.includes("FileNameW") && typeof clipboard.readBuffer === "function") { const files = collectExistingFiles(decodeWindowsFileNameW(clipboard.readBuffer("FileNameW")), options); if (files.length > 0) return files; } if (formats.includes("FileName") && typeof clipboard.readBuffer === "function") { const files = collectExistingFiles(decodeWindowsFileName(clipboard.readBuffer("FileName")), options); if (files.length > 0) return files; } if ( typeof clipboard.readText === "function" && formats.some((format) => URI_LIST_FORMATS.includes(format)) ) { return parseClipboardTextFilePaths(clipboard.readText(), options); } if (typeof clipboard.read === "function") { for (const format of ["NSFilenamesPboardType", "public.file-url"]) { if (!formats.includes(format)) continue; const raw = clipboard.read(format); if (typeof raw !== "string" || raw.trim().length === 0) continue; if (raw.includes("file://")) { const files = parseClipboardTextFilePaths(raw, options); if (files.length > 0) return files; } const files = collectExistingFiles( raw.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean), options, ); if (files.length > 0) return files; } } } catch { return []; } return []; } function formatClipboardImageTimestamp(date = new Date()) { const pad = (value, width = 2) => String(value).padStart(width, "0"); return [ date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate()), "-", pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds()), "-", pad(date.getMilliseconds(), 3), ].join(""); } function createClipboardImageFileName(date = new Date()) { return `netcatty-paste-${formatClipboardImageTimestamp(date)}.png`; } async function readClipboardImage({ clipboard, fsImpl = fs, tempDirBridge, now = () => new Date(), } = {}) { if (!clipboard || typeof clipboard.readImage !== "function" || !tempDirBridge?.getTempFilePath) { return null; } try { const image = clipboard.readImage(); if (!image || (typeof image.isEmpty === "function" && image.isEmpty())) return null; if (typeof image.toPNG !== "function") return null; const content = image.toPNG(); if (!Buffer.isBuffer(content) || content.length === 0) return null; const name = createClipboardImageFileName(now()); const imagePath = tempDirBridge.getTempFilePath(name); await fsImpl.promises.writeFile(imagePath, content); return { path: imagePath, name, mediaType: "image/png", size: content.length, }; } catch { return null; } } /** * Peek whether the OS clipboard currently holds an image without writing a * temp file. Used by local terminal paste to decide whether to forward Ctrl+V * to nested TUIs (e.g. Claude Code image paste) after Electron's Edit>Paste * turns the chord into a clipboard paste event. */ function hasClipboardImage({ clipboard } = {}) { if (!clipboard || typeof clipboard.readImage !== "function") return false; try { if (typeof clipboard.availableFormats === "function") { try { const formats = clipboard.availableFormats(); if (Array.isArray(formats) && formats.some((format) => typeof format === "string" && format.startsWith("image/"))) { return true; } } catch { // Fall through to readImage when format probing fails. } } const image = clipboard.readImage(); if (!image) return false; if (typeof image.isEmpty === "function" && image.isEmpty()) return false; if (typeof image.getSize === "function") { const size = image.getSize(); return Boolean(size && size.width > 0 && size.height > 0); } if (typeof image.toPNG !== "function") return false; const content = image.toPNG(); return Buffer.isBuffer(content) && content.length > 0; } catch { return false; } } module.exports = { createClipboardImageFileName, decodeWindowsHDrop, decodeWindowsFileNameW, hasClipboardImage, parseClipboardTextFilePaths, readClipboardFiles, readClipboardImage, };