luckman212 Posted June 9, 2021 Share Posted June 9, 2021 I'm looking for an efficient way to get the bundle ID of the frontmost app into a workflow variable to be used in a script filter. (basically want the script filter to display context-specific results based on the frontmost app) I tried searching for a way to do this but came up short. anyone got a tip? I know I can get this info using e.g. Python... but that seems a bit slow/inefficient #!/usr/bin/python from AppKit import NSWorkspace try: bundle_id = NSWorkspace.sharedWorkspace().frontmostApplication().bundleIdentifier() except: print('unknown') exit(1) if bundle_id: print(bundle_id) Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 (edited) 2 hours ago, luckman212 said: but that seems a bit slow/inefficient If using a hotkey is out (Alfred can give you the bundle ID), you should use AppleScript or JXA instead: it's a lot faster than importing PyObjC libraries. tell application "System Events" set _name to name of (first process whose frontmost is true) end tell tell application _name to return id as text Edited June 9, 2021 by deanishe Link to comment
Mr Pennyworth Posted June 9, 2021 Share Posted June 9, 2021 (edited) 2 hours ago, deanishe said: If using a hotkey is out (Alfred can give you the bundle ID), you should use AppleScript or JXA instead: it's a lot faster than importing PyObjC libraries. Or even faster, you could use swift. The applescript one takes ~800 milliseconds on my machine Swift one takes ~80 milliseconds import AppKit print(NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "Unknown") Disclaimer: hardly any rigorous testing / benchmarking: Edited June 9, 2021 by Mr Pennyworth Link to comment
Martin Packer Posted June 9, 2021 Share Posted June 9, 2021 @Mr Pennyworth what does one need to make Swift work? XCode? Or is a compiler built in? I suspect the former. Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 17 minutes ago, Martin Packer said: XCode? Or is a compiler built in? Neither. You can install Xcode, or you can just install the developer command-line tools, which are far, far smaller. Run xcode-select --install in a shell to install them. If you're planning to distribute the workflow, be aware that binaries in a workflow can cause a lot of hassle unless they're signed. Link to comment
luckman212 Posted June 9, 2021 Author Share Posted June 9, 2021 (edited) @Mr Pennyworth that's cool - how would I use that to get the variable into the script filter? I'd call out to that "./frontmost" executable from inside my Python script? edit: I compiled your swift binary and it is indeed very fast. I got it to work using this #!/usr/bin/python import os try: cmd = '/usr/local/bin/frontmost' bundle_id = os.popen(cmd).read() except: exit(1) ...rest of the workflow I'm sure there are much better/safer ways to do this using e.g. subprocess()... is this the right path to take? Edited June 9, 2021 by luckman212 Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 1 hour ago, luckman212 said: I'm sure there are much better/safer ways to do this using e.g. subprocess()... is this the right path to take? subprocess.check_output() is a bit simpler. luckman212 1 Link to comment
Mr Pennyworth Posted June 9, 2021 Share Posted June 9, 2021 (edited) 4 hours ago, luckman212 said: inside my Python script Oh you are already using a python script!! That changes things! The biggest performance problem I have with python scripts for workflow is the interpreter startup time. For a script that has already started, the additional cost of calling the AppKit API seems to be negligible. Take a look at this ipython session: In under 70 milliseconds, the code has queried bundle ID 10,000 times. Not that's consistent with the next one where it took 8 milliseconds to query the bundle ID 1000 times. In [1]: import timeit In [2]: imports = 'from AppKit import NSWorkspace' In [3]: code = 'NSWorkspace.sharedWorkspace().frontmostApplication().bundleIdentifier()' In [4]: timeit.timeit(setup=imports, stmt=code, number=10000) Out[4]: 0.068 In [5]: timeit.timeit(setup=imports, stmt=code, number=1000) Out[5]: 0.008 Edited June 9, 2021 by Mr Pennyworth Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 32 minutes ago, Mr Pennyworth said: the additional cost of calling the AppKit API seems to be negligible It's not the calling that takes the time, it's the importing. Mr Pennyworth 1 Link to comment
Mr Pennyworth Posted June 9, 2021 Share Posted June 9, 2021 15 hours ago, deanishe said: it's a lot faster than importing PyObjC libraries. Reading forum posts properly would save me some precious time! 🤦🏻♂️🤣 deanishe 1 Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 (edited) 2 hours ago, Mr Pennyworth said: Reading forum posts properly would save me some precious time! 🤦🏻♂️🤣 Poking at the various solutions, there doesn’t look to be much difference between Python and AppleScript. The AS is faster, but as you note, the Python script is already running, which makes calling the AS script from Python no faster than just doing from AppKit import NSWorkspace. The Swift solution is obviously massively faster, but if you don’t want to use a binary, you might as well do it all in Python, but only import NSWorkspace when you need it (if that’s possible). Edited June 9, 2021 by deanishe Link to comment
luckman212 Posted June 9, 2021 Author Share Posted June 9, 2021 Thanks @deanishe and @Mr Pennyworth for the advice and knowledge. Figured I'd share what I ended up with so far, in case it's useful. I'd also welcome any advice on whether this code is utter garbage or wildly unsafe etc, as I am not a Swift developer by any stretch. The below produces a small binary which outputs 3 fields (tab-separated): bundle_id, localized app name, and path. There's some error checking in there but it was a bit of trial and error, so not sure it's correct. I use this in my script filter to filter the results by bundle ID of the frontmost app, and use the App Name and Path in the JSON to prefix the items e.g. "Google Chrome: foo bar" and grab the native app icon to distinguish those results. The workflow is called "Mnemonics" and it's kind of my own TLDR system to help me remember how to do little things e.g. How do I set a variable to the contents of a heredoc in bash? or What's the keyboard shortcut for opening the Artboard in Adobe Illustrator?. I will probably publish it soon. Just need to switch the filter from Bash to Python (to remove the dependency on jq) and switch the config file from plaintxt JSON to SQLite... frontmost.swift (compile with `swiftc frontmost.swift`) import AppKit final class StandardErrorOutputStream: TextOutputStream { func write(_ string: String) { FileHandle.standardError.write(Data(string.utf8)) } } enum frontmostError: Error { case bundleid case path } do { let frontApp: NSRunningApplication = NSWorkspace.shared.frontmostApplication! guard let bundleid: String = frontApp.bundleIdentifier else { throw frontmostError.bundleid } guard let path: String = frontApp.bundleURL?.path else { throw frontmostError.path } let name: String = frontApp.localizedName ?? bundleid print(bundleid, name, path, separator:"\t", terminator:"\n") } catch { var outputStream = StandardErrorOutputStream() print("error: \(error)", to: &outputStream) exit(EXIT_FAILURE) } Bash script filter (partial) IFS=$'\t' read -r bundle_id app_name app_path < <(/usr/local/bin/frontmost) [ -n "$bundle_id" ] || exit /usr/local/bin/jq <config.json \ --arg b "$bundle_id" \ --arg n "$app_name" \ --arg p "$app_path" \ '{ items: [ .[] | select(.contexts != null) | .contexts as $c | select($b | IN($c[])) | ... Link to comment
deanishe Posted June 9, 2021 Share Posted June 9, 2021 Man, that bash/jq looks hairy. A couple more lines like that, and I'd switch to writing the whole thing in Swift. Link to comment
luckman212 Posted June 10, 2021 Author Share Posted June 10, 2021 Wish I had the chops for that @deanishe but it's likely above my pay grade. Maybe I'll try. Do you think in general it's a good idea to switch my configuration to a SQLite db instead of the current plaintext json? My reasons for wanting to do this are mainly: reduce chances of error in data entry ability to query ahead of time to constrain results vs doing the filtering after (although there's a tradeoff of course since once the results are cached, Alfreds builtin filtering is probably faster) ability to quickly sort entries by the various fields currently the config.json data looks like this (sample): [ { "title": "Mnemonics snips folder", "desc": "open folder of snips for this workflow", "keywords": [ "mnemonics","snippets","cfg","config" ], "type": "file", "icon": "f-atom.png", "object": "./snips/", "bundleid": "com.apple.finder", "contexts": [ "com.runningwithcrayons.Alfred-Preferences" ] }, { "title": "Search for executables in Terminal", "keywords": [ "command","find","binaries","perm","perms","type","111" ], "type": "copy", "icon": "terminal.png", "object": "find . -type f -perm 111 ! -name \"*.*\"" }, { "title": "Create symlinks", "desc": "ln -s[fiv] /path/to/file [/path/to/symlink] (default=current dir)", "keywords": [ "soft","hard","command","ln","alias","syntax","link" ], "type": "copy", "icon": "terminal.png", "object": "ln -s /path/to/file /path/to/symlink", "url": "x-man-page://ln" }, ... Link to comment
deanishe Posted June 10, 2021 Share Posted June 10, 2021 (edited) 5 hours ago, luckman212 said: Do you think in general it's a good idea to switch my configuration to a SQLite db Probably not. It's a lot more complicated than a JSON file. The database might be easier to edit without messing anything up, but the code is harder to get right, imo. There's probably no performance advantage to SQLite, either. That's something to start thinking about when you're dealing with thousands of items. 5 hours ago, luckman212 said: Wish I had the chops for that How much is there left to do? Read and write a bit of JSON? It's no more difficult than what you've already written, imo. It's a lot more straightforward than the bash code. Edited June 10, 2021 by deanishe Link to comment
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now