Jump to content

get bundle_id of frontmost app in Alfred workflow?


Recommended Posts

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
Posted (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 by deanishe
Link to comment
Posted (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:

image.thumb.png.c907c5e17aacec9f8d369de3dc9f9921.png

Edited by Mr Pennyworth
Link to comment
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
Posted (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 by luckman212
Link to comment
Posted (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 by Mr Pennyworth
Link to comment
Posted (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 by deanishe
Link to comment

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

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
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 by deanishe
Link to comment

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...