If that title sounds eerily familiar, it’s because I stole it from Gruber. I did that because 1) I’m doing the exact same thing here, 2) there is a small bug in his solution, and 3) there’s a big ass bug in the
processes collection in System Events’s dictionary, which combined, lead his solution to double failure.
The small bug is really simple, Gruber returns the
name of _app. But
name will return “Safari” for both Safari and WebKit. The property that we want is
Now for the Fun Part
Gruber’s code, corrected to use
short name, ends up as:
tell application "System Events" set _app to item 1 of (every process whose frontmost is true) return short name of _app end tell
And that will generally work. Sometimes.
If we have both Safari and WebKit open, System Events gets confused. As a general rule, it’ll always return the process for the application which was started first. Here’s a quick example in Ruby.
First I started WebKit, then Safari, then ran this:
>> Appscript::app('System Events').processes['Safari'].short_name.get => "WebKit"
But that’s not the particular way in which System Events gets confused in Gruber’s script.
Superfluous Assignments Considered Harmful
It’s a big pet peeve of mine that people spend way too many lines of code assigning stuff to variables, instead of just doing whatever is needed to be done with the values right away. For once — however incidentally — I’m validated.
Here’s Gruber’s code, refactored yet again for brevity:
tell application "System Events" to set _app to the first process whose frontmost is true get the short name of _app
When he gets the first process and assigns it to the
_app variable, that’s where System Events can get confused and return the wrong process. Funny enough, this code works perfectly:
tell application "System Events" to get the short name of the first process whose frontmost is true
Somehow, skipping the intermediate assignment and going straight for the kill makes System Events behave sanely.
Now, I went through great lengths to confirm my observed behavior. I wrote a script that gets the short name for the process that is frontmost, testing it with different launch orders and varying the frontmost browser, using both code variants: with temporary assignment, and with direct access. I reformatted the output into the following pretty ASCII table:
+----------------+-----------+----------+----------+ | Launch order | Frontmost | Directly | Assigned | +----------------+-----------+----------+----------+ | Safari | | Safari | Safari | | WebKit | | WebKit | WebKit | | Safari, WebKit | Safari | Safari | Safari | | Safari, WebKit | WebKit | WebKit | Safari * | | WebKit, Safari | WebKit | WebKit | WebKit | | WebKit, Safari | Safari | Safari | WebKit * | +----------------+-----------+----------+----------+
The two entries marked with an
* are where we the script returned the incorrect browser name.
So if you just love plain AppleScript and want a fix for Gruber’s framework, here’s his amended
on GetCurrentApp() tell application "System Events" to ¬ get short name of first process whose frontmost is true end GetCurrentApp
Ruby Roolz AppleScript Droolz
It wouldn’t be me without a Ruby solution, especially since all the past scripts I posted here were written in Ruby.
The following method will return an
Appscript::Application for the preferred browser, according to similar rules as originally defined in Gruber’s article, but with a bit of extra smarts:
First, it’ll look at the frontmost app; if it’s one of the valid browsers (WebKit or Safari), it’ll return that. Then it’ll look into the running browser processes. It’ll return the running browser, giving preference to the default browser if both are running. If no valid browser is running, it’ll just return the default. It’ll only return a valid browser. If it has to fallback to the default browser and it is, say, Chrome, it’ll just return
VALID_BROWSERS = %w[ WebKit Safari ] def browser processes = Appscript::app("System Events").processes target, = VALID_BROWSERS & processes[Appscript.its.frontmost.eq(true)].short_name.get target ||= begin default_browser = VALID_BROWSERS & [%x(VERSIONER_PERL_PREFER_32_BIT=yes /usr/bin/perl -MMac::InternetConfig -le 'print +(GetICHelper "http")').chomp] running_browsers = VALID_BROWSERS & processes.short_name.get (running_browsers & default_browser) || running_browsers || default_browser end Appscript::app(target) if target end
Here’s the full thing as a standalone Ruby file.
In a follow-up article, Gruber talks about premature optimization, how shelling out to Perl is potentially expensive, but fast enough for practical purposes. He gives an execution time for his browser detection script of “less than 0.1 seconds”.
Now, I tend to agree with him. That’s fast enough. Hell, I write my stuff in rb-appscript, which has a significant overhead over compiled AppleScripts. But still, just for fun, here’s a solution that clocks in at ~0.023s on my machine.
It’s an Objective-C file, which has to be compiled. The compiler command is listed as a comment at the beginning of the source file. It relies on AppKit to get the frontmost process, and LaunchServices to get the default browser. If anything, LaunchServices should stand the test of time longer than Perl’s Mac::InternetConfig.
The semantics for this are pretty much identical to Gruber’s original
GetDefaultWebBrowser solution, without any of the extra smarts of my Ruby version, because writing Objective-C is nearly as boring as writing AppleScript, and this was just a gimmick anyway.
In the next weeks I intend to post my Safari/WebKit appscript scripts so we can actually put my
browser.rb to some use.
Got comments? Use reddit and/or poke me on twitter.