clipaste
Version:
Cross-platform CLI tool for clipboard operations - paste, copy, and manage text and images
970 lines (860 loc) • 32 kB
JavaScript
// clipboardy v3+ is ESM-only. In a CommonJS project we use dynamic import() and cache the promise.
const { spawn } = require('child_process')
const { performance } = require('perf_hooks')
const path = require('path')
const fs = require('fs')
const os = require('os')
let _clipboardyPromise = null
let _injectedClipboardy = null // test injection / mocking
// Dynamic phase profiling toggle
let _phaseEnabledFlag = !!process.env.CLIPASTE_PHASE_PROF
function phaseEnabled () { return _phaseEnabledFlag }
function enablePhaseProfiling () { _phaseEnabledFlag = true }
function disablePhaseProfiling () { _phaseEnabledFlag = false }
const _phaseStats = {}
function _recordPhase (name, dur) {
if (!phaseEnabled()) return
const s = (_phaseStats[name] = _phaseStats[name] || { total: 0, count: 0 })
s.total += dur
s.count += 1
}
const { isHeadlessEnvironment } = require('./utils/environment')
async function getClipboardy () {
if (_injectedClipboardy) return _injectedClipboardy
const start = phaseEnabled() ? performance.now() : 0
if (!_clipboardyPromise) {
_clipboardyPromise = import('clipboardy')
.then(mod => mod.default || mod)
.catch(err => {
throw new Error(`Failed to load clipboardy: ${err.message}`)
})
}
const result = await _clipboardyPromise
if (phaseEnabled()) _recordPhase('clipboardy.load', performance.now() - start)
return result
}
class ClipboardManager {
constructor () {
this.isWindows = process.platform === 'win32'
this._snapshot = null
this._snapshotTime = 0
this._snapshotTTL = parseInt(process.env.CLIPASTE_SNAPSHOT_TTL || '10', 10)
}
_cacheEnabled () { return !process.env.CLIPASTE_CACHE_DISABLE }
_testMode () { return process.env.NODE_ENV === 'test' }
_snapshotValid () {
if (!this._cacheEnabled() || this._testMode()) return false
if (!this._snapshot) return false
return (performance.now() - this._snapshotTime) <= this._snapshotTTL
}
_invalidateSnapshot () { this._snapshot = null; this._snapshotTime = 0 }
_updateSnapshot (raw, typeHint) {
if (!this._cacheEnabled()) return
const s = (typeof raw === 'string') ? raw : ''
const trimmed = s.trim()
const isEmpty = !trimmed
let type = typeHint
if (!type) {
if (isEmpty) type = 'empty'
else if (this.isBase64Image(s)) type = 'image'
else if (this.isBinaryData(s)) type = 'binary'
else type = 'text'
}
this._snapshot = { raw: s, isEmpty, type }
this._snapshotTime = performance.now()
}
getSnapshot () { return this._snapshotValid() ? this._snapshot : null }
// Windows-specific method to check clipboard content using PowerShell
async checkWindowsClipboard () {
if (!this.isWindows) return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
// Create a temporary PowerShell script file to avoid command line escaping issues
const tempScript = path.join(os.tmpdir(), `clipaste-check-${Date.now()}.ps1`)
const scriptContent = `Add-Type -AssemblyName System.Windows.Forms
$clipboard = [System.Windows.Forms.Clipboard]::GetDataObject()
if ($null -eq $clipboard) {
Write-Output "empty"
} else {
$formats = $clipboard.GetFormats()
if ($formats.Count -eq 0) {
Write-Output "empty"
} elseif ($clipboard.GetDataPresent([System.Windows.Forms.DataFormats]::Bitmap)) {
Write-Output "image"
} elseif ($clipboard.GetDataPresent([System.Windows.Forms.DataFormats]::Text)) {
$text = $clipboard.GetData([System.Windows.Forms.DataFormats]::Text)
if ([string]::IsNullOrWhiteSpace($text)) {
Write-Output "empty"
} else {
Write-Output "text"
}
} else {
Write-Output "unknown"
}
}`
try {
fs.writeFileSync(tempScript, scriptContent)
} catch (error) {
resolve(null)
return
}
const ps = spawn('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-File',
tempScript
], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
let hasErrored = false
ps.stdout.on('data', (data) => {
output += data.toString()
})
ps.stderr.on('data', () => {
hasErrored = true
})
ps.on('close', (code) => {
clearTimeout(timeout)
if (phaseEnabled()) _recordPhase('windows.check', performance.now() - outerStart)
// Clean up temp script file
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
} catch (e) { /* ignore */ }
if (hasErrored || code !== 0) {
resolve(null) // Fall back to clipboardy
} else {
resolve(output.trim())
}
})
// Timeout after 5 seconds
const timeout = setTimeout(() => {
ps.kill()
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
} catch (e) { /* ignore */ }
if (phaseEnabled()) _recordPhase('windows.check.timeout', performance.now() - outerStart)
resolve(null)
}, 5000)
})
}
// macOS-specific method to check clipboard content using AppleScript
async checkMacClipboard () {
if (process.platform !== 'darwin') return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
const osascript = spawn('osascript', ['-e', 'clipboard info'], {
stdio: ['pipe', 'pipe', 'pipe']
})
let output = ''
let hasErrored = false
osascript.stdout.on('data', (data) => {
output += data.toString()
})
osascript.stderr.on('data', () => {
hasErrored = true
})
osascript.on('close', (code) => {
clearTimeout(timeout)
if (phaseEnabled()) _recordPhase('mac.check', performance.now() - outerStart)
if (hasErrored || code !== 0) {
resolve(null)
} else {
const clipboardInfo = output.trim()
if (!clipboardInfo) {
resolve('empty')
} else if (clipboardInfo.includes('picture') ||
clipboardInfo.includes('PNGf') ||
clipboardInfo.includes('JPEG') ||
clipboardInfo.includes('TIFF') ||
clipboardInfo.includes('GIF') ||
clipboardInfo.includes('BMP')) {
resolve('image')
} else {
resolve('text')
}
}
})
// Timeout after 5 seconds
const timeout = setTimeout(() => {
osascript.kill()
if (phaseEnabled()) _recordPhase('mac.check.timeout', performance.now() - outerStart)
resolve(null)
}, 5000)
})
}
async hasContent () {
// In headless environments, simulate empty clipboard
// For unit tests with injected dependencies, don't treat as headless
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return false
}
try {
if (this._snapshotValid()) return !this._snapshot.isEmpty
const clipboardy = await getClipboardy()
let lastContentRead = ''
// Retry a few times in case of transient empty clipboard on some platforms (e.g., Windows or older Node versions)
for (let attempt = 0; attempt < 3; attempt++) {
try {
const t0 = phaseEnabled() ? performance.now() : 0
const content = await clipboardy.read()
if (phaseEnabled()) _recordPhase('clipboardy.read', performance.now() - t0)
lastContentRead = content || ''
if (content != null && content.trim().length > 0) {
this._updateSnapshot(content, 'text')
return true
}
} catch (error) {
// On Windows, if clipboardy fails but we detected content via PowerShell, return true
if (this.isWindows && (
error.message.includes('Element not found') ||
error.message.includes('Elementtiä ei löydy') ||
error.message.includes('Could not paste from clipboard') ||
error.message.includes('thread \'main\' panicked')
)) {
const winType = await this.checkWindowsClipboard()
if (winType === 'image' || winType === 'text') {
this._updateSnapshot(lastContentRead, winType === 'image' ? 'image' : 'text')
return true
}
if (winType === 'empty' || winType === null) {
this._updateSnapshot(lastContentRead, 'empty')
return false
}
}
// Re-throw other errors on final attempt
if (attempt === 2) throw error
}
// Small delay before retry unless last attempt
if (attempt < 2) await new Promise(resolve => setTimeout(resolve, 15))
}
// On macOS, if clipboardy returned empty, check if there's image content
if (process.platform === 'darwin') {
const macType = await this.checkMacClipboard()
if (macType === 'image') { this._updateSnapshot('', 'image'); return true }
if (macType === 'text') {
// Double-check if the text content is actually meaningful
try {
const t0 = phaseEnabled() ? performance.now() : 0
const content = await clipboardy.read()
if (phaseEnabled()) _recordPhase('clipboardy.read', performance.now() - t0)
const has = content != null && content.trim().length > 0
this._updateSnapshot(content || '', has ? 'text' : 'empty')
return has
} catch {
return false
}
}
}
return false
} catch (error) {
// In case of clipboard access errors, simulate empty clipboard in headless environments
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return false
}
throw new Error(`Failed to read clipboard: ${error.message}`)
}
}
async readText () {
// In headless environments, return empty string
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return ''
}
if (this._snapshotValid()) {
if (this._snapshot.type === 'text' || this._snapshot.type === 'empty') {
return this._snapshot.raw
}
}
try {
const clipboardy = await getClipboardy()
let finalContent = ''
for (let attempt = 0; attempt < 3; attempt++) {
try {
const t0 = phaseEnabled() ? performance.now() : 0
const content = await clipboardy.read()
if (phaseEnabled()) _recordPhase('clipboardy.read', performance.now() - t0)
if (content != null && content.length > 0) {
this._updateSnapshot(content, 'text')
return content
}
finalContent = content || ''
} catch (error) {
// On Windows, if clipboardy fails with the specific error, check if it's actually an image or completely empty
if (this.isWindows && (
error.message.includes('Element not found') ||
error.message.includes('Elementtiä ei löydy') ||
error.message.includes('Could not paste from clipboard') ||
error.message.includes('thread \'main\' panicked')
)) {
const winType = await this.checkWindowsClipboard()
if (winType === 'image') { this._updateSnapshot('', 'image'); throw new Error('Clipboard contains image data, not text. Use readImage() instead.') }
if (winType === 'empty' || winType === null) { this._updateSnapshot('', 'empty'); return '' }
}
// Re-throw other errors on final attempt
if (attempt === 2) throw error
}
if (attempt < 2) await new Promise(resolve => setTimeout(resolve, 15))
}
// Return empty string if still empty to preserve existing semantics for empty clipboard
this._updateSnapshot(finalContent || '', (finalContent && finalContent.trim()) ? 'text' : 'empty')
return finalContent || ''
} catch (error) {
// In headless environments, return empty string instead of throwing
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return ''
}
throw new Error(`Failed to read text from clipboard: ${error.message}`)
}
}
async writeText (content) {
// In headless environments, simulate successful write
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return true
}
try {
const clipboardy = await getClipboardy()
const t0 = phaseEnabled() ? performance.now() : 0
await clipboardy.write(content)
if (phaseEnabled()) _recordPhase('clipboardy.write', performance.now() - t0)
this._invalidateSnapshot()
return true
} catch (error) {
// In headless environments, simulate successful write instead of throwing
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return true
}
throw new Error(`Failed to write text to clipboard: ${error.message}`)
}
}
async clear () {
// In headless environments, simulate successful clear
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return true
}
try {
const clipboardy = await getClipboardy()
const t0 = phaseEnabled() ? performance.now() : 0
await clipboardy.write('')
if (phaseEnabled()) _recordPhase('clipboardy.write', performance.now() - t0)
this._invalidateSnapshot()
return true
} catch (error) {
// In headless environments, simulate successful clear instead of throwing
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return true
}
throw new Error(`Failed to clear clipboard: ${error.message}`)
}
}
// macOS-specific method to write image to clipboard using AppleScript
async writeMacImage (imagePath) {
if (process.platform !== 'darwin') return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
const osascript = spawn('osascript', [
'-e',
`try
set imageFile to POSIX file "${imagePath}"
set the clipboard to (read imageFile as picture)
return "success"
on error
return "error"
end try`
], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
let hasErrored = false
osascript.stdout.on('data', (data) => {
output += data.toString()
})
osascript.stderr.on('data', () => {
hasErrored = true
})
osascript.on('close', (code) => {
clearTimeout(timeout)
if (hasErrored || code !== 0 || !output.includes('success')) {
if (phaseEnabled()) _recordPhase('mac.writeImage.fail', performance.now() - outerStart)
resolve(false)
} else {
if (phaseEnabled()) _recordPhase('mac.writeImage', performance.now() - outerStart)
resolve(true)
}
})
// Timeout after 5 seconds
const timeout = setTimeout(() => {
osascript.kill()
if (phaseEnabled()) _recordPhase('mac.writeImage.timeout', performance.now() - outerStart)
resolve(false)
}, 5000)
})
}
// Windows-specific method to write image to clipboard using PowerShell
async writeWindowsImage (imagePath) {
if (!this.isWindows) return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
const tempScript = path.join(os.tmpdir(), `clipaste-write-image-${Date.now()}.ps1`)
// Escape the path for PowerShell
const escapedPath = imagePath.replace(/\\/g, '\\\\')
const scriptContent = `Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
try {
$image = [System.Drawing.Image]::FromFile('${escapedPath}')
[System.Windows.Forms.Clipboard]::SetImage($image)
$image.Dispose()
Write-Output "success"
} catch {
Write-Output "error: $($_.Exception.Message)"
}`
try {
fs.writeFileSync(tempScript, scriptContent)
} catch (error) {
resolve(false)
return
}
const ps = spawn('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-File',
tempScript
], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
let hasErrored = false
ps.stdout.on('data', (data) => {
output += data.toString()
})
ps.stderr.on('data', () => {
hasErrored = true
})
ps.on('close', (code) => {
clearTimeout(timeout)
// Clean up temp script file
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
} catch {}
if (hasErrored || code !== 0 || !output.includes('success')) {
if (phaseEnabled()) _recordPhase('windows.writeImage.fail', performance.now() - outerStart)
resolve(false)
} else {
if (phaseEnabled()) _recordPhase('windows.writeImage', performance.now() - outerStart)
resolve(true)
}
})
// Timeout after 10 seconds
const timeout = setTimeout(() => {
ps.kill()
// Clean up temp script file
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
} catch {}
if (phaseEnabled()) _recordPhase('windows.writeImage.timeout', performance.now() - outerStart)
resolve(false)
}, 10000)
})
}
async writeImage (imagePath) {
// In headless environments, simulate successful write
if (isHeadlessEnvironment(!_injectedClipboardy)) {
return true
}
try {
// Verify the file exists
if (!fs.existsSync(imagePath)) {
throw new Error(`Image file not found: ${imagePath}`)
}
// Platform-specific implementation
if (process.platform === 'darwin') {
const ok = await this.writeMacImage(imagePath)
if (ok) this._invalidateSnapshot()
return ok
} else if (this.isWindows) {
const ok = await this.writeWindowsImage(imagePath)
if (ok) this._invalidateSnapshot()
return ok
} else {
// TODO: Implement Linux image writing
throw new Error('Linux image-to-clipboard functionality not yet implemented')
}
} catch (error) {
throw new Error(`Failed to write image to clipboard: ${error.message}`)
}
}
// Windows-specific method to read image from clipboard using PowerShell
async readWindowsImage () {
if (!this.isWindows) return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
const tempPath = path.join(os.tmpdir(), `clipaste-temp-${Date.now()}.png`)
const tempScript = path.join(os.tmpdir(), `clipaste-image-${Date.now()}.ps1`)
const scriptContent = `Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$clipboard = [System.Windows.Forms.Clipboard]::GetDataObject()
if ($clipboard.GetDataPresent([System.Windows.Forms.DataFormats]::Bitmap)) {
$bitmap = $clipboard.GetData([System.Windows.Forms.DataFormats]::Bitmap)
$bitmap.Save('${tempPath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
Write-Output "success"
} else {
Write-Output "no-image"
}`
try {
fs.writeFileSync(tempScript, scriptContent)
} catch (error) {
resolve(null)
return
}
const ps = spawn('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-File',
tempScript
], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
let hasErrored = false
ps.stdout.on('data', (data) => {
output += data.toString()
})
ps.stderr.on('data', () => {
hasErrored = true
})
ps.on('close', async (code) => {
clearTimeout(timeout)
// Clean up temp script file
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
} catch (e) { /* ignore */ }
if (hasErrored || code !== 0 || !output.includes('success')) {
// Clean up temp image file if it exists
try {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
} catch (e) { /* ignore */ }
if (phaseEnabled()) _recordPhase('windows.readImage.fail', performance.now() - outerStart)
resolve(null)
} else {
try {
// Read the saved image file
if (fs.existsSync(tempPath)) {
const buffer = fs.readFileSync(tempPath)
fs.unlinkSync(tempPath) // Clean up
if (phaseEnabled()) _recordPhase('windows.readImage', performance.now() - outerStart)
resolve({
format: 'png',
data: buffer
})
} else {
if (phaseEnabled()) _recordPhase('windows.readImage.missing', performance.now() - outerStart)
resolve(null)
}
} catch (error) {
if (phaseEnabled()) _recordPhase('windows.readImage.error', performance.now() - outerStart)
resolve(null)
}
}
})
// Timeout after 10 seconds
const timeout = setTimeout(() => {
ps.kill()
try {
if (fs.existsSync(tempScript)) {
fs.unlinkSync(tempScript)
}
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
} catch (e) { /* ignore */ }
if (phaseEnabled()) _recordPhase('windows.readImage.timeout', performance.now() - outerStart)
resolve(null)
}, 10000)
})
}
// macOS-specific method to read image from clipboard using AppleScript
async readMacImage () {
if (process.platform !== 'darwin') return null
const outerStart = phaseEnabled() ? performance.now() : 0
return new Promise((resolve) => {
const tempPath = path.join(os.tmpdir(), `clipaste-temp-${Date.now()}.img`)
const escapedPath = tempPath.replace(/"/g, '\\"')
const appleScript = `set tempPath to POSIX file "${escapedPath}"
set fileRef to missing value
set imageData to missing value
set formatLabel to ""
set formatPairs to {{"png", \u00ABclass PNGf\u00BB}, {"jpeg", \u00ABclass JPEG\u00BB}, {"gif", \u00ABclass GIFf\u00BB}, {"tiff", \u00ABclass TIFF\u00BB}, {"bmp", \u00ABclass BMPf\u00BB}}
repeat with pairItem in formatPairs
set currentLabel to item 1 of pairItem
set currentClass to item 2 of pairItem
try
set imageData to the clipboard as currentClass
set formatLabel to currentLabel
exit repeat
on error
set imageData to missing value
end try
end repeat
if imageData is missing value then
return "no-image"
end if
try
set fileRef to open for access tempPath with write permission
set eof of fileRef to 0
write imageData to fileRef
close access fileRef
return "success:" & formatLabel
on error errMsg number errNum
try
if fileRef is not missing value then close access fileRef
end try
return "error:" & errMsg
end try`
const osascript = spawn('osascript', [
'-e',
appleScript
], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
let hasErrored = false
osascript.stdout.on('data', (data) => {
output += data.toString()
})
osascript.stderr.on('data', () => {
hasErrored = true
})
osascript.on('close', (code) => {
clearTimeout(timeout)
const cleanupTempFile = () => {
try {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
} catch (e) { /* ignore */ }
}
const trimmedOutput = output.trim()
if (hasErrored || code !== 0 || !trimmedOutput || trimmedOutput.startsWith('error')) {
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage.fail', performance.now() - outerStart)
resolve(null)
return
}
if (trimmedOutput === 'no-image') {
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage.noimage', performance.now() - outerStart)
resolve(null)
return
}
if (!trimmedOutput.startsWith('success:')) {
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage.unexpected', performance.now() - outerStart)
resolve(null)
return
}
const format = trimmedOutput.split(':')[1] || 'png'
try {
if (fs.existsSync(tempPath)) {
const buffer = fs.readFileSync(tempPath)
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage', performance.now() - outerStart)
resolve({
format,
data: buffer
})
return
}
} catch (error) {
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage.error', performance.now() - outerStart)
resolve(null)
return
}
cleanupTempFile()
if (phaseEnabled()) _recordPhase('mac.readImage.missing', performance.now() - outerStart)
resolve(null)
})
// Timeout after 10 seconds (cleared on close)
const timeout = setTimeout(() => {
osascript.kill()
try {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
} catch (e) { /* ignore */ }
if (phaseEnabled()) _recordPhase('mac.readImage.timeout', performance.now() - outerStart)
resolve(null)
}, 10000)
})
}
async readImage () {
try {
const clipboardy = await getClipboardy()
let content
try {
const t0 = phaseEnabled() ? performance.now() : 0
content = await clipboardy.read()
if (phaseEnabled()) _recordPhase('clipboardy.read', performance.now() - t0)
} catch (error) {
// On Windows, if clipboardy fails but we know there's an image, try Windows method
if (this.isWindows && (error.message.includes('Element not found') || error.message.includes('Elementtiä ei löydy'))) {
const winImage = await this.readWindowsImage()
if (winImage) return winImage
}
throw error
}
// Check if content looks like base64 image data
if (this.isBase64Image(content)) {
return this.parseBase64Image(content)
}
// On Windows, if we didn't get base64 image data, try the PowerShell approach
if (this.isWindows) {
const winType = await this.checkWindowsClipboard()
if (winType === 'image') {
const winImage = await this.readWindowsImage()
if (winImage) return winImage
}
}
// On macOS, if we didn't get base64 image data, try the AppleScript approach
if (process.platform === 'darwin') {
const macType = await this.checkMacClipboard()
if (macType === 'image') {
const macImage = await this.readMacImage()
if (macImage) return macImage
}
}
// For now, we'll focus on text content
// Image clipboard support varies by platform and would need native bindings
return null
} catch (error) {
throw new Error(`Failed to read image from clipboard: ${error.message}`)
}
}
isBase64Image (content) {
if (!content || typeof content !== 'string') return false
// Check for data URL format (trim whitespace first)
const dataUrlRegex = /^data:image\/(png|jpeg|jpg|gif|bmp|webp|svg);base64,/i
return dataUrlRegex.test(content.trim())
}
parseBase64Image (content) {
const match = content.trim().match(/^data:image\/(\w+);base64,(.+)$/)
if (!match) return null
try {
// Validate base64 data
const base64Data = match[2]
// Basic base64 validation - should only contain valid base64 characters
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
return null
}
const buffer = Buffer.from(base64Data, 'base64')
// Verify the buffer is not empty and has reasonable size
if (buffer.length === 0) {
return null
}
return {
format: match[1],
data: buffer
}
} catch (error) {
// Invalid base64 data
return null
}
}
async getContentType () {
try {
// In test mode skip snapshot fast-path to avoid stale mocked sequence expectations
if (this._snapshotValid() && !this._testMode()) return this._snapshot.type
const clipboardy = await getClipboardy()
let content
try {
const t0 = phaseEnabled() ? performance.now() : 0
content = await clipboardy.read()
if (phaseEnabled()) _recordPhase('clipboardy.read', performance.now() - t0)
} catch (error) {
// On Windows, if clipboardy fails with various clipboard errors, check via PowerShell
if (this.isWindows && (
error.message.includes('Element not found') ||
error.message.includes('Elementtiä ei löydy') ||
error.message.includes('Could not paste from clipboard') ||
error.message.includes('thread \'main\' panicked')
)) {
const winType = await this.checkWindowsClipboard()
if (winType === 'image') return 'image'
if (winType === 'text') return 'text'
if (winType === 'empty') return 'empty'
}
throw error
}
if (!content || content.trim().length === 0) {
// On macOS, if clipboardy returned empty, check if there's image content
if (process.platform === 'darwin') {
const macType = await this.checkMacClipboard()
if (macType === 'image') return 'image'
if (macType === 'text') return 'text'
if (macType === 'empty') return 'empty'
}
this._updateSnapshot(content || '', 'empty')
return 'empty'
}
if (this.isBase64Image(content)) {
this._updateSnapshot(content, 'image')
return 'image'
}
// Check if content looks like binary data
if (this.isBinaryData(content)) {
this._updateSnapshot(content, 'binary')
return 'binary'
}
this._updateSnapshot(content, 'text')
return 'text'
} catch (error) {
throw new Error(`Failed to determine clipboard content type: ${error.message}`)
}
}
isBinaryData (content) {
if (typeof content !== 'string') return false
// Simple heuristic: count null bytes and non-printable characters via char codes (avoids control chars in regex)
let nullBytes = 0
let nonPrintable = 0
for (let i = 0; i < content.length; i++) {
const code = content.charCodeAt(i)
if (code === 0x00) nullBytes++
if (
(code >= 0x00 && code <= 0x08) ||
code === 0x0B ||
code === 0x0C ||
(code >= 0x0E && code <= 0x1F) ||
(code >= 0x7F && code <= 0x9F)
) {
nonPrintable++
}
}
return nullBytes > content.length * 0.1 || nonPrintable > content.length * 0.3
}
}
module.exports = ClipboardManager
module.exports.__setMockClipboardy = (mock) => { _injectedClipboardy = mock }
module.exports.getPhaseStats = (reset = false) => {
if (!phaseEnabled()) return {}
const out = {}
for (const [k, v] of Object.entries(_phaseStats)) {
out[k] = {
count: v.count,
totalMs: +v.total.toFixed(3),
avgMs: +((v.total / v.count) || 0).toFixed(3)
}
}
if (reset) {
for (const k of Object.keys(_phaseStats)) delete _phaseStats[k]
}
return out
}
module.exports.enablePhaseProfiling = enablePhaseProfiling
module.exports.disablePhaseProfiling = disablePhaseProfiling