By John Gruber
1Password — Secure every sign-in for every app on every device.
First, a bit of background information. “WebKit”, in addition to being the name of the HTML/CSS/JavaScript web rendering engine at the heart of Safari (and, now, numerous other browsers like Chrome), is also the name of an application for the Mac that is, more or less, just like Safari but uses the latest nightly build of the WebKit rendering framework instead of the standard system-wide version of the WebKit framework that ships as part of Mac OS X. You can download the latest WebKit nightly build here.
The WebKit app is so much like Safari that its application menu — the one right next to the Apple menu — even says “Safari”. It reads and writes the same preferences and bookmarks as Safari. It’s more like a phantom copy of Safari than a separate application.1 But WebKit does run as its own distinct process. You can, for example, run Safari and WebKit at the same time. WebKit has its own app icon, too. And, with regard to AppleScript, WebKit needs to be addressed specifically.
This script will launch or activate Safari:
tell application "Safari" to activate
This script will launch or activate WebKit:
tell application "WebKit" to activate
I have a handful of AppleScripts I’ve written that do things with Safari. They’re saved in a folder at ~/Library/Scripts/Applications/Safari/, and when Safari is the current application, they appear at the top of my system-wide Script menu. (I use FastScripts rather than the Script menu from Apple because FastScripts allows you to assign keyboard shortcuts to scripts.)
What I’d like, when I’m running WebKit, is to have access to the exact same set of scripts I created for use within Safari. The obvious solution is tedious:
tell application
target in each of those scripts from “Safari” to “WebKit”.It’s worth noting that because WebKit is just a phantom version of Safari, it shares the exact same scripting dictionary. A script that works with Safari should work exactly the same with WebKit — except that you need to make WebKit the target of the tell
block. The problem with this obvious solution is that every time I change a script or create a new one, I’d have to maintain two separate copies.
A better solution is to use what I’ll call dynamic tell targets — instead of making the tell
target a hard-coded application name, make it a variable that is determined at runtime. When I first set out to do this, I couldn’t find examples of it being done before. Perhaps that’s because it’s something you so rarely need to do in AppleScript. When you say tell application "Foo"
in AppleScript, you’re telling the script that the current block is using terms from Foo’s scripting dictionary. And a big part of the entire point (and pain) of AppleScript is that each app provides its own unique dictionary.
Safari and WebKit are an exception. (The technique I outline below might, hypothetically, also be of use if you found yourself needing to write scripts that work against multiple versions of the same app, provided both versions use the same scripting terms.)
There are two main contexts from which you’d want to run a script that dynamically targets either Safari or WebKit:
The framework I’ve created supports both of these. Even for scripts I only intend to use from the Script menu (therefore using technique #2, dynamically targeting the current active application), it’s useful to include technique #1 as a fallback so that the script works while editing and debugging it — when you run a script from within Script Editor, Script Editor is the frontmost application.
Here’s the basic framework:
set _browser to GetCurrentApp()
if _browser is not in {"Safari", "WebKit"} then
set _browser to GetDefaultWebBrowser()
end if
using terms from application "Safari"
tell application _browser
-- do stuff with Safari or WebKit here
end tell
end using terms from
on GetCurrentApp()
tell application "System Events" to ¬
get short name of first process whose frontmost is true
end GetCurrentApp
on GetDefaultWebBrowser()
-- First line of _scpt is a workaround for Snow Leopard issues
-- with 32-bit Mac:: Carbon modules
set _scpt to "export VERSIONER_PERL_PREFER_32_BIT=yes; " & ¬
"perl -MMac::InternetConfig -le " & ¬
"'print +(GetICHelper \"http\")[1]'"
return do shell script _scpt
end GetDefaultWebBrowser
There are two utility handlers, GetCurrentApp()
and GetDefaultWebBrowser()
, both of which return the name of an app as a string. My thanks to Christopher Masto for the bit of Perl code that gets the user’s current default web browser.2 The script first gets the name of the frontmost app. If it’s “Safari” or “WebKit”, then it assumes that’s the app you’re targeting. If not, it gets the name of the user’s default web browser and uses that. There is a big assumption here: if neither the frontmost app nor the default web browser is Safari or WebKit, the script will target whatever the current default web browser actually is, and it almost certainly won’t work. If you use something other than Safari or WebKit as your default browser, you’ll want to change this.
So at this point the variable _browser
is a string set to either “Safari” or “WebKit”. And you can use this variable as the target of a tell application
block. But, if you use any Safari/WebKit-specific scripting terms inside the tell
block — and you almost certainly will, since that’s the entire point of writing a script that targets these apps — the script will not compile without the outer using terms from
block.
There’s a two-step processing to running AppleScript source code: first it compiles, then it can run. But AppleScript can’t compile terms from a specific application dictionary without knowing exactly which application that is, and the whole point of this exercise is that we don’t want to determine the target of the tell
block until runtime. The using terms from
block lets us work around that. We still have to hard-code the name of a specific application, but by putting it in the using terms from
block rather than the tell
block, it’s just information for AppleScript to use at compile time. (We could just as easily use WebKit as the target of the using terms from
block, but I figure it’s better to use Safari since every Mac ships with Safari installed.)
After re-writing all my Safari-targeting AppleScripts to use this framework, I simply made an alias to my ~/Library/Scripts/Applications/Safari/ folder, and named the alias “WebKit”. I now have one set of scripts that work in (and appear at the top of my Script menu in) both apps.
The WebKit.app bundle is really just two things: updated WebKit frameworks, and an executable binary that launches an instance of the regular Safari binary with a directive to use the new WebKit frameworks rather than the standard system frameworks. So as applications, Safari and WebKit are truly identical — the only difference is the rendering engine. ↩︎
I updated the Perl bit slightly in September 2009 for compatibility with Mac OS X 10.6 (Snow Leopard). ↩︎
Previous: | The Truth |
Next: | ‘Do Shell Script’ and Premature Optimization |