open-finder-dialog
Version:
Open a finder dialog window (finder prompt) programmatically. Only works on MacOS.
1 lines • 5.69 kB
Source Map (JSON)
{"version":3,"sources":["../index.ts"],"sourcesContent":["import cp from 'node:child_process';\nimport { detectTerminal } from 'detect-terminal';\n\nconst escapeQuotedPath = (input: string): string => {\n return input.replace(/(?<!\\\\(?<!\\\\))\"/g, '\\\\$&');\n};\n\nconst isCanceled = (v: string): boolean => /cancell?ed/i.test(v);\n\ninterface OpenDialogOptions {\n filters?: string[];\n limit?: number;\n terminal?: string;\n}\n\n/**\n * Builds the AppleScript 'choose file' command.\n * - allowMultiple: when limit is greater than 1, this will be true.\n * - filters: file type filters (e.g. {\"public.jpeg\", \"public.png\"}).\n */\n\nconst buildCommand = (\n allowMultiple: boolean,\n filters: string[]\n): string => {\n const base = 'choose file';\n const withPrompt = ' with prompt \"Select files to open\"';\n const ofType = filters.length > 0\n ? ` of type {${filters.map(f => `\"${f}\"`).join(', ')}}`\n : '';\n const multi = allowMultiple ? ' with multiple selections allowed' : '';\n return base + withPrompt + ofType + multi;\n};\n\n/**\n * Opens a Finder dialog to select files.\n * @param initialDirectory - Directory dialog opens to.\n * @param options - Dialog config options.\n */\n\nexport const openFinderDialog = async (\n initialDirectory: string = process.cwd(),\n options: OpenDialogOptions = {}\n): Promise<{ files: string[]; canceled: boolean }> => {\n return new Promise((resolve, reject) => {\n const { filters = [], terminal = detectTerminal(), limit = 100 } = options;\n\n const maxFiles = Math.max(1, limit);\n const command = buildCommand(limit > 1, filters);\n const escapedPath = escapeQuotedPath(initialDirectory);\n const pathSeparator = '<__PATH_SEPARATOR__>';\n\n const appleScript = `\n set defaultPath to POSIX file \"${escapedPath}\"\n set limit to ${maxFiles}\n\n try\n -- Open the file dialog within SystemUIServer to bring it to focus\n tell application \"SystemUIServer\"\n activate\n delay 0.2 -- Allow SystemUIServer to activate\n set file_list to ${command} default location defaultPath\n if limit = 1 then\n set file_list to {file_list}\n end if\n end tell\n\n -- Apply the maximum-files rule\n if limit > 0 and (count of file_list) > limit then\n set file_list to items 1 thru limit of file_list\n end if\n\n -- Process the selected files\n set posixPaths to {}\n\n repeat with aFile in file_list\n set end of posixPaths to POSIX path of aFile & \"${pathSeparator}\"\n end repeat\n\n -- Return focus to ${terminal}\n tell application \"${terminal}\"\n activate\n end tell\n\n set text item delimiters to \"\"\n set posixPathsString to posixPaths as string\n\n return posixPathsString\n on error\n -- Return focus to terminal app even if the dialog is canceled\n tell application \"${terminal}\"\n activate\n end tell\n return \"CANCELLED\"\n end try\n `;\n\n const child = cp.spawn('osascript', ['-']);\n\n let stdoutData = '';\n let stderrData = '';\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('close', code => {\n if (code !== 0 && stderrData) {\n reject(new Error(stderrData.trim()));\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 resolve({ files, canceled: false });\n });\n\n // Write the AppleScript code to stdin\n child.stdin.write(appleScript);\n child.stdin.end();\n });\n};\n\nexport default openFinderDialog;\n"],"mappings":";;;;AAAA,OAAO,QAAQ;AACf,SAAS,sBAAsB;AAE/B,IAAM,mBAAmB,wBAAC,UAA0B;AAClD,SAAO,MAAM,QAAQ,oBAAoB,MAAM;AACjD,GAFyB;AAIzB,IAAM,aAAa,wBAAC,MAAuB,cAAc,KAAK,CAAC,GAA5C;AAcnB,IAAM,eAAe,wBACnB,eACA,YACW;AACX,QAAM,OAAO;AACb,QAAM,aAAa;AACnB,QAAM,SAAS,QAAQ,SAAS,IAC5B,aAAa,QAAQ,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,MAClD;AACJ,QAAM,QAAQ,gBAAgB,sCAAsC;AACpE,SAAO,OAAO,aAAa,SAAS;AACtC,GAXqB;AAmBd,IAAM,mBAAmB,8BAC9B,mBAA2B,QAAQ,IAAI,GACvC,UAA6B,CAAC,MACsB;AACpD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,EAAE,UAAU,CAAC,GAAG,WAAW,eAAe,GAAG,QAAQ,IAAI,IAAI;AAEnE,UAAM,WAAW,KAAK,IAAI,GAAG,KAAK;AAClC,UAAM,UAAU,aAAa,QAAQ,GAAG,OAAO;AAC/C,UAAM,cAAc,iBAAiB,gBAAgB;AACrD,UAAM,gBAAgB;AAEtB,UAAM,cAAc;AAAA,uCACe,WAAW;AAAA,qBAC7B,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAOA,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4DAewB,aAAa;AAAA;AAAA;AAAA,6BAG5C,QAAQ;AAAA,4BACT,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAUR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAOhC,UAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC;AAEzC,QAAI,aAAa;AACjB,QAAI,aAAa;AAEjB,UAAM,OAAO,GAAG,QAAQ,UAAQ;AAC9B,oBAAc,KAAK,SAAS;AAAA,IAC9B,CAAC;AAED,UAAM,OAAO,GAAG,QAAQ,UAAQ;AAC9B,oBAAc,KAAK,SAAS;AAAA,IAC9B,CAAC;AAED,UAAM,GAAG,SAAS,UAAQ;AACxB,UAAI,SAAS,KAAK,YAAY;AAC5B,eAAO,IAAI,MAAM,WAAW,KAAK,CAAC,CAAC;AACnC;AAAA,MACF;AAEA,YAAM,SAAS,WAAW,KAAK;AAE/B,UAAI,WAAW,MAAM,GAAG;AACtB,gBAAQ,EAAE,OAAO,CAAC,GAAG,UAAU,KAAK,CAAC;AACrC;AAAA,MACF;AAEA,YAAM,QAAQ,OACX,MAAM,aAAa,EACnB,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAK,MAAM,EAAE;AAEvB,cAAQ,EAAE,OAAO,UAAU,MAAM,CAAC;AAAA,IACpC,CAAC;AAGD,UAAM,MAAM,MAAM,WAAW;AAC7B,UAAM,MAAM,IAAI;AAAA,EAClB,CAAC;AACH,GA/FgC;AAiGhC,IAAO,6BAAQ;","names":[]}