Interaction Select From List - Dialog Focus
bold textHi all,
I've found a way to Focus the Select From List Interaction Dialog, it works quite well for me so I hope it'll be the same for other users.
Here are the steps to set it up and use it on your system:
1 - Create a New "Run AppleScript" Quick Action in Apple's Automator
2 - Add this code in it:
tell application "System Events"
tell application process "osascript"
set frontmost to true
end tell
end tell
3 - Save it with a name to recall later, say "Dialog Focus" in this case
4 - Go to System Preferences/Keyboard/Keyboard Shortcuts/Services/General and find "Dialog Focus"
5 - Double click on the right where it says "none" and add a shortcut that won't create issues with your DAW say for me:
"ctrl+cmd+alt+shift+f19"
6 - Add this code to your codes with Select From List Dialogs:
function dialogFocus() {
sf.keyboard.press({ keys: "ctrl+cmd+alt+shift+f19" });
};
and call it before the List codes, say:
dialogFocus();
const selectFromList = sf.interaction.selectFromList({
title: `TITLE`,
prompt: ``,
items: ['item 1', 'item 2', 'item 3'],
allowEmptySelection: false,
allowMultipleSelections: true,
}).list
7 - Run it and give it permissions to the necessary apps and you'll be good to go
Enjoy!!
Linked from:
- Kitch Membery @Kitch2024-10-29 00:23:50.941Z
Ah yes, the current implementation under SoundFlow's hood does not focus the dialog window yet. I'll log a report internally but for now, rather than using this workaround try using this script I mocked up.
function selectFromList(settings) { // Get focused app const focusedApp = sf.ui.frontmostApp; const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton } = settings; const script = `set options to {"${items.join('", "')}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title}" ¬ with prompt "${prompt}" ¬ OK button name "${oKButton}" ¬ default items {"${defaultItems.join('", "')}"} ¬ cancel button name "${cancelButton}" ¬ empty selection allowed ${JSON.stringify(allowEmptySelection)} ¬ multiple selections allowed ${JSON.stringify(allowMultipleSelections)} end tell`; const result = sf.system.execAppleScript({ script }).result; // Refocus previous app. focusedApp.appActivate(); return result; } const selectedItems = selectFromList({ title: `Title`, prompt: `Please select items.`, items: ['item 1', 'item 2', 'item 3', 'item 4'], defaultItems: ['item 2', 'item 4'], allowEmptySelection: true, allowMultipleSelections: true, cancelButton: "Cancel", oKButton: "OK", }); log(selectedItems);
It's by no means a perfect solution, but it works without having to leave SoundFlow. It will also refocus the previous app after the selection is made.
- NNicolas Aparicio @Nicolas_Aparicio
OMG @Kitch, you are amazing!
I'll try this later tonight and let you know how I go. And absolutely, much better to keep it all inside SoundFlow, thanks legend - In reply toKitch⬆:NNicolas Aparicio @Nicolas_Aparicio
Hi @Kitch,
I've found a way to focus the Interactions (selectFromList, selectFolder, selectFile, etc.) from inside SoundFlow, or well, close enough at least.
Thanks to the legend @Chad, I've come up with this:function interactionFocus() { setTimeout(() => { sf.system.execAppleScript({ script: ` repeat tell application "System Events" tell application process "osascript" set frontmost to true end tell end tell delay 0.3 end repeat `, }); }, 200) } interactionFocus(); sf.interaction.selectFromList({ title: `TEST`, prompt: ``, items: ['item 1', 'item 2', 'item 3'], allowEmptySelection: false, allowMultipleSelections: true, }).list
The 200ms wait from the Timeout is for the selectFromList as it basically comes up in focus with it, and the delay 0.3, is for everything else as the Timeout won't catch them.
I'm sure it can be done much better but this one works for me everytime!.
Cheers,
Nic.- NNicolas Aparicio @Nicolas_Aparicio
Though I find that this code does it for everything regardless and I've noticed the repeat from the one above, can cause issues after the interaction has been closed.
function interactionFocus() { setTimeout(() => { sf.system.execAppleScript({ script: ` tell application "System Events" tell application process "osascript" set frontmost to true end tell end tell `, }); }, 500) };
🤙
Chad Wahlbrink @Chad2024-11-12 19:05:21.790Z
Excellent use of
setTimeout()
, @Nicolas_Aparicio!! 👏👏- NNicolas Aparicio @Nicolas_Aparicio
Thank you sir!
Chad Wahlbrink @Chad2024-11-13 18:13:08.402Z
Hard-coding the dialogs using Kitch's approach is still probably more robust than relying on
setTimeOut()
overall. However, I needed a version of this for a script of mine today, so I did some tweaking to make sure the repeat doesn't go off the rails and it always waits for the window to exist.This will only repeat until a window exists for the interaction. Then I'm using a try/catch/finally to clean up stray apple script processes if the script exits or is cancelled. You could put your whole script in the try block of the try/catch/finally if it made more sense that way!
function interactionFocus() { setTimeout(() => { sf.system.execAppleScript({ script: ` tell application "System Events" repeat until (exists window of application process "osascript") delay 0.2 end repeat tell application process "osascript" set frontmost to true end tell end tell `, implementation: "OSAScript", }); }, 200) }; //////////////////////////////////////////////////// try { interactionFocus(); // PUT YOUR INTERACTION HERE sf.interaction.selectApplication(); } catch (err) { throw err; } finally { // KILL STRAY PROCESSES sf.system.exec({ commandLine: `pkill -9 osascript` }) }
- NNicolas Aparicio @Nicolas_Aparicio
Hi @Chad.
Absolutely, Kitch's code does a much cleaner, tighter job. I'll eventually set those codes in my scripts when I have extra time to fix them all but yeah, for now I needed a quick/temporary fix as this code would work for all interactions, which are tons 😅
Ah right, try catch of course!, and the applescript is much better. I was trying this one but kept getting errors after the interaction window would close:
tell application "System Events" tell application process "osascript" set frontmost to true repeat until (exists window of application process "osascript") delay 0.2 end repeat end tell end tell
Thanks a lot Chad!
- In reply toNicolas_Aparicio⬆:
Chris Shaw @Chris_Shaw2024-11-12 19:32:07.072Z
This is very useful - thanks for posting!
- In reply toNicolas_Aparicio⬆:
Kitch Membery @Kitch2024-11-12 19:41:34.128Z
Thanks for sharing Nick.
Did my selectFromList script in the thread above work?
As cool as the setTimeout function is, (nice work on that) it would be more robust to create a script similar to the selectFromList function for the other interactions, to avoid timing issues.
If the script I shared works for you, let me know, and when I have a chance, I'll make a version that works for the others.
Rock on!
- NNicolas Aparicio @Nicolas_Aparicio
Hi @Kitch, ah sorry, I forgot to reply to that.
Yeah, that work awesome, quicker and cleaner though I noticed that the code will fail if the "defaultItems" are missing, as it fails to see line 23 "join". Also, it could be nice to default the "ok" and "cancel" buttons to these values if not specified?.
I'm just using the timeout atm as it was easier for me to add that line before all my interactions rather that changing them one by one but will use yours eventually. 🤘Thanks legend,
Nic.Kitch Membery @Kitch2024-11-12 20:54:49.581Z
Solid approach for now... And great feedback on my selectFromList function (Noted!).
It will be great to get these interactions fixed internally at some point, but we'll need to do a bunch of testing once we've got a robust solution. As you can imagine these are used in so many scripts. :-)
Rock on!
- NNicolas Aparicio @Nicolas_Aparicio
Thanks @Kitch, you legend.
And absolutely, if the interactions could have a fix like yours internally, it'll be a game changer!.
I'll keep testing yours on different situations and with different inputs to check it's behavior 🤙.Kitch Membery @Kitch2024-11-12 22:11:50.616Z
Here is an updated version where all the properties can be left undefined. (Adding here for reference)
function selectFromList(settings) { // Get focused app const focusedApp = sf.ui.frontmostApp; const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton } = settings; const script = `set options to {"${items !== undefined?items.join('", "'):""}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title !== undefined? title:""}" ¬ with prompt "${prompt !== undefined? prompt: ""}" ¬ OK button name "${oKButton ? oKButton : "OK"}" ¬ default items {"${defaultItems && defaultItems.length > 0 ? defaultItems.join('", "') : []}"} ¬ cancel button name "${cancelButton ? cancelButton : "Cancel"}" ¬ empty selection allowed ${allowEmptySelection !== undefined ? JSON.stringify(allowEmptySelection) : true} ¬ multiple selections allowed ${allowMultipleSelections !== undefined ? JSON.stringify(allowMultipleSelections) : true} end tell`; const result = sf.system.execAppleScript({ script }).result; // Refocus previous app. focusedApp.appActivate(); return result; } const selectedItems = selectFromList({ title: `Title`, prompt: `Please select items.`, items: ['item 1', 'item 2', 'item 3', 'item 4'], //defaultItems: ["item 2"], //allowEmptySelection: true, //allowMultipleSelections: true, //cancelButton: "Cancel", //oKButton: "OK", }); log(selectedItems);
- NNicolas Aparicio @Nicolas_Aparicio
Hi @Kitch, this one works as expected and fields can be left undefined! however, it seems to have a 3 second lag before the keyboard or mouse can be used to scroll down though the list is in focus. Also, I've had log() as undefined sometimes with processes like:
let files = sf.file.directoryGetFiles({ path: paths, isRecursive: true }).paths
or mapping items from a json like:
let fileRead = sf.file.readJson({ path: path }).json let items = fileRead.map(i => i.name)
Looks like the "const result" could be the issue?
Kitch Membery @Kitch2024-11-13 21:29:50.716Z
"this one works as expected and fields can be left undefined!" Great!
I'm not sure I follow the rest of what you're saying in your post. Can you maybe provide a screen recording of what you mean?
- NNicolas Aparicio @Nicolas_Aparicio
Hi @Kitch, just sent you an email with the video for this 🤙
Kitch Membery @Kitch2024-11-14 00:58:29.691Z
Hi Nick,
I'm not seeing a delay here. Out of interest, how many files and folders are being returned from the
sf.file.directoryGetFiles()
method?I'd say that is where the bottleneck is... Maybe try to filter out items you don't need before pushing them to the selectFromList dialog method or test with a folder that contains fewer items.
- NNicolas Aparicio @Nicolas_Aparicio
Hey Kitch,
False alarm, I'm a dork and had ".List" at the end of your function so... 😅 my bad, it logs fine.
The "lag" was happening as the line:const result = sf.system.execAppleScript({ script, implementation: 'OSAScript' }).result;
didn't have the "OSAScript" implementation, adding it solved it for me 🤙.
Loved this code Kitch, amazing work!I was thinking, these could be added all into an interactions function? something like:
function interactions() { function selectFromList(settings) { // Get focused app const focusedApp = sf.ui.frontmostApp; const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton } = settings; const script = `set options to {"${items !== undefined ? items.join('", "') : ""}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title !== undefined ? title : ""}" ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ OK button name "${oKButton ? oKButton : "OK"}" ¬ default items {"${defaultItems && defaultItems.length > 0 ? defaultItems.join('", "') : []}"} ¬ cancel button name "${cancelButton ? cancelButton : "Cancel"}" ¬ empty selection allowed ${allowEmptySelection !== undefined ? JSON.stringify(allowEmptySelection) : true} ¬ multiple selections allowed ${allowMultipleSelections !== undefined ? JSON.stringify(allowMultipleSelections) : true} end tell`; const result = sf.system.execAppleScript({ script, implementation: 'OSAScript' }).result; // Refocus previous app. focusedApp.appActivate(); return result; } function selectFolder() { //selectFolderInteraction } function selectFile() { //selectFileInteraction } //etc, etc return { selectFromList, selectFolder, selectFile, etc, etc } }
So we can call back the function similar to SF like:
const test = interactions().selectFromList({ title: `TEST`, prompt: ``, items: ['item 1', 'item 2', 'item 3'], allowEmptySelection: false, allowMultipleSelections: true, }); log(test)
- NNicolas Aparicio @Nicolas_Aparicio
@Kitch btw, I thought of saving you some time so I've added all the interaction's applescripts for you.
I'll keep working on this to try and combine them all into one function and make the code shorter.
Also started adding the properties for "selectFromList" so ctrl+space gives you the same values as SF does. I'm very new to that area so, not sure if that'll work.function interactions() { function main(script) { // Get focused app const focusedApp = sf.ui.frontmostApp; const result = sf.system.execAppleScript({ script, implementation: 'OSAScript' }).result; // Refocus previous app. focusedApp.appActivate(); return result; } function selectApplication(settings) { const { title, prompt, } = settings; const script = ` tell application "System Events" activate set theApp to choose application with prompt "${prompt}" with title "${title}" end tell` main(script) } function selectApplications(settings) { const { title, prompt, } = settings; const script = ` tell application "System Events" activate set theApp to choose application with prompt "${prompt}" with title "${title}" ¬ with multiple selections allowed end tell` main(script) } function selectFile(settings) { const { defaultLocation, prompt, allowedFileTypes, } = settings; const script = ` tell application "System Events" activate set directory to POSIX path of (choose file of type {"${allowedFileTypes[0]}"} with prompt "${prompt}" default location ("${defaultLocation}")) end tell` main(script) } function selectFiles(settings) { const { defaultLocation, prompt, allowedFileTypes, } = settings; const script = ` tell application "System Events" activate set directory to POSIX path of (choose file of type {"${allowedFileTypes[0]}"} with prompt "${prompt}" default location ("${defaultLocation}") ¬ with multiple selections allowed) end tell` main(script) } function selectFolder(settings) { const { defaultLocation, prompt, } = settings; const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder with prompt "${prompt}" default location ("${defaultLocation}")) end tell` main(script) } function selectFolders(settings) { const { defaultLocation, prompt, } = settings; const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder with prompt "${prompt}" default location ("${defaultLocation}") ¬ with multiple selections allowed) end tell` main(script) } /** * @param {{ * title: String, * prompt: String, * items: Array, * defaultItems: Array, * allowEmptySelection: Boolean, * allowMultipleSelections: Boolean, * cancelButton: String, * oKButton: String, * }} settings */ function selectFromList(settings) { const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton } = settings; const script = `set options to {"${items !== undefined ? items.join('", "') : ""}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title !== undefined ? title : ""}" ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ OK button name "${oKButton ? oKButton : "OK"}" ¬ default items {"${defaultItems && defaultItems.length > 0 ? defaultItems.join('", "') : []}"} ¬ cancel button name "${cancelButton ? cancelButton : "Cancel"}" ¬ empty selection allowed ${allowEmptySelection !== undefined ? JSON.stringify(allowEmptySelection) : true} ¬ multiple selections allowed ${allowMultipleSelections !== undefined ? JSON.stringify(allowMultipleSelections) : true} end tell`; main(script) } function selectSaveFileName(settings) { const { defaultLocation, prompt, } = settings; //missing default name const script = ` tell application "System Events" activate set theNewFilePath to choose file name with prompt "${prompt}" default location ("${defaultLocation}") end tell` main(script) } return { selectApplication, selectApplications, selectFile, selectFiles, selectFolder, selectFolders, selectFromList, selectSaveFileName } } interactions().selectFromList({ title: 'Test Title', prompt: 'Test Prompt', });
Chad Wahlbrink @Chad2024-11-14 16:02:28.672Z
Epic work, @Nicolas_Aparicio !
- In reply toNicolas_Aparicio⬆:
Kitch Membery @Kitch2024-11-14 18:03:20.926Z
Great stuff @Nicolas_Aparicio. :-)
- In reply toNicolas_Aparicio⬆:NNicolas Aparicio @Nicolas_Aparicio
Hi all, I've done my version of this in the meantime if anyone is interested.
•All interactions that currently don't come up in focus are added to this script.
•Not only they come up in focus but they work almost the same way as the ones in soundFlow. So, you can just replace sf.interaction for interactions() on your current scripts and it'll still work the same "famous last words"
(btw never as good as the already implemented interactions but they"ll do the job)
• I've added most of the importat stuff except for "onError" and "onCancel" which I'll try to add later on.
• control + spacebar also shows you the available options per interaction
• Interactions like selectFile, work with undefined settings like: interactions().selectFile()
• The interactions will return the same values, so you can still log them and retrieve the same info
How to:1 - Add function to a script
2 - Replace intended sf.interaction with interactions() and you should be good to go
Cheers team,
Nicfunction interactions() { /** * @param {Object} [settings] * @param {any} [settings.title] * @param {any} [settings.prompt] */ function selectApplication(settings) { if (settings === undefined) { var title = undefined var prompt = undefined } else { var { title, prompt, } = settings; }; const script = ` tell application "System Events" activate set theApp to choose application ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ with title "${title !== undefined ? title : ""}" end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const appName = appleScript.result const appPath = sf.system.execAppleScript({ script: `POSIX path of (path to application "${appName.trim()}")`, implementation: 'OSAScript' }).result.replace('\n', '') const path = appPath const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { path, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.title] * @param {any} [settings.prompt] */ function selectApplications(settings) { if (settings === undefined) { var title = undefined var prompt = undefined } else { var { title, prompt, } = settings; }; const script = ` tell application "System Events" activate set theApp to choose application ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ with title "${title !== undefined ? title : ""}" ¬ with multiple selections allowed end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const appNames = appleScript.result.split(',').map(i => i.trim().replace('\n', '')) const appPaths = [] appNames.forEach(app => { let appPath = sf.system.execAppleScript({ script: `POSIX path of (path to application "${app}")`, implementation: 'OSAScript' }).result.replace('\n', '') appPaths.push(appPath) }); const paths = appPaths const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { paths, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {any} [settings.allowedFileTypes] */ function selectFile(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined var allowedFileTypes = undefined } else { var { defaultLocation, prompt, allowedFileTypes, } = settings; }; const desktopPath = sf.system.execAppleScript({ script: `set theTargetFolder to (path to Desktop Folder) as string`, }).result.split(':').slice(1).join('/') const script = ` tell application "System Events" activate set directory to POSIX path of (choose file ¬ of type {"${allowedFileTypes !== undefined ? allowedFileTypes[0] : ""}"} ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath}")) end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const path = appleScript.result const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { path, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {any} [settings.allowedFileTypes] */ function selectFiles(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined var allowedFileTypes = undefined } else { var { defaultLocation, prompt, allowedFileTypes, } = settings; }; const desktopPath = sf.system.execAppleScript({ script: `set theTargetFolder to (path to Desktop Folder) as string`, }).result.split(':').slice(1).join('/') const script = ` tell application "System Events" activate set directory to POSIX path of (choose file ¬ of type {"${allowedFileTypes !== undefined ? allowedFileTypes[0] : ""}"} ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath}") ¬ with multiple selections allowed) end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const paths = appleScript.standardError.split(`alias `).slice(1).map(i => '/' + i.split('"')[1].split(':').slice(1).join('/')) const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { paths, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] */ function selectFolder(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined } else { var { defaultLocation, prompt, } = settings; }; const desktopPath = sf.system.execAppleScript({ script: `set theTargetFolder to (path to Desktop Folder) as string`, }).result.split(':').slice(1).join('/') const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath}")) end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const path = appleScript.result const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { path, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] */ function selectFolders(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined } else { var { defaultLocation, prompt, } = settings; }; const desktopPath = sf.system.execAppleScript({ script: `set theTargetFolder to (path to Desktop Folder) as string`, }).result.split(':').slice(1).join('/') const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath}") ¬ with multiple selections allowed) end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const paths = appleScript.standardError.split(`alias `).slice(1).map(i => '/' + i.split('"')[1].split(':').slice(1, -1).join('/')) const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { paths, success, error, userCancelled }; } /** * @param {Object} settings * @param {string} [settings.title] * @param {string} [settings.prompt] * @param {array} [settings.items] * @param {array} [settings.defaultItems] * @param {Boolean} [settings.allowEmptySelection] * @param {Boolean} [settings.allowMultipleSelections] * @param {string} [settings.oKButton] * @param {string} [settings.cancelButton] */ function selectFromList(settings) { const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton, } = settings; const script = `set options to {"${items !== undefined ? items.join('", "') : ""}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title !== undefined ? title : ""}" ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ OK button name "${oKButton ? oKButton : "OK"}" ¬ default items {"${defaultItems && defaultItems.length > 0 ? defaultItems.join('", "') : []}"} ¬ cancel button name "${cancelButton ? cancelButton : "Cancel"}" ¬ empty selection allowed ${allowEmptySelection !== undefined ? JSON.stringify(allowEmptySelection) : true} ¬ multiple selections allowed ${allowMultipleSelections !== undefined ? JSON.stringify(allowMultipleSelections) : true} end tell`; const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const list = appleScript.result.replace('\n', '').split(',').map(i => i.trim()) const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { list, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.defaultName] * @param {any} [settings.prompt] */ function selectSaveFileName(settings) { if (settings === undefined) { var defaultLocation = undefined var defaultName = undefined var prompt = undefined } else { var { defaultLocation, defaultName, prompt } = settings }; const desktopPath = sf.system.execAppleScript({ script: `set theTargetFolder to (path to Desktop Folder) as string`, }).result.split(':').slice(1).join('/') const script = ` tell application "System Events" activate set theNewFilePath to choose file name ¬ with prompt "${prompt !== undefined ? prompt : ""}" ¬ default name "${defaultName !== undefined ? defaultName : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath}") end tell` const appleScript = sf.system.execAppleScript({ script, implementation: 'OSAScript' }); const path = appleScript.result.split(':').slice(1).map(i => '/' + i.trim().replace('\n', '')).join('') const success = appleScript.success const error = appleScript.error const userCancelled = appleScript.userCancelled return { path, success, error, userCancelled }; } return { selectApplication, selectApplications, selectFile, selectFiles, selectFolder, selectFolders, selectFromList, selectSaveFileName } }
- NNicolas Aparicio @Nicolas_Aparicio
Hi @Kitch, me again 😅, I'm obsessed with this 😆.
Here's a refined version of the interactions in focus code after a few weeks of testing. I'm sure it can be written in a much more elegant way but it'll give you a starting point.
So far it's returning and behaving as expected on a few different systems but I'm sure there are still a few tweaks to add. Also, my error handling and values return might not be the best so keen to see your approach on it.
Hope it helps,
Nic.function interactions() { const desktopPath = () => sf.system.exec({ commandLine: `echo ~/Desktop` }).result function runApplescript(script) { return sf.system.execAppleScript({ script, implementation: 'OSAScript' }); } function returnPaths(appleScript) { let regex = /alias "(.*?)"/g; let matches; let pathsToFix = []; while ((matches = regex.exec(appleScript.standardError)) !== null) { pathsToFix.push(matches[1]); } const paths = pathsToFix.map(path => { return '/Volumes/' + path.replace(/Macintosh HD:/, '/') .replace(/:/g, '/') .replace(/\\:/g, '') .replace(/ \[.*?\]/g, ""); }); return paths } function onCancelFunction(settings) { if (settings !== undefined && settings.onCancel !== undefined) { if (settings.onCancel === "ThrowError") throw 'Error, user cancelled' if (settings.onCancel === "Abort") throw 0 if (settings.onCancel === "Continue") { } } else { throw 0 } } function onErrorFunction(settings) { if (settings !== undefined && settings.onError !== undefined) { if (settings.onError === "ThrowError") throw `Interaction Error` if (settings.onError === "Continue") { } } else { throw `Unexpected Error` } } /** * @param {Object} [settings] * @param {any} [settings.title] * @param {any} [settings.prompt] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectApplication(settings) { if (settings === undefined) { var title = undefined var prompt = undefined } else { var { title, prompt, } = settings; }; const script = ` tell application "System Events" activate set theApp to choose application ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ with title "${title !== undefined ? title.replace(/"/g, `“`) : ""}" end tell` const appleScript = runApplescript(script) const appName = appleScript.result.replace('\n', '') const appPath = sf.system.exec({ commandLine: `mdfind "kMDItemKind == 'Application' && kMDItemFSName == '${appName}.app'"` }).result const path = appPath const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { path, success, error, userCancelled } } /** * @param {Object} [settings] * @param {any} [settings.title] * @param {any} [settings.prompt] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectApplications(settings) { if (settings === undefined) { var title = undefined var prompt = undefined } else { var { title, prompt, } = settings; }; const script = ` tell application "System Events" activate set theApp to choose application ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ with title "${title !== undefined ? title.replace(/"/g, `“`) : ""}" ¬ with multiple selections allowed end tell` const appleScript = runApplescript(script) const appNames = appleScript.result.split(',').map(i => i.trim().replace('\n', '')) const appPaths = [] appNames.forEach(app => { let appPath = sf.system.exec({ commandLine: `mdfind "kMDItemKind == 'Application' && kMDItemFSName == '${app}.app'"` }) appPaths.push(appPath) }); const paths = appPaths.map(i => i.result) const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { paths, success, error, userCancelled } } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {any} [settings.allowedFileTypes] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectFile(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined var allowedFileTypes = undefined } else { var { defaultLocation, prompt, allowedFileTypes, } = settings; }; const script = ` tell application "System Events" activate set directory to POSIX path of (choose file ¬ ${allowedFileTypes !== undefined ? `of type {"${allowedFileTypes.join('", "')}"} ¬` : `¬`} with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath()}")) end tell` const appleScript = runApplescript(script) const path = appleScript.result.replace('\n', '') const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { path, success, error, userCancelled } } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {any} [settings.allowedFileTypes] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectFiles(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined var allowedFileTypes = undefined } else { var { defaultLocation, prompt, allowedFileTypes, } = settings; }; const script = ` tell application "System Events" activate set directory to POSIX path of (choose file ¬ ${allowedFileTypes !== undefined ? `of type {"${allowedFileTypes.join('", "')}"} ¬` : `¬`} with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath()}") ¬ with multiple selections allowed) end tell` const appleScript = runApplescript(script) const paths = returnPaths(appleScript); const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { paths, success, error, userCancelled } } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectFolder(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined } else { var { defaultLocation, prompt, } = settings; }; const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath()}")) end tell` const appleScript = runApplescript(script) const path = String(appleScript.result.replace('\n', '')) const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { path, success, error, userCancelled } } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.prompt] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectFolders(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined } else { var { defaultLocation, prompt, } = settings; }; const script = ` tell application "System Events" activate set directory to POSIX path of (choose folder ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath()}") ¬ with multiple selections allowed) end tell` const appleScript = runApplescript(script) const paths = returnPaths(appleScript); const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { paths, success, error, userCancelled } } /** * @param {Object} settings * @param {string} [settings.title] * @param {string} [settings.prompt] * @param {array} [settings.items] * @param {array} [settings.defaultItems] * @param {Boolean} [settings.allowEmptySelection] * @param {Boolean} [settings.allowMultipleSelections] * @param {string} [settings.oKButton] * @param {string} [settings.cancelButton] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectFromList(settings) { const { title, prompt, items, defaultItems, allowEmptySelection, allowMultipleSelections, cancelButton, oKButton, } = settings; const script = `set options to {"${items !== undefined ? items.join('", "') : ""}"} tell application "System Events" activate set selectedItems to choose from list options ¬ with title "${title !== undefined ? title.replace(/"/g, `“`) : ""}" ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ OK button name "${oKButton ? oKButton : "OK"}" ¬ default items {"${defaultItems && defaultItems.length > 0 ? defaultItems.join('", "') : []}"} ¬ cancel button name "${cancelButton ? cancelButton : "Cancel"}" ¬ empty selection allowed ${allowEmptySelection !== undefined ? JSON.stringify(allowEmptySelection) : true} ¬ multiple selections allowed ${allowMultipleSelections !== undefined ? JSON.stringify(allowMultipleSelections) : true} end tell`; const appleScript = runApplescript(script) const list = appleScript.result.replace('\n', '').split(',').map(i => i.trim()) const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { list, success, error, userCancelled }; } /** * @param {Object} [settings] * @param {any} [settings.defaultLocation] * @param {any} [settings.defaultName] * @param {any} [settings.prompt] * @param {"Continue"|"ThrowError"} [settings.onError] * @param {"Abort"|"Continue"|"ThrowError"} [settings.onCancel] */ function selectSaveFileName(settings) { if (settings === undefined) { var defaultLocation = undefined var prompt = undefined var defaultName = undefined } else { var { defaultLocation, defaultName, prompt } = settings; }; const script = ` tell application "System Events" activate set theNewFilePath to choose file name ¬ with prompt "${prompt !== undefined ? prompt.replace(/"/g, `“`) : ""}" ¬ default name "${defaultName !== undefined ? defaultName : ""}" ¬ default location ("${defaultLocation !== undefined ? defaultLocation : desktopPath()}") end tell` const appleScript = runApplescript(script) const path = '/Volumes/' + appleScript.result .replace(/file /g, "") .replace(/ \[.*?\]/g, "") .replace(/:/g, '/') .replace(/\n/g, ''); const success = appleScript.success const error = appleScript.error error !== null ? onErrorFunction(settings) : null const userCancelled = appleScript.standardError.includes("User cancelled") || appleScript.result.replace('\n', '') === "false" userCancelled ? onCancelFunction(settings) : null return { path, success, error, userCancelled } } return { selectApplication, selectApplications, selectFile, selectFiles, selectFolder, selectFolders, selectFromList, selectSaveFileName } } let interaction = interactions();
Kitch Membery @Kitch2024-12-10 00:10:53.603Z
Great stuff Nicolas,
I may not have time this week to take a look. But will do so when I get a chance.
Side note, please don't spend too much more time on this as this is something that should probably be taken care of in the SoundFlow API. (Your work so far is not in vain though, I'm sure it will come in very handy when implementing the internal fix.)
I have it logged internally as SF-340, and you'll get updated once it's been implemented.
Rock on!
- NNicolas Aparicio @Nicolas_Aparicio
Thanks @Kitch, no worries at all, it was a good opportunity to learn a bit more 🤙
Cheers.Kitch Membery @Kitch2024-12-10 00:15:25.454Z
- In reply toNicolas_Aparicio⬆:
Chad Wahlbrink @Chad2024-12-10 02:05:57.230Z
It's such a great utility script until the SF API is updated. Thanks for sharing!
I used your functions with a script I wrote over the weekend, which worked great. The version I copied hadn't added the replace function to the Select File or Select Folder functions –
const path = appleScript.result.replace('\n', '')
. I usedappleScript.result.trim()
, which seemed to do a similar job of solving that issue.Great work. I love the practice of working this out. Thanks for sharing.
- NNicolas Aparicio @Nicolas_Aparicio
Thanks legend @Chad_Wahlbrink1, yep I changed a few things as some interactions were not returning the right info.
Ah right, useful to know trim() does that, it's an interesting world when looking into the applescript language.
Thanks a lot for the compliments to you and to @KitchChad Wahlbrink @Chad2024-12-10 03:47:29.862Z
Yes for sure. AppleScript is a funny little scripting language.
Also, just a small note that my main account is @Chad. 🙏
Keep up the good work, Nicolas
- In reply toNicolas_Aparicio⬆:
Kitch Membery @Kitch2024-11-13 21:34:31.772Z
If the
directoryGetFiles
returning undefined and the mapping JSON issues are separate issues, please open a new thread for them.Thanks in advance.
- SIn reply toNicolas_Aparicio⬆:SoundFlow Bot @soundflowbot
This report was now added to the internal issue tracked by SoundFlow as SF-340
- SIn reply toNicolas_Aparicio⬆:SoundFlow Bot @soundflowbot
The linked internal issue SF-340 has been marked as Done
- SIn reply toNicolas_Aparicio⬆:SoundFlow Bot @soundflowbot
Linked issue SF-340 updated: This will be fixed in SF 5.10.0