@@ -218,7 +218,7 @@ function hasImageWindows(): boolean {
218218 Add-Type -AssemblyName System.Windows.Forms
219219 if ([System.Windows.Forms.Clipboard]::ContainsImage()) { Write-Output "true" } else { Write-Output "false" }
220220 `
221- const result = spawnSync ( 'powershell' , [ '-Command' , script ] , {
221+ const result = spawnSync ( 'powershell' , [ '-STA' , '- Command', script ] , {
222222 encoding : 'utf-8' ,
223223 timeout : 5000 ,
224224 } )
@@ -249,7 +249,7 @@ function readImageWindows(): ClipboardImageResult {
249249 }
250250 `
251251
252- const result = spawnSync ( 'powershell' , [ '-Command' , script ] , {
252+ const result = spawnSync ( 'powershell' , [ '-STA' , '- Command', script ] , {
253253 encoding : 'utf-8' ,
254254 timeout : 10000 ,
255255 } )
@@ -353,6 +353,165 @@ export function getImageFilePathFromText(text: string, cwd: string): string | nu
353353 }
354354}
355355
356+ /**
357+ * Read file URL/path from clipboard when a file has been copied (e.g., from Finder).
358+ * Returns the POSIX path if a file URL is found, null otherwise.
359+ *
360+ * When you copy a file in Finder (Cmd+C), the clipboard contains a file reference,
361+ * not plain text. pbpaste won't return the path, but we can use AppleScript to
362+ * extract it.
363+ */
364+ function readClipboardFilePathMacOS ( ) : string | null {
365+ try {
366+ // First check if clipboard contains a file URL
367+ const infoResult = spawnSync ( 'osascript' , [
368+ '-e' ,
369+ 'clipboard info' ,
370+ ] , { encoding : 'utf-8' , timeout : 1000 } )
371+
372+ if ( infoResult . status !== 0 ) return null
373+
374+ const info = infoResult . stdout || ''
375+ // Check for file URL type in clipboard (furl = file URL)
376+ if ( ! info . includes ( '«class furl»' ) && ! info . includes ( 'public.file-url' ) ) {
377+ return null
378+ }
379+
380+ // Extract the file path using AppleScript
381+ const script = `
382+ try
383+ set theFile to the clipboard as «class furl»
384+ return POSIX path of theFile
385+ on error
386+ return ""
387+ end try
388+ `
389+
390+ const result = spawnSync ( 'osascript' , [ '-e' , script ] , {
391+ encoding : 'utf-8' ,
392+ timeout : 1000 ,
393+ } )
394+
395+ if ( result . status === 0 && result . stdout ) {
396+ const filePath = result . stdout . trim ( )
397+ if ( filePath && existsSync ( filePath ) ) {
398+ return filePath
399+ }
400+ }
401+ return null
402+ } catch {
403+ return null
404+ }
405+ }
406+
407+ /**
408+ * Read file path from clipboard when a file has been copied (Windows).
409+ * Returns the file path if found, null otherwise.
410+ */
411+ function readClipboardFilePathWindows ( ) : string | null {
412+ try {
413+ const script = `
414+ Add-Type -AssemblyName System.Windows.Forms
415+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
416+ if ($files.Count -gt 0) {
417+ Write-Output $files[0]
418+ }
419+ `
420+ const result = spawnSync ( 'powershell' , [ '-STA' , '-Command' , script ] , {
421+ encoding : 'utf-8' ,
422+ timeout : 1000 ,
423+ } )
424+
425+ if ( result . status === 0 && result . stdout ) {
426+ const filePath = result . stdout . trim ( )
427+ if ( filePath && existsSync ( filePath ) ) {
428+ return filePath
429+ }
430+ }
431+ return null
432+ } catch {
433+ return null
434+ }
435+ }
436+
437+ /**
438+ * Read file path from clipboard when a file has been copied (Linux).
439+ * Returns the file path if found, null otherwise.
440+ */
441+ function readClipboardFilePathLinux ( ) : string | null {
442+ try {
443+ // Try to get file URI from clipboard
444+ let result = spawnSync ( 'xclip' , [
445+ '-selection' , 'clipboard' ,
446+ '-t' , 'text/uri-list' ,
447+ '-o' ,
448+ ] , { encoding : 'utf-8' , timeout : 1000 } )
449+
450+ if ( result . status !== 0 ) {
451+ // Try wl-paste for Wayland
452+ result = spawnSync ( 'wl-paste' , [ '--type' , 'text/uri-list' ] , {
453+ encoding : 'utf-8' ,
454+ timeout : 1000 ,
455+ } )
456+ }
457+
458+ if ( result . status === 0 && result . stdout ) {
459+ const output = result . stdout . trim ( )
460+ // Parse file:// URLs
461+ const lines = output . split ( '\n' )
462+ for ( const line of lines ) {
463+ const trimmed = line . trim ( )
464+ if ( trimmed . startsWith ( 'file://' ) ) {
465+ const filePath = decodeURIComponent ( trimmed . slice ( 7 ) )
466+ if ( existsSync ( filePath ) ) {
467+ return filePath
468+ }
469+ }
470+ }
471+ }
472+ return null
473+ } catch {
474+ return null
475+ }
476+ }
477+
478+ /**
479+ * Read file path from clipboard when a file has been copied.
480+ * This handles the case where a user copies a file in their file manager.
481+ * Returns the file path if found, null otherwise.
482+ *
483+ * Note: This returns ANY file path, not just images. Callers should check
484+ * if the file is an image using isImageFile() if needed.
485+ */
486+ export function readClipboardFilePath ( ) : string | null {
487+ const platform = process . platform
488+
489+ switch ( platform ) {
490+ case 'darwin' :
491+ return readClipboardFilePathMacOS ( )
492+ case 'win32' :
493+ return readClipboardFilePathWindows ( )
494+ case 'linux' :
495+ return readClipboardFilePathLinux ( )
496+ default :
497+ return null
498+ }
499+ }
500+
501+ /**
502+ * Read image file path from clipboard when an image file has been copied.
503+ * This is a convenience wrapper that combines readClipboardFilePath() with
504+ * an image file check.
505+ * Returns the file path if it's an image file, null otherwise.
506+ */
507+ export function readClipboardImageFilePath ( ) : string | null {
508+ const filePath = readClipboardFilePath ( )
509+ if ( filePath && isImageFile ( filePath ) ) {
510+ return filePath
511+ }
512+ return null
513+ }
514+
356515/**
357516 * Read text from clipboard. Returns null if reading fails.
358517 */
0 commit comments