open-windows-file-dialog
Version:
Programmatically open a file dialog window (explorer) for picking files. Only works on Windows. Also see: open-finder-dialog, open-linux-file-dialog, and open-file-manager-dialog for other platforms.
1 lines • 9.56 kB
Source Map (JSON)
{"version":3,"sources":["../index.ts"],"sourcesContent":["import cp from 'node:child_process';\nimport path from 'node:path';\nimport util from 'node:util';\n\nconst execAsync = util.promisify(cp.exec);\n\ninterface FileDialogOptions {\n multiple?: boolean;\n checkFileExists?: boolean;\n filter?: string;\n title?: string;\n defaultExtension?: string;\n}\n\ninterface FileDialogResult {\n files: string[];\n canceled: boolean;\n}\n\nclass FileDialogError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'FileDialogError';\n }\n}\n\nconst isCanceled = (v: string): boolean => /cancell?ed/i.test(v);\n\n// Escape special characters for PowerShell\nconst escapeForPowerShell = (str: string): string => {\n return str.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '`\"').replace(/\\$/g, '`$');\n};\n\nconst buildFilterString = (filter: string): string => {\n // Handle common filter formats\n if (filter === '*' || filter === '*.*') {\n return 'All files (*.*)|*.*';\n }\n\n // If it's already in the correct format, return as is\n if (filter.includes('|')) {\n return filter;\n }\n\n // Convert simple patterns to proper format\n // e.g., \"*.txt\" becomes \"Text files (*.txt)|*.txt\"\n const ext = filter.replace('*', '').replace('.', '');\n if (ext) {\n return `${ext.toUpperCase()} files (${filter})|${filter}`;\n }\n\n return 'All files (*.*)|*.*';\n};\n\nconst checkPowerShellAvailable = async (): Promise<boolean> => {\n try {\n const { stdout } = await execAsync('powershell.exe -Command \"echo test\"', {\n timeout: 5000\n });\n return stdout.trim() === 'test';\n } catch {\n return false;\n }\n};\n\nexport const openWindowsFileDialog = async (\n filepath: string = process.cwd(),\n options: FileDialogOptions = {}\n): Promise<FileDialogResult> => {\n if (typeof filepath !== 'string') {\n throw new FileDialogError('Filepath must be a string');\n }\n\n const normalizedPath = path.resolve(filepath);\n const isPowerShellAvailable = await checkPowerShellAvailable();\n if (!isPowerShellAvailable) {\n throw new FileDialogError('PowerShell is not available on this system');\n }\n\n const opts: Required<FileDialogOptions> = {\n multiple: true,\n checkFileExists: true,\n filter: '*.*',\n title: 'Select File(s)',\n defaultExtension: '',\n maxTimeout: 5 * 60 * 1000, // 5 minutes\n ...options\n };\n\n const escapedPath = escapeForPowerShell(normalizedPath);\n const escapedTitle = escapeForPowerShell(opts.title);\n const filterString = buildFilterString(opts.filter);\n const pathSeparator = '|<<PATH_SEPARATOR>>|';\n\n const powershellScript = `\n try {\n Add-Type -AssemblyName System.Windows.Forms\n [System.Windows.Forms.Application]::EnableVisualStyles()\n\n $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog\n $OpenFileDialog.Multiselect = ${opts.multiple ? '$true' : '$false'}\n $OpenFileDialog.InitialDirectory = \"${escapedPath}\"\n $OpenFileDialog.Filter = \"${escapeForPowerShell(filterString)}\"\n $OpenFileDialog.CheckFileExists = ${opts.checkFileExists ? '$true' : '$false'}\n $OpenFileDialog.CheckPathExists = $true\n $OpenFileDialog.SupportMultiDottedExtensions = $true\n $OpenFileDialog.Title = \"${escapedTitle}\"\n $OpenFileDialog.RestoreDirectory = $true\n ${opts.defaultExtension ? `$OpenFileDialog.DefaultExt = \"${escapeForPowerShell(opts.defaultExtension)}\"` : ''}\n\n # Ensure dialog appears on top\n $OpenFileDialog.ShowHelp = $false\n\n $DialogResult = $OpenFileDialog.ShowDialog()\n\n if ($DialogResult -eq [System.Windows.Forms.DialogResult]::OK) {\n foreach ($FileName in $OpenFileDialog.FileNames) {\n Write-Host \"$FileName${pathSeparator}\"\n }\n } else {\n Write-Host \"CANCELLED\"\n }\n } catch {\n Write-Error $_.Exception.Message\n exit 1\n }\n `;\n\n return new Promise((resolve, reject) => {\n const child = cp.spawn(\n 'powershell.exe',\n ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-Command', '-'],\n {\n windowsHide: true,\n shell: false\n }\n );\n\n let stdoutData = '';\n let stderrData = '';\n let hasResolved = false;\n\n child.stdout.on('data', data => {\n stdoutData += data.toString();\n });\n\n child.stderr.on('data', data => {\n stderrData += data.toString();\n });\n\n child.on('error', error => {\n if (!hasResolved) {\n hasResolved = true;\n reject(new FileDialogError(`Failed to spawn PowerShell: ${error.message}`));\n }\n });\n\n child.on('close', code => {\n if (hasResolved) return;\n hasResolved = true;\n\n if (code !== 0 && stderrData) {\n const errorMessage = stderrData.trim();\n reject(new FileDialogError(`PowerShell error: ${errorMessage}`));\n return;\n }\n\n const output = stdoutData.trim();\n\n if (isCanceled(output)) {\n resolve({ files: [], canceled: true });\n return;\n }\n\n const files = output\n .split(pathSeparator)\n .map(s => s.trim())\n .filter(s => s !== '');\n\n const validFiles = files.filter(f => path.isAbsolute(f));\n if (validFiles.length !== files.length) {\n console.warn('Some file paths were not absolute and were filtered out');\n }\n\n resolve({ files: validFiles, canceled: false });\n });\n\n if (opts.maxTimeout) {\n // If maxTimeout is set, set timeout to kill the process if it takes too long\n const timeout = setTimeout(() => {\n if (!hasResolved) {\n hasResolved = true;\n child.kill();\n reject(new FileDialogError('Dialog timeout after 5 minutes'));\n }\n }, opts.maxTimeout);\n\n child.on('exit', () => {\n clearTimeout(timeout);\n });\n }\n\n child.stdin.write(powershellScript);\n child.stdin.end();\n });\n};\n\nexport const openWindowsFileDialogSync = async (filepath?: string, options?: FileDialogOptions): Promise<string[]> => {\n const result = await openWindowsFileDialog(filepath, options);\n return result.files;\n};\n\nexport default openWindowsFileDialog;\n"],"mappings":"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","names":[]}