Application Technologies • 41:50
The easiest way for your application to work with Automator is for it to be scriptable. Most of the effort required involves defining your application's dictionary, which contains the various commands and classes accessible to AppleScript. Learn how to design a dictionary for a sample application, how to define a well-designed scripting interface for your own application, and how to specify the dictionary in the new sdef format.
Speaker: Chris Nebel
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Welcome to Designing a Scripting Interface for Mac OS X. I'd like to introduce Christopher Nebel, Senior AppleScript Engineer. Thank you, Chris, for coming to talk to you about this subject. All righty. Like John said, I'm Christopher Nebel, AppleScript in Automator Engineering. Welcome to session 103, Designing a Scripting Interface. If this is not the session you plan to attend, now would be an excellent time to leave.
Matt Dash for the Exit is finished. I should point out that this session is actually one of a series. Today is designing a scripting interface. Tomorrow -- so when you're done here go home and think about what you want to design. Come back tomorrow morning and there's a session on implementing your scripting interface. You can make it do something aside from just sit there and look pretty.
And then if you still have questions there's a lab later that same afternoon so we can answer all your questions there. Now I'm going to have to apologize. I'm going to pull a bit of a bait and switch on you. This is supposed to be a hands-on session but because of some snafus which were at least partly my fault we don't actually have the sample code here. So it's going to be more of a guided tour. You get to kind of follow along. You might have a few extra questions at the lab but there you go.
"So, designing a scripting interface. What you'll learn, you know, what the heck is a scripting interface, what should it look like, and the hands-on, virtually speaking, how to actually write one, how to write it down in sdef, which is the modern, you know, The file format for specifying a scripting interface. That screenshot you can barely read right there is what it actually looks like to a user. This is a screenshot of the dictionary viewer in Tiger.
So keep talking scripting interface, scripting interface, scripting interface. Great. What's a scripting interface? So presumably everyone knows what a graphical user interface is. It's all the windows and menus and buttons and whatnot that a user with a mouse and a keyboard can push to make your application do stuff.
So a scripting interface is simply the bits of your application that are exposed so a program can control your application to make it do stuff. It's an API for your application. looked at it in a slightly different way. It's the objects and commands that your application exposes to the rest of the world, exposes to other programs so they can control you.
If you look at it in Model-View-Controller terms, Model-View-Controller is a common Cocoa design pattern, originally came from the small talk world. It's a way of segmenting your application into the model layer which is sort of the things your application considers real, the view which is presentation, and controller mediates between the two. It's simply another view onto your model objects. Now, the interesting thing about a scripting interface is that while the graphical view involves a lot of abstraction and presentation logic, a scripting interface's view will map very directly onto your model. It's a very, very thin translation layer.
If you think of it in implementation terms, a scripting interface is simply an sdef, or something you derive from one. There are a couple older scripting interface formats that you can derive from an sdef. Now you will notice that so far I have not mentioned AppleScript. That is because scripting interfaces are not merely useful for AppleScript. They can be used by other scripting languages, they can be used by other applications, they can be used by Automator actions. So if you are not real keen on AppleScript, we don't care. It has lots of other purposes besides.
Speaking of which, why would you want one of these things anyway? Well, we've built up a number of reasons over the years. Obviously, the first and foremost is so people can build automated workflows using your application. Put together a long task, they can push one button and get a whole bunch of stuff done. You can use it yourself, in-house. You can use your scripting interface to test your model harder, faster, and more repeatedly than you ever could using graphical interface.
You can use it to interoperate with other applications. I mentioned that other applications can use your Scripting Interface. Mail and iPhoto have this kind of relationship. That iPhoto can take photos out of mail messages and put them into iPhoto using iPhoto's Scripting Interface. And iPhoto uses Mail's Scripting Interface to send photos to people.
You can use it to save yourself some work, frankly. Not add so many features. Everybody's got one raving, lunatic user, actually usually more than one, who just has to have this one feature that's like, well, but you're the only person who cares. So, well, if you have a scripting interface, you can quite often write this guy a script or get someone else to write it for him, or he can do it himself.
And now he's got his feature. He's happy. And you didn't have to write a new feature for him. Now, if suddenly you find a whole bunch of users all raving for the same thing, well, congratulations. You've just identified a new feature for yourself. And you've got your users to do it for you.
And finally, you can make your application quite literally indispensable to your users. Remember those automated workflows? Once somebody has one, and the larger the better, and they've got it working, they don't want to give it up. Your application is now an integral part of their job, and they're going to really, really resist switching away from it.
And finally, in Tiger, we've got one more reason, which is that a Scripting Interface is really the best way to tie your application into Automator. Writing a scripting interface for your application is a lot easier in almost all cases than writing a linkable C API to your application.
So now you know why, let's talk a little bit about how. There are, despite the fact that this is API, you know, it's code, people think, oh, you know, it's code, whatever, there are no standards for that. Well, think again. There really are design standards for these things.
Let's talk about some of them now. The first one, the biggest one, is know your model. That's model in model view controller terms. Scriptability is really all defined in terms of model scripting, not view scripting. People make this mistake sometimes. They think, oh, obviously you just click menus, you do buttons. You imitate the things that you do in the graphical user interface.
And they say, well, why wouldn't you? Because then I don't have to do anything. Well, that's true, but the end result doesn't work very well. View scripts are rather difficult to write, and they're very difficult to read. They're also just kind of brittle. People rearrange their views all the time, move menu commands around and whatnot.
And as soon as you do that, all your view scripts break. The model tends to be a bit more stable. What your application does is relatively constant. So model scripts will stay working longer. And the other thing is that Automator and AppleScript are both very, very different. They're both based around model scripting.
There are a number of features in AppleScript that really don't work very well if you don't have objects to talk to. And Automator itself is based on the notion of application objects flowing from action to action to action. Well, you can't really do that if you don't have any.
So, a little example here. So at the top we have--these are two scripts that essentially do the same thing. The top one uses view scripting. So, quick, what does a script do? Well, you really don't have much of an idea. It's like, okay, select something, type something, select a menu item, and okay.
And obviously if this menu item moves somewhere else, then okay, we're in trouble. The bottom one is the model scripting version. It's set the myCard property and address book to me, person Chris Nebel. That's myCard. So it's a lot more, obviously a lot simpler to read and write.
Next up, know your users. This has two different aspects to it. The first is that your scripting interface is -- you should think of it in terms of talking to users, not your fellow implementers down the hall. So any object or command that shows up in your scripting interface, its purpose should be immediately apparent to any normal user of your application. They should know what it is. Ideally, you know, the terms in your documentation in your graphical user interface and your scripting interface should all match.
So avoid any internal only implementation objects. To the extent that your implementation objects are your model objects, then that's fine. Go ahead and use them. But if you have a model concept that's actually represented by two or three different classes, then don't reveal those two or three different classes. Make a facade that just shows the one model object that users will actually recognize.
The other aspect to know your users is simply know what they care about. Some applications are very large. You've got a humongous application you look at and say, "Oh my God, making this thing completely scriptable would just take forever. We're not going to do any of it." Right identification of the problem, wrong solution. What you do is you know your users, you find out what's most important to them, make those things scriptable first, stage the rest, some things you might be able to put off forever.
So this is an example of the first aspect. So at the top we have a bit of the iTunes dictionary. This is the top-level application object, which contains everything, eventually, that iTunes knows about. And looking through the various sub-elements, I can see that I recognize most of these things. There are various kinds of windows, encoders.
The fact that there's apparently an arbitrary number of EQ windows is a little odd, because I happen to know there's only one of them. But there's this one called Sources. And this one's kind of weird, because the word Source appears in exactly one place in the iTunes interface.
It's at the top of that left-hand column. And the things that the scripting interface tells me are sources do not really match what's in that column. This is just kind of funky. A more sensible version is down below. This is iPhoto, which very sensibly says, "I contain albums, keywords, photos, windows. I know what all these things are if I'm a normal iPhoto user. No problems." And they all have the obvious relationships to each other, too.
Third thing, be object-oriented. This might seem a little obvious now in 2005, but people still make this mistake. Your scripting interface should really be organized around -- well, first off, you should have some objects to organize things around. And, second, the objects are the focus, not the commands in most cases.
This has a number of interesting effects. One, it's generally easier to understand because most applications are really organized around the objects in the first place. Those are the things that are really important. You have objects, you operate on them. The other thing is that it gives you a smaller and simpler interface that's still just as powerful because you get this multiplicative effect. You have objects times verbs where to do it with just verbs you need to list all 300 of them separately.
And the last thing is that you can exploit features of certain scripting languages, notably AppleScript. Like I said earlier, they don't work real well without objects to talk to. Things like tell blocks and whose clauses. These are very, very powerful features of AppleScript. But again, they don't work without objects. And the corollary to this is you should only define new commands when you need to.
There's a standard set of commands to cover basic object operations: get, set, make, delete, add, remove. So use those where they make sense. If you have something that genuinely doesn't fit in, for instance, mail has a send command that has no relation to the standard set. So that one's fine. But use the standard set where you can. Some applications, and in fact the one we'll be looking at is kind of like this, really that's all they need. Their entire application is just based around manipulating object properties, making and deleting them.
That's really all they do. So an example here, this top one comes from the old Mosaic browser suite years and years ago, so I suppose they can be forgiven now. They defined a closeAllWindows command. It's perfectly obvious what this thing does. It closes all the windows. But they didn't really need to do this, because there's already a window object which responds to close messages. And what do you need with closeAllWindows? The fact that you can close all windows just falls out of normal object addressing syntax.
"Thing 4: Don't re-invent stuff." Again, this means reusing the standard stuff. You can steal stuff from yourself, too. If you've got two objects that are mostly like each other, hey, you can use inheritance. Everybody knows what inheritance is. Feel free to go ahead and define abstract base classes. That's a case where things that the user doesn't see directly is actually okay.
The other part is, you know, steal stuff from others. Imitate similar applications. Take a look at other applications that do roughly what you do and look at borrowing their model terms and how they organize things. So, you know, you're doing text processing, take a look at text edit, you're doing personal information management. Look at address book.
And so on. Okay, so to sum this all up into a checklist, I've been talking a bunch about different design principles. These are all written down along with a lot of other more detailed stuff. In Tech Note 2106, Scripting Interface Guidelines, write that number down. The next thing you're going to need to do is actually think about your application. You need to identify what are your model objects, what are their attributes, how do they relate to each other.
Who contains what? You identify your unique operations, the ones that don't fit into the standard suite, and then go ahead and design your entire scripting interface. I mean all of it. Ideally you should do this before you even write any code. It actually helps in writing the code.
If you need to, phase the implementation. If you've got a large enough application, you don't have to do it all at once. It's perfectly okay to do less than your graphical user interface. A funny thing here is that it's also okay to do more than your graphical user interface.
Scripting is actually a really good way to expose functionality that for some reason you don't want to put in your graphical user interface yet. You don't have the time, you're trying to keep things simple. The scripting interface can be a bit of a backdoor to this. For instance, Address Book added scripting interface fields for, what was it, nickname and prefix, I believe it was, one release before they added any graphical user interface for them.
Last step, write it all down in sdef. That's what the guided tour is all about. And follow the guidelines. Consistency is power. A big part of the Macintosh graphical interface and why it was so powerful, or why it is so powerful, isn't just that it's graphical, isn't just that people can, you know, it's C and point instead of remember and type. It's that it's consistent.
Things that you learn in one application work in another. Things that look the same work the same. Things that work the same look the same. The same consistency is equally powerful, if not more so, in scripting tasks because the whole point is to be tying different applications together. You'd like them to behave as consistently as possible.
In fact, probably the primary complaint of just about any scripting user about any scriptable application these days is that it doesn't work like any other scriptable application, or at least has some oddities to it. So please, follow the guidelines. If there are parts of the guidelines you find incomplete, ask us.
Okay, so now we'll move on to the guided tour. Obviously, I didn't fix my slides. So we'll see, you know, adding a basic sdef to the application. We've got a skeleton one we start from. We can include all the standard stuff. We're going to define a couple of application-specific classes, show an inheritance relationship, adding a property, hook up an element relationship between the two.
We're going to add a couple of commands, and finally we'll look at a little shortcut if you've actually done some of this work already, but not necessarily in an sdef. And tomorrow morning we will be implementing most of the same stuff. Right now we're just going to do the interface part.
If I could switch over to demo one, please. Thank you. So for this example, we are going to be using Sketch, a venerable Cocoa sample. If you need to get it yourself, it's in the standard developer examples. Developer, examples, app kit, sketch. For those of you who haven't seen it before, it's a simple drawing program.
[Transcript missing]
"I'm going to slap various colors on things." You know, standard stuff. It's really there more to be a sample of Cocoa Document application than to really be a great artist tool. So we're going to add, we're going to make this thing scriptable. So I'm going to have the Sketch project open here already.
So the first thing we're going to do is add the, kind of a skeleton sdef. This contains all the standard stuff, and this is the thing that I was supposed to have on the server for you, but don't. This should be available on the attendee website by the time the conference is over. Failing that, we'll have copies at the lab tomorrow, so come grab one then if you want. And the main thing is just sticking in the Resources group. "And that's that. So let's have a look at this."
So an sdef is simply an XML document. It is not a plist, mind you. It has its own structure. "Your plist editing tools will not work on this." We're actually going to be doing all of this hardcore by hand. There are third-party applications out there to edit these things in a more graphical form.
The other thing we're going to need to do first is tell the runtime that this thing actually exists. We're going to do that by adding an extra key to our plist. OSAScriptingDefinition = a string sketch.sdef. So this is simply-- oh, I forgot. Didn't I? I need to rename this bad boy. Okay, there we go. So now the InfoP list is hooked up. This actually triggers the runtime machinery to read the thing and make it actually work.
So now that we've got the standard suite, like I said, this defines all the standard verbs. But not everything in here will actually apply to all applications. For instance, if your application, you know, you've got some tiny little control panel-ish thing, doesn't really print anything, you should remove the print verb.
Everything that shows up in here will be reflected in your dictionary, so remove all the bits that don't actually apply to you. In this case, there's really only one that we need to get rid of, the as parameter for save, because Sketch only has one file format that it can ever save anything in, so the as would be fairly pointless.
So that's zapped. The other thing we'll need to change in here One of the things about an sdef is that it contains not merely the interface information, it also contains some implementation information about how these interface classes hook to your actual implementation classes. So one of those things is, well, what's the actual backing implementation class for, say, a document? We actually do define our own document class. So change that NS to SKT. Draw a document.
Okay, so that's all the standard stuff. So the next thing we're going to do is add what's called a suite for all of our sketch stuff, the application-specific stuff. Now, you might well ask, "What's a suite?" Suites are just organizational blocks to hold related classes and commands. There's no particular technical reason to have them. They're really just for the user's benefit to help them find things. So there's no technical limit, upper or lower, on how many things you can have in a single suite. It's simply a matter of taste. Order things in a way you think makes sense.
So it's traditional to have at least one suite for your application. So we can add that now. And it's fairly straightforward, a suite element. You give it a name, Sketch Suite. Everything in a Scripting Interface will have a four, or in some cases, eight character code associated with it. This has mostly to do with how AppleScript works, and it's kind of a historical architecture thing. And you can add a simple description along with it. The description is optional.
Okay, so we have a perfectly reasonable skeleton here. Save that. And, well, what are we going to do with this now? What are we going to add? Now, ideally, you should be designing your scripting even before you write any code. It actually helps a lot to design your model and make sure it actually makes sense and generally plot out your implementation before you do these things. Now, that's ideally, of course. Practically speaking, you're probably working on an existing application. You're retrofitting scripting into it. So you have to actually analyze your existing model classes.
Now, a well-designed implementation will actually align quite nicely with your scripting interface. There'll be a one-to-one match. This, you know, doesn't always happen, but... So we're going to pretend that I'm the new guy who's just been assigned this task. I have to figure out the bloody thing in the first place. So we're going to take a bit of a shortcut here. We're going to use the xDesign modeling tools. Get us a little model. Helpfully, they've already separated the model classes into their own group. So click Model.
[Transcript missing]
Okay, this is probably kind of hard for you to read, but there are three big clusters here that you can see. There's this one, which is NSView and an SKT rendering view. This is internal implementation. A user will not know what this is, so we don't care about this. Over here, we have NSDocument descends SKTDrawDocument, and then there's a category sticking on here. And we'll notice that SKTDrawDocument contains--it has a graphics attribute.
Over here is the big cluster. This is the really interesting one. An SKT graphic, which is sort of the generic shape, and then there are all these specific ones hanging off it: image, text area, line, circle, rectangle. A graphic defines pretty much all the properties of There are also some implementation properties that we won't reveal, things like a back pointer to the document, G flags, a ridge bounds, which I presume has something to do with undo. This cluster here is obviously a good place to start.
[Transcript missing]
So our first thing will be to add the Graphic class, because that's the common ancestor for all the shapes.
So again, it follows a reasonably familiar structure: class, name equals graphic. The name is always the name that the user is going to see. Again, the optional description and the four-character code. Inside that, we have this line, a Cocoa element, which specifies the implementation class that represents a graphic.
And then a variety of properties. Now notice that these do not map exactly to the implementation properties. For instance, in the original there was simply a bounds field, which was a rectangle. Here it's been split out into four different properties: an x position, y position, width, and height.
[Transcript missing]
So now we've got a Graphic class. We need to actually hook it into our document. So we saw earlier that documents contain graphics. So to do that, we should just add an element element. So in the document class, it says element type equals graphic. And again, there's some key value coding magic going on behind the scenes here. It knows to call the graphics accessor.
So now we've got our common base class. Let's add one of the specific ones. I'm going to go for the boxes. So this one is extremely simple because it turns out that boxes don't define any of their own attributes. Everything they need they inherit from the graphic class. So we simply have name equals box.
You can define a plural name if it doesn't match just tack an s onto the end. In this case, box is code. This is how you specify inheritance in an sdef. You just say inherits equals and then the name of another class somewhere else in your sdef. Order is incidentally irrelevant. There's no declare before use rule. It looks at the whole thing and figures out all the linkages. and the Cocoa class. And like I said, this, according to the existing sketch, is done. This is all we have for the Box class.
Well, let's go into a little more detail. We decide that something might be interesting to have via scripting is an orientation property. Is the box landscape size, or is it portrait size, or is it square? So we can add that easily enough with a property, again, name, code, and now we have a type. And just, hmm, type, orientation, that's new. Well, that's because I haven't added it yet. Orientation is going to be one of our own types.
It's an enumerated type. This is one of the detailed design rules I didn't mention. It's considered bad taste to have a property which is simply a list of numbers where each of the numbers means something. You should actually use an enumerator, and then your different options can have real descriptive names so people don't have to remember the numbers. So in this case, our possible orientations are portrait, landscape, or square.
And since this is merely designing the interface, that's all we're going to do. Oh, I forgot one thing. Because we'd also like to be able to ask about... "Not merely graphics of a document, but also boxes of a document. We need to add that element of relationship too."
So again, here's the key value coding override I was talking about. In the implementation, boxes are actually called rectangles, so that's what the accessor wound up being named. So we have to override that with, again, one of these Cocoa elements. So that's it for our objects, at least as far as we're going to do here. So now let's try for a command. Now, just about any graphics program will have a rotate command. So let's try for that one.
So again, the familiar name, code, description. The code for verbs, again this is kind of for historical reasons, is eight characters, not four. But aside from that, it's the same pattern. The things for a command are a little different, naturally. A command has what's called a direct parameter. Yes, this is the same thing as a direct object in a sentence that you are possibly having bad flashbacks to your grammar school days about.
So in this case, rotate takes a direct parameter which is of type graphic. You can rotate any of the graphic objects. And you need a specific parameter which has, again, a name, code, in this case, type, which is the number of degrees to rotate it. If you're implementing this in Cocoa, your parameters will come to you as an NSDictionary, so you need to tell it the key name to use for that dictionary. In this case, we're going to call it "by degrees."
Now, skipping ahead slightly to implementation, there are kind of two different ways you can do commands. There's object-first dispatch and there's verb-first dispatch. And they're useful for different kinds of operations. In some cases, it doesn't really matter how many objects you send the message to, because they'll all do it themselves. Rotate is basically like this, that each object should rotate itself by 45 degrees or whatever.
So, in that case, Cocoa can do object-first, that it sorts out all the objects and then sends each of them a rotate message. Now, sometimes this doesn't work. Sometimes you need to operate on the whole collection collectively, for lack of a better word. So, in that case, you do verb-first dispatch, and we'll see an example of that in a little bit.
In this case, for object-first dispatch, now that we've got the command, we have to specify what objects respond to it. So we can do that by adding a "RespondsTo" to the Graphic class. So we say "RespondsTo" the rotate command, and the Cocoa method to call when you see this is "Rotate:".
So now let's have a look at an example of VerbFirst. There's an align command in Sketch. And this is a perfect example of something that doesn't make sense to send to each object individually. Because the whole point of align is that you're aligning a set of objects to each other. It wouldn't make any sense if you just sent a line to a single object. It would have to-- it either wouldn't know what to do, or it would have to go out and find all the other objects, which would be kind of a waste of its time.
Much simpler to make Cocoa do it for you. So in this case, you can see the name of the command, description, direct parameter. In this case, align, we're being specific that it's always going to take a list of objects, because aligning a single object would be sort of pointless. So the way you specify a list is with a type sub-element. You can say type equals graphic, list equals yes. So this will show up in the dictionary as a list of graphics.
Oh, and again, we're using a parameter with an edge type. We need an edge to align these things to, another enumeration of ours, which we should add here. We have the left, right, bottom, and top edges, and also the horizontal and vertical center edge, as it were. Now, for verb-first dispatch, what Cocoa is going to do is it is simply going to look at the verb, and you define a class to handle that command.
By default, everything just goes to NSScriptCommand, and that knows how to do the object-first dispatch. If you need to do verb-first dispatch, you define your own descendant of NSScriptCommand. In this case, we're calling it skt-alignCommand, and that has a method that it overrides, which will get all the objects, and then it can do the operation. So to do that, to specify that override, again, you use a Cocoa element. Say Cocoa class equals SKT align command.
Okay, so now we've got all this. I should be able to build Go. Yeah, I don't really care about that, but let's save it anyway. Yeah, yeah, sure. Yeesh. Should have said no. Okay. So we've got Sketch running. And, oh silly me, I actually have to work for this one. Fire up Script Editor here. "Proversely enough, this is the one thing that I didn't have in Demo Monkey." Okay, tell application sketch This is an example of using the Objects and Tel Blocks. This lets me abbreviate object specifiers.
[Transcript missing]
So this is simple script to just slap in some random colors. And you'll notice that-- remember that close all windows example? And I said that, well, you don't need a close all windows command. The fact that you can address all windows just kind of drops out of normal object addressing syntax.
Same deal here. I could change the sum to every. Or I can use a random one. I could name a specific one. And the scripting language actually takes care of this for me. All I have to worry about is defining a graphic class. and the language will actually take care of the rest.
A little overenthusiastic there with the copy and paste. There we go. Whee. Look at the colors. Okay, now those of you who have actually worked with Sketch before might have noticed something a little funny, which is that, you know, Sketch is already scriptable. It had its scripting defined in the older Cocoa Suite definition files, ScriptSuite and ScriptTerminology pairs.
So you might well ask yourself, "Geez, why do we have to write in sdef by hand?" Well, there's a very pleasant answer to that, which is, you don't have to. There's a tool on the system. It's new in Tiger. It didn't exist before. It's actually a command line tool called dsdp. There's an older SDP tool which takes sdefs and turns them into some of the older formats. So this does the reverse, hence the DSDP. So you can simply point it at a built copy of the application.
[Transcript missing]
Here's everything that it derived from our old scripting format. It works on AET applications just as well. That's the old Carbon format. So you take all this and base your scripting on it. Now, the thing about DSDP is that the result that it produces, it's going to need some editing. It's not a complete solution. The main reason for that is simply that sdef is more expressive than either of the old two formats.
So, for instance, the Cocoa Script Suite script terminology files can't actually specify the order that things come out in. Sdef can. You can have more elaborate comments than you could in either of them, things like that. But especially for a larger application, it gives you a great starting point and saves you a lot of work.
I forgot about all of these. I should have been skimming through. Sorry, these were my notes to myself, which I was watching on a... : So theoretically, at this point, you've learned what a scripting interface is, why you would want one, some of the design guidelines and where to find rest, and some of the basics about how to write an sdef. There's more complete documentation on the format. Some of it's in the Cocoa documentation. There's also a man page for the sdef format itself, just say man sdef. And if you've done some of the work already, you've already got a Scripting Interface, how to leverage some of that.
So, some more information pointers. Man, I was really flying on this one. So, related sessions, I mentioned these at the beginning. Obviously, tomorrow morning, bright and early, 9:00, implementing a Scripting Interface. I'll show you how to actually hook some of this stuff up and make it do something.
And then later that afternoon, if you've got any questions left, come to the lab. Like I said, I'll have copies of the skeleton sdef there for you if you want to take that home and start playing with it. People to contact: Our esteemed Marketing Manager, Sal Segoian. I'm sure he's around here somewhere. And Todd Fernandez is the Engineering Manager for the AppleScript and Automator group.