Application Technologies • 36:59
Mac OS X contains many powerful applications and utilities that you can use from your application via Apple events. Learn about new and improved ways to invoke that power easily and efficiently, whether your language of choice is AppleScript, Objective C, Python, or Ruby.
Speakers: Chris Nebel, Adam Goldstein
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good afternoon. Welcome to session 116, Controlling Scriptable Applications. I'm Chris Nebel, your host for the hour. And Controlling Scriptable Applications. This title could mean a couple of different things, and you know, not everyone reads the session descriptions before coming in here. It's okay, I do it myself. So let me take a couple minutes to talk about what we are not going to talk about in this session. This session is not about learning AppleScript. I'm not going to teach you about how to write subroutines in AppleScript. You're not going to learn any clever Photoshop scripting tricks.
This session is not about making your application scriptable. We would love it if you did that. We would be happy to help you. But this is not the session for it. The official time for that was all this morning, but there's still unofficial lab time tomorrow. Go to the lab across the hall. Ask for John Comiskey. He's the real expert. I can help you with some of it, too. We'd be happy to help you with whatever problems you have.
And lastly, and this is the kind of important one, this session is not about just AppleScript. We will be talking about AppleScript, but it's not just AppleScript. A lot of people, some of my management chain among them, have this idea that if you're going to make a Scriptable Application do something, you have to use AppleScript to do it. Scriptable equals AppleScriptable. And this is not really true.
The basic mechanism by which an application is Scriptable, Apple events, those were designed to be language neutral. So you can use AppleScript if you want to, by all means do so, works great, we'll be talking about some ways that it's improved in Leopard. But you could equally well be using Objective C, or Python, or Ruby, or any of a number of other scripting languages that are installed on the system.
Because all of them can control Scriptable Applications in one way or another. Now, some of the mechanisms that they use to do it are not very well publicized, some of them are not as pretty as they could be. But we are working on improving that in Leopard, which brings us to what we are going to talk about.
Using features from other applications. You can make other applications do your work for you. There are lots of Scriptable Applications on the system, some of them from Apple, some more from third parties, and they will do all sorts of useful things for you if you ask them to. For instance, iPhoto's Mail Photos feature is actually implemented by telling Mail to make a new message and attach this photo.
They didn't have to write their own mailer. They had Mail do it for them. So you can do the same thing, and we'll be talking about how to do that. Now, there are a number of different mechanisms you can use to do this. Some of them work better than others. Some of them have certain pitfalls. So we'll be talking about how to do that without causing undue suffering, either to you or to your users.
And we'll be talking about how to do that, like I said, from not just AppleScript, but a variety of languages-- AppleScript, Objective C, Python, and Ruby-- in that order. So let's begin with AppleScript. So AppleScript is kind of the original make Scriptable Applications do stuff language. It's been around since System 7. Works very nicely. It's very, very good at controlling Scriptable Applications, as it should be, since that's what it was designed to do from the beginning.
So for the past number of releases, we've been making basically smallish changes, small but useful stuff, expanding AppleScript here and there, adding new Scriptable Applications. This is a list of some of the high points from Tiger. There's a complete list now available on the web filed under the Release Notes section in the developer site.
But by and large, the core language is pretty much what it was at the beginning. Now, this is good in that it makes a nice stable base to write your code on. But it's not so good in that time marches on. There are certain key technologies now that really just weren't there when AppleScript was originally designed, so it doesn't work all that well with them.
And there's some things that seemed like good ideas at the time and have turned out really to not work so well. So for Leopard, we're getting a bit more aggressive, and we're actually tackling some of those. So the first one is all Unicode, all the time. AppleScript is now Unicode top to bottom.
You can put anything you want in any language. If the system can manage to render it, it's good script. You can put anything you want in a string or comment. The syntax for identifiers did not change, so don't get too crazy there. All internal text operations are Unicode.
So however you combine and recombine and generally mangle your strings, it will all work. And yes, we did in fact add a feature to translate between code points and the actual characters. In general, you shouldn't actually notice the difference. It's just that it will do the wrong thing less often.
Next one is we improved the application model some. Added some new features here. We've supported telling an application at a particular HFS path for a while. We've made POSIX path an equal citizen there. You can tell an application by its bundle ID or by its signature. This becomes very important when you're distributing scripts. Users rename applications from time to time. They'll move them around. Developers will rename applications on you. FileMaker likes to do this with every major rev.
and there's some new intrinsic application properties. All of these work without launching the application, by the way. So you can directly get the version, you can get the ID, you can find out if it's frontmost or if it's running. That last one is because of a pet peeve of mine. About once a month or so, someone will ask on the AppleScript users list, "How do I tell whether or not this application is running?" And invariably the first five responses will be the lame, slow way to do it.
So now there's a nice, clean, direct way. I should mention that all of these features here, you can do these things now, but they tend to take a lot more, they tend to take at least five or six lines of code and don't necessarily work all that well. So we've cleaned them up, we've made them convenient to use.
[Transcript missing]
There's a certain amount of housekeeping going on. You've been probably hearing a bunch about 64-bit. AppleScript is making the transition to 64-bit right along with the rest of the system. As far as AppleScript itself is concerned, if you're writing scripts, you basically don't have to care. AppleScript all works the same. It's just works more stuff now.
If you're writing to the C APIs to actually run AppleScript that way, there are few APIs that are going away. You probably don't care about any of these unless you're writing your own script editor, and we basically know who you are. If you do care, these are the ones that are going away, the originals on the left and their new replacements on the right. Oh, and one more thing. Ah, I think I just hit the wrong button. We're updating the language guide.
Next slide. Objective C. So for Objective C, you actually have your choice of APIs. There are three different ones you could conceivably use to control a Scriptable application. The sort of basic low level one is NSAppleEventDescriptor. So if you know exactly what you're going to do when you're coding your applications, so like the iPhoto mail a photo feature falls into this category, you can construct the exact Apple events that you want to send and do that straight from Objective C code. Like I said, because these are compiled into your application, you have to know what you're doing at build time. You can't change it afterwards. But this is the way that uses less memory and it runs the fastest.
Now, you don't have to do this. What you could do instead is actually ask to run a script and make that AppleScript control the Scriptable Application. And this becomes useful because you can change the script much later. You don't have to decide what the script is until the very instant that you run it.
So if you want to make your application somehow modifiable, if you want to let the user supply the script, then this is the thing to do. Mail rules, for instance, can do this. One of the possible actions in a mail rule is to run an AppleScript, which the user can supply. So it's a good way to make your application more customizable.
And if you want to be extra studly and let the user edit scripts right in your application, you can use OSAKit. It supplies an editor to do that. So a script editor uses this, as does Automator's run AppleScript action. Now, the hitch with these two upper layers is that because you're actually running a script, you're not just building the raw Apple events.
You have to pay the cost for the AppleScript interpreter, which is non-trivial. You actually pay a noticeable performance cost for doing that. So in theory, if you just want to control a Scriptable Application, you should go use NSAppleEventDescriptor. The funny thing is that people don't do that generally. They usually just go straight to NSAppleScript, even if they don't need to.
So why is that? Well, it's pretty simple, really. Say you want to get the name of the current track in iTunes. Right? Fairly simple operation. If you want to use NSApple's Apple Event Descriptor, which, of course, you want to do, it's fast and it's efficient, all you have to do is write this.
Right. In fact, it's even worse than that. Because if you look carefully, all those little four character codes in there, those are not really documented. And you have to kind of dig them out of iTunes. And how to do that isn't documented either. So yeah, get right on that.
Then you wander over to NSAppleScript, and to do the same thing in NSAppleScript, you have to write this. This is a little more manageable. So naturally, developers, you have limited time, you have limited resources. You pretty much go for this, even if it is a little bit slower.
The problem, of course, is that it is, in fact, slower and it's limited. You have to pay-- it runs 10 to 100 times slower than an SAPL event descriptor, depending on how much of the interpreter is hot and live. You use 5 to 10 megabytes of additional memory. It doesn't scale very well. The more logic you have to put into the script, the worse it gets.
And it doesn't integrate very well with Objective C. The more information you have to pass in and out, it starts to get more and more complicated very rapidly. And of course, you have to know AppleScript to do it. You have to at least mentally change gears from Objective C into AppleScript. So what you would like is something that hits this sweet spot of it's fast and efficient and doesn't make you write a lot of code. And ideally, that lets you work in Objective C terms that you're actually used to.
So by remarkable coincidence, we have such a thing in Leopard. This does not come as a complete surprise to many of you, because Ted talked a little bit about this in the tool State of the Union. It's called Scripting Bridge. So here's what you have to do to use it.
So step one is you generate some glue files for the particular application that you're interested in, in this case, iTunes. There's a command line that you type to get the SDEF, the scripting definition file, from iTunes. and then pipe that to a tool that will generate the glue files. So the cost to you for this is however long it takes you to type this, probably a minute tops.
Step two is you take those glue files and you add them to your project. And you link to a new framework called ScriptingBridge Framework, which supplies a bunch of the foundation glue for doing this. Step three is you write your code. This is the code that you write to get the name of the current track in iTunes.
This is shorter than the NSAppleScript version, and it runs as fast as the NSAppleEventDescriptor version. It is, I believe, the graph that Ted showed was up to 81 times faster than using NSAppleScript. And it's actually obvious what the heck you're doing here. And notice that it's getting the result directly as an NSString.
So talk is cheap. Let's see a demo. For this, I'd like to bring Adam Goldstein up on stage. Adam has been doing-- Adam wrote most of the Scripting Bridge framework part. He's been working as our intern over the summer. He is also the author of AppleScript: The Missing Manual. He'll be doing a book signing at the Apple Store tomorrow at 6:00, I believe. So Adam Goldstein, please.
[Transcript missing]
We get this nicely formatted XML document, and you can look through it to learn how iTunes does things. But luckily, you don't have to, because we've got this SDP command as well. Let me clear this out. This SDP command, when you pipe the output of SDF into it, will actually generate the glue code for you.
So as you can see here, we generate the SDF and then the options that we put on SDP are -fmh. And what that tells SDP is to generate both the .m and the .h files to control iTunes. And then in the base name option, we actually specify the name of the program that we're controlling.
And this will be the name of the files that we create and also the prefix for the classes contained within those files. And then finally, we provide this bundle ID option where we specify the identifier of iTunes so that this will work on any computer and all it has to do is know the bundle ID to locate the program. So let's generate this. Very quick.
Once we go into the folder in which we created these, we find-- oh, there they are. So now we can just drag these two items into Xcode. Add them to our project. And now we can talk to iTunes. Oh, actually, forgot a step. We have to link to the ScriptingBridge framework. So I can just click on Frameworks, Add, Existing Frameworks, and let's go down to ScriptingBridge.Framework.
So what's the actual code that we use to do this? Well, let's open up-- oops, wrong .m file. Let's open up our controller and say add a pause method. Now, the first thing that we want to do is to check whether iTunes is running, because it doesn't make much sense to launch iTunes just to see if it's, you know, just to pause it. So first we declare the iTunes application here. Simple command that Chris showed in his presentation. Then we check to see if it's running. And we do this using the isrunning command, which is defined for all applications.
So now that we've checked whether iTunes is running, all we have to do is send it the pause command, which is a simple one-line command like that. So that's the pause method. Now let's talk about the play method. Obviously, this is going to be a little more complicated because we're going to have to fade in. So first, again, we declare iTunes so that we can communicate with it. Then, again, we check to see whether it's running. And finally, we have this fade-in play section. And let me step you through this.
So we've got these two variables here, v and original volume. v is whatever iTunes' current volume is, and obviously, original volume is what it started at. So we set the original volume variable to the current volume of iTunes right now. And then we set the volume to zero right here.
And then we say, iTunes, play once. Now, this might seem like a little bit of an odd method call to use, but what the once part of that method does is it tells iTunes whether to play through the current song and then stop, or whether to play through the current song and then continue playing. So since we specify no, iTunes knows to play through the current song and then continue playing.
Then, as you see here, we have a standard C-style for loop, where we set the current volume to zero, then iterate 16 times right here until we reach the original volume. And within this for loop, we set the sound volume to whatever that V variable is at the moment, and then pause for a tenth of a second.
And then, just to be sure that we're set back to our original volume and we didn't have any strange math errors, on this final line, we set the sound volume back to what it was to begin with. So now that we've got all this code in our project, we can build and go. Save this. That's strange. Let's make sure that we've got iTunes imported. Oh, I should add it to the project and copy the files.
[Transcript missing]
And here it is. So let's quit iTunes so that I can show you that when we click one of these buttons when iTunes isn't running, nothing happens. You can see iTunes down in the dock hasn't launched. Now let's open iTunes up. Now let's say it's playing... and, okay, a phone call comes in. Paused. Now, let me zoom in so you can see what happens when we play back. Watch the volume slider. Very slick. So that's the power of Scripting Bridge in a nutshell. Now, back to Chris.
Thank you, Adam. So that was it. That's scripting bridge in a nutshell. Generate the glue, add it to your code, use the nice simple method calls. Straight from Objective C, it looks like the stuff you're currently using. And that's kind of the basic conceit of scripting bridge, is that it makes this other scriptable application look like just a bunch of normal Cocoa objects. It uses basically all the same techniques you're already using now if you're using Objective C.
To get an attribute or set an attribute, it's just normal KVC accessors. If you want to get an element that looks like an NSArray, the object index works, all the NSArray stuff works. In fact, it actually is an NSArray. It's descended from NSArray. If you want to create an element, init, alloc init, and add object. And to call a command, that's just a method call.
There are a couple new ones, because scripting dictionaries can do a couple things that NSArrays can't, that I should call out. Object with name and object with ID. If you know specifically the name of the thing that you're going for, there's a special method you can use to do that.
For somewhat more advanced stuff, again, it's still just standard Cocoa. MakeObjectsPerformSelector to do many objects at once. GetElementsMatchingAndCondition. You use filtered array using predicates, same as you do with core data. And again, there's one new one here. Get a property of many objects at once. There's a new one, ArrayByApplyingSelector. It's just like MakeObjectsPerformSelector, but it returns a result and returns you an NSArray of the results.
So this is so simple, so obvious, you want to go run out and start using it right now. Wait for the fine print. There are a couple of tricky bits, which will bite you if you don't know what's going on. So the first one's slightly Zen-like. That subtitle there is the first line from the Tao Te Ching. The way you can speak of is not the true way.
And this seemed kind of appropriate because in a bridge like this, the objects you see in your application are not the real objects. The real objects are on the other side of the bridge in another process. You never get to see the real objects yourself. What you have is a reference.
What's a reference? A reference is a set of directions to find the true object. So let me give you an example. Adam is not the real person. The directions, front row, fourth from the left, those are my directions to find Adam. Now, there are two interesting upshots of this. One is that the bridge is lazy.
Actually, sending an event across the bridge is relatively expensive, so we try to put that off until we absolutely have to send it. And there are two cases in which we do that. One is when you tell it to do some command. It has to do that right now. And the other is when you get an attribute of an object. That's an attribute like some sort of primitive data, something that's not another application object.
The other big thing is that some directions are better than others. I said, for instance, to locate Adam front row, fourth from the left. Say, you know, I was coming in here, I'm the understudy, I've been told that somebody is going to help me with my demo. But I've never actually met Adam before, but I have the directions to find him.
Front row, fourth from the left. That works fine right now, but, you know, say he gets up and goes to get a drink and somebody steals his seat, all three of us are going to get a very unpleasant surprise on the next demo. If, however, I'd been told go into the room and ask for Adam Goldstein, that would work no matter where he moved.
So let's look at an example where this actually makes a difference. So going back to iTunes again, my favorite example. If you want to get the library, this is pretty much what you have to do. iTunes, Sources, and then the source within that is the library. And, you know, this is cool. Your library isn't going anywhere. So this basically works. But let's look at something a little more complicated.
Say you're writing a program where you want to let the user edit stuff about the currently playing track, kind of on the fly, as it's playing. So you might be tempted to write something like this. Get the current track, say we're working on the description, let the user edit, and then when they're done, set the description back.
Now, this has kind of an obvious bug in it, which is that the current track might change between the first line and the last line, and you don't want to set the description on the wrong track. So, okay, fine, the solution is reasonably obvious, right? You hoist iTunes' current track out into a variable track, and then you refer to that.
Except that doesn't actually work in the scripting bridge. Remember I mentioned that it was lazy? This actually sends the exact same events as the previous one and doesn't work for the exact same reason, because iTunes' current track is really just a reference. It's just the directions get the currently playing track. It's not the actual track.
Except that doesn't actually work in the scripting bridge. Remember I mentioned that it was lazy? This actually sends the exact same events as the previous one and doesn't work for the exact same reason, because iTunes' current track is really just a reference. It's just the directions get the currently playing track.
And to do that, you add this one extra call, get. So that tells iTunes, go get me the real specifier for this object, something durable. And what iTunes will give you back is a track by ID. That will always refer to the same track, unless the user actually deletes it, in which case there's not much you can do. So get to override that lazy behavior. If you need to force evaluation right now, that's the thing to do.
The next tricky bit is that in a Scriptable Application, objects have to exist in a container. When you're using an NSArray normally, and you have direct control over the objects, you create something and then you add it to a container. But in a Scriptable Application, you can't have an object that's just kind of floating out in space somewhere. It has to actually belong to a container.
But at the same time, we wanted to make it look like an NSArray so you can use the same patterns. So what we did was we cheated. When you say alloc init with property dictionary, it doesn't actually make a new object right then. It just makes a little proxy, which is the properties to make it with. And then when you actually add it to a container, then it makes it.
But until then, the object doesn't really exist. And you can't do anything with it other than add it to a container. The corresponding thing is that on the flip side, when you remove an object, that'll typically wind up deleting the object. It doesn't just remove it from the container.
And lastly, there's the disclaimer. The basic problem with ScriptingBridge The basic limitation is that we're limited by-- it's all based off of what the application says is their scripting dictionary, the things that they say they can do. So that's not actually what that says. More succinctly, garbage in, garbage out.
If the application lies or is somehow otherwise wrong about what it can do, either it doesn't say that it does something that it can, or it does say that it does something that it can't, we're kind of stuck for it. We'll still generate an Objective C header that matches the scripting dictionary, even though it's wrong. The solution is to have them fix their dictionary. Also, if the application has just a weirdly written scripting interface, the Objective C interface is going to be just as weird. It's just that it's in Objective C now.
So to sum up, you still have the choice of APIs that you had before. You can use the low level-- use OSAKit if you want to edit scripts, use NSAppleScripts if you want to let the user attach their own scripts. And you can still use NSAppleEventDescriptor if you want. It's still there. There's nothing wrong with it intrinsically. But you'll probably be a lot happier if you use ScriptingBridge instead.
Moving on to Python and Ruby, two wonderful popular scripting languages. They're installed on the system. Lots of interesting things going on around them. I hear there's a-- I believe there's a talk on writing full Cocoa applications in Python and Ruby on Thursday, I believe it is. And they actually both have their own scripting-- control scriptable applications interfaces already.
But there are certain problems with them, so we're working on improving them. For Python, the existing one is gen-suite module. It's part of the standard distribution. development on it is actually frozen. They're not doing anything more with it. Because the interface that Gensuite module uses, the stuff that you have to actually write, is not very Python-like.
It's really more like AppleScript with a strong Python accent. So we're replacing that, or at least somebody's replacing that. The replacement is actually a third party solution. You can actually get it-- this is available now. It's been available for a while, in fact, called Apps Script, written by a very good fellow named Hamish Sanderson. It's open source, so you can examine it all if you like. There's the URL for it. We will be installing this as part of Leopard. And our current track example, this is what it looks like in using Apps Script. Again, fairly obvious.
So for Ruby, the current situation is a little more complicated. The existing one is called Ruby AE-OSA, which is written by Hisakuni Fujimoto, who I'm told is in the audience, if you would care to stand up and take a small bow, because it is very cool. Thank you.
So Ruby AeoSA is actually very good at what it does. The problem is that what it does is that it lets you send raw Apple events and lets you run scripts. But it doesn't hit that sweet spot in the middle like scripting the bridge does. So we talked about this, and we agreed to deprecate it in favor of a new thing that we are writing.
and I are here today to talk about a new application called Ruby OSA. It's being written here at Apple, but we're still making it open source. You can go check it out now. It's on a public repository. And we'll be installing this with Leopard. So similar to Python, this lets you use sort of normal Ruby conventions to write your code. And so this is, again, what Current Track looks like there. So that's all very nice. But once again, talk is cheap. Demo time. Adam, if you would please. ADAM GALPIN: Thanks, Chris.
So I thought I'd do the same example that I just showed you in Objective C, just using Python and Ruby to do, well, basically exactly the same thing. So let me show you some simple scripts that we wrote. Start with the Python one for pausing the current iTunes track.
As you can see, this is extremely simple. First line, we just import Apps Script, and then we get iTunes and send it the pause method, and that's it. So if we have iTunes running, And then switch over to the terminal. You can type Python, and then drag in our script.
Magic. Now let's say we wanted to fade back in the playback. So why not use Ruby for that, just for a little change of pace? Here's the Ruby script. We require RBOSA for controlling it. And then here we just get iTunes, similar to Scripting Bridge. Set the original volume. Set the current volume to zero.
Start playing. And then again, in here, loop up, increasing the volume in 16 intervals with a tenth of a second in between. And finally, setting the volume to what it started as. So let's just bring this up so that we can see the slider fade in. Shrink this down a little bit. And zoom in just so the people in the back can see. So let's say Ruby and then drag in our Ruby script and hit run.
Magic. So as you can see, Scripting Bridge isn't the only technology that you can use to control external applications besides AppleScript. Now you've got your choice of Ruby and Python as well. Thank you. Chris? Thank you, Adam. OK, so to sum up, please, control other applications. Make them do your work for you.
You get to be lazy. It's less work for you. It is probably a superior experience for your users. Remember the mail example? Why write your own mailer? Mail's already done it for you. They've done a far better job than you probably really want to take the time to do. So make them do your work for you. It's all there. and the rest of the world.
You can use it. And you can use your preferred language to do it. Whatever language you're currently using, stick with it. You don't have to shell out to AppleScript. You can use the appropriate bridge from your language. For AppleScript, you know, you just keep using AppleScript. For Objective C, you use scripting bridge.
Python, Apps Script, Ruby, Ruby OSA. If you've got other favorites, let us know. That's what Q&A is for. And that's it. Oh, yes, more information. You can contact our product manager, Sal Segoyan. My boss is Todd Fernandez, if you wanted engineering questions. And for documentation, we actually have documentation already. Thank Adam again.
It's up on the developer site. It's basically a summary of this session, plus all the sample code with actual text there, plus some additional details that we didn't cover. There is a lab tomorrow, which is specifically for this stuff and any AppleScript-specific questions that you have. I will be there. Most of the AppleScript team will be there.
Another thing that I should recommend, if you are new to controlling Scriptable Applications, you know, you've never actually done it before, but it seems kind of interesting. Writing Automator Actions is actually a very, very good way to get started, because Automator Actions, they're small, they're compact. You can actually do something useful in an hour or two.