You’re looking at a draft of a chapter from a work in progress, tentatively titled Scripting Mac Applications With Ruby: An AppleScript Alternative, by Matt Neuburg.
Covers rb-appscript 0.6.1. Last revised Jun 23, 2012. All content ©2012 by the author, all rights reserved.


Prev: Commands
Next: The Dictionary
Contents

Chapter 7: Datatypes

1. Raw Data

2. Boolean

3. Numbers

4. Dates

5. Text

6. Files and Folders

7. Lists

8. Records

9. Units of Measurement

In the world of a scriptable application, values have a class_, such as :string or :integer. In the world of Ruby, objects have a class, such as String or Integer. Whenever data travels between rb-appscript and a scriptable application, in either direction, some sort of transformation must take place. In every case, there is a correspondence between a Ruby class and an Apple event class_, along with rules for transforming data from one to the other. That’s what this chapter is about.

This chapter is not about values that travel back and forth between Apple events and Ruby as object references, as discussed in Chapter 5 in the section “Copies vs. References”. We already know how references work. No matter what the scriptable application may claim is the class_ of such a value, when it arrives in the Ruby world it is transformed into an Appscript::Reference.

itu = Appscript.app("iTunes.app")
p itu.current_track.class_.get #=> :file_track
p itu.current_track.get.class #=> Appscript::Reference

What we’re interested in are values that travel back and forth as what Chapter 5 calls copies. For example:

itu = Appscript.app("iTunes.app")
p itu.current_track.name.get.class #=> String

In the scriptable application’s world, the name of the current_track is a :string; in Ruby, it’s a String.

Ironically, it is quite difficult to find out directly what a value’s class_ is in the Apple event world. If you ask a scriptable application for the class_ of a property of this sort, the answer is :property. The only reliable way to learn the Apple event-world datatype of a value is to intercept the Apple event as it is sent; every value is accompanied by a four-letter code stating its datatype. For example, in the Apple event that we intercepted in Chapter 3, we see these lines:

key 'seld' - 
  { 1 } 'long':  4 bytes {
    1 (0x1)
  }

This means that this 'seld' is a 'long' whose value is 1; a 'long' is an :integer. Similarly, we see these lines:

key 'seld' - 
  { 1 } 'TEXT':  7 bytes {
    "Library"

So this 'seld' is a 'TEXT' whose value is "Library"; a 'TEXT' is a :string. Every piece of data in an Apple event is marked like that. But to use this approach, you have to intercept the Apple event, and you have to know the meanings of the four-letter codes. You can discover the correspondence between four-letter codes and class_ names by looking in defaultterminology.rb, one of the rb-appscript files; and in this chapter I will cite lines from this file as appropriate.

More commonly, though, you’ll just consult the scriptable application’s dictionary, which tells you what sort of data is expected (though sometimes inadequately or inaccurately).

1. Raw Data

Some types of data cannot be converted to a standard Ruby class, and will appear instead as an AE::AEDesc instance. Here, for instance, a scriptable application has sent us the data constituting a TIFF image:

p tiff #=> #<AE::AEDesc type="TIFF" size=321510>

Raw data of this sort remains useful for passing back and forth via Apple events. If you know how to deal with the data directly, you can fetch it with the data method. Here, for example, I write out the TIFF image data as a file and open it:

f = "/Users/mattleopard/Desktop/mytiff.tiff"
File.open(f, "w") {|io| io.write tiff.data}
`open '#{f}'`

In the same way, you can create a raw data value to send to a scriptable application, using AE::AEDesc.new. You supply a type (listed in kae.rb) and the data. Mostly you’d do this to prevent rb-appscript from treating your data as a string, which it would do by default. In this example we read a .jpeg file from disk and use it to set the artwork on the currently selected song in iTunes:

data = File.read('/Users/mattleopard/Desktop/Tchaikovsky.jpg')
raw = AE::AEDesc.new(KAE::TypeJPEG, data)
itu = Appscript.app("iTunes")
sel = itu.selection.get()[0]
sel.artworks[1].data_.set raw

2. Boolean

Booleans ('bool' => :boolean) correspond to Ruby booleans (true or false).

3. Numbers

The most common Apple event numeric types are integer ('long' => :integer) and float ('doub' => :float). These are mapped to the Integer subclass Fixnum (or Bignum, if necessary) and Float, respectively.

Thanks to Bignum, Ruby can represent integers that are far larger than an Apple event can deal with; naturally, rb-appscript will convert as necessary. But in general you should try to stay within the Apple event limits; the largest Apple event integer is 536870911 positive or negative.

Other numeric types are rarely used, and most of them are virtual dead letters:

'shor' => :short_integer, # occasionally used
'long' => :integer, # common
'magn' => :unsigned_integer,
'comp' => :double_integer, # occasionally used

'fixd' => :fixed,
'lfxd' => :long_fixed,
'decm' => :decimal_struct,

'sing' => :short_float,
'doub' => :float, # common
'exte' => :extended_float,
'ldbl' => :float_128bit

Here’s a rare instance where a number is neither an :integer nor a :float:

f = Appscript.app("Finder.app")
sz = f.startup_disk.size.get

In that example, sz is sent to us as a :double_integer, and is represented in Ruby as a Bignum.

4. Dates

Dates ('ldt ' => :date) are mapped to the Ruby Time class. For example:

f = Appscript.app("Finder.app")
d = f.files[1].modification_date.get
p d #=> Fri May 01 13:30:47 -0700 2009
p d.class #=> Time

Behind the scenes, each type relies on a notion of seconds since a certain fixed moment, known as the epoch, but the epoch is different for each type. Unix dates are reckoned from midnight at the start of 1970; Apple event dates (technically known as LongDateTimes) are reckoned from midnight at the start of 1904. All this is adjusted for you behind the scenes, and you should be okay as long as you stick with dates fairly close to now.

(That caveat is intended to cover the fact that a LongDateTime is limited to whole seconds and knows nothing of such niceties as the invention of the Gregorian calendar, whereas the Ruby Time class handles fractions of seconds, and the Ruby Date class can be made to do all sorts of sophisticated things.)

5. Text

The legacy Macintosh text type ('TEXT' => :string) has MacRoman encoding. But of course Mac OS X uses Unicode, and it has taken Apple events and scriptable applications a long time to catch up with this change. There is now a Unicode text type ('utxt' => :unicode_text), which uses UTF-16 encoding internally.

When receiving text data from a scriptable application, there’s no problem; the datatype is known, and therefore so is the encoding. But what about when sending text data to a scriptable application? Officially, the old :string type is deprecated, so rb-appscript by default uses :unicode_text. There is an outside chance, though, that some scriptable application will be so old that it can’t deal with this.

For example, here we attempt to save an AppleWorks document while converting it to RTF:

aw = Appscript.app("AppleWorks 6")
f = MacTypes::FileURL.path("/Users/mattleopard/Desktop/testDocument.rtf")
aw.documents[1].save({:in => f, :using_translator => "RTF"})

(The second line, specifying a new document path, is explained later in this chapter.) The code runs and the document is saved, but not as RTF. The conversion has failed silently. The reason is that the AppleWorks :using_translator parameter has to be an old-fashioned :string. In a case like this you can generate the occasional :string yourself:

aw = Appscript.app("AppleWorks 6")
f = MacTypes::FileURL.path("/Users/mattleopard/Desktop/testDocument.rtf")
aw.documents[1].save({:in => f, :using_translator => AE::AEDesc.new(KAE::TypeChar, "RTF")})

More broadly, if you know that a legacy application will always accept old-fashioned :string text and that you will never try to send it a string that can’t be expressed in MacRoman, you can tell rb-appscript to send this application all strings as :string types:

aw = Appscript.app("AppleWorks 6")
aw.AS_app_data.pack_strings_as_type("TEXT")
f = MacTypes::FileURL.path("/Users/mattleopard/Desktop/testDocument.rtf")
aw.documents[1].save({:in => f, :using_translator => "RTF"})

A few other text class_ types crop up from time to time; others are virtual dead letters.

'TEXT' => :string, # the common legacy type
'cstr' => :c_string,
'pstr' => :pascal_string,
'STXT' => :styled_text, # sometimes used in legacy apps
'tsty' => :text_style_info,
'styl' => :styled_clipboard_text,
'encs' => :encoded_string,
'psct' => :writing_code,
'intl' => :international_writing_code,
'itxt' => :international_text, # sometimes used in legacy apps
'sutx' => :styled_unicode_text,
'utxt' => :unicode_text, # the current standard type
'utf8' => :utf8_text, # typeUTF8Text
'ut16' => :utf16_text, # typeUTF16ExternalRepresentation

The AppleWorks :using_translator parameter that we had trouble with earlier is described in the dictionary as an :international_text type, though it turns out to be reducible to a :string. The :styled_text type actually carries two pieces of information, the text plus a series of bytes intended to describe its styling (stretches of bold, for example); I say “intended” because in reality the style bytes were commonly misused to carry encoding information before Unicode came along. None of this will matter to you, though, because they are all coerced by rb-appscript to a UTF-8 Ruby String.

At the Ruby end of things, a String’s encoding will be UTF-8. That is, any sort of text that arrives from an Apple event will be translated into a UTF-8 String, and any String to be sent in an Apple event must be valid UTF-8 (unless you arrange to pack it in some other encoding, as we did in the AppleWorks example earlier). Dealing with this fact is up to you, but it has nothing to do with Apple events or with rb-appscript. Most of the time, it won’t even matter.

I use TextMate and Ruby 1.8.6; in Ruby 1.8.6, a String is just a sequence of bytes, and TextMate expects strings to be UTF-8 — it’s a UTF-8 editor, and RubyMate, the component of TextMate that executes Ruby scripts, sets the command-line -KU flag. So there’s usually no problem. When I display a UTF-8 String in TextMate using puts or p, it displays correctly. When I type a String literal and send it to a scriptable application with rb-appscript, it is sent correctly.

The only time an issue does arise is when a transformation or calculation is to be performed on a String. For example, here’s a naive (and somewhat artificial) script that’s supposed to boldify the first occurrence of the word “test” in a Microsoft Word document:

mw = Appscript.app("Microsoft Word")
s = mw.active_document.text_object.content.get
s =~ /test/i
before = $`.length
mw.active_document.create_range(:start => before, :end_ => before + 4).bold.set true

That might work, but then again it might not. The problem is the use of length in the next-to-last line. The String length method in Ruby 1.8.6 merely counts bytes. But in a UTF-8 String, a character might occupy more than one byte. A safer way to do this, therefore, is to use the jcode library, specifying that String objects are UTF-8, and call jlength instead of length:

$KCODE = 'u' # not needed when running in TextMate
require 'jcode'
 # ...
before = $`.jlength # jlength instead of length...

Alternatively, if activesupport is installed (usually in connection with rails), you can use its multibyte support:

require 'activesupport' # might have to require 'rubygems' first
 # ...
before = $`.mb_chars.length # mb_chars.length instead of length...

In Ruby 1.9, on the other hand, a String has encoding information, and things like length are automatically handled correctly. For a splendid introduction to string encoding issues in Ruby, see the series of articles by James Edward Gray II at http://blog.grayproductions.net/articles/understanding_m17n. None of this has anything whatever to do with Apple events and scriptability, though, so I won’t say more about it.

6. Files and Folders

There are four chief Apple event ways to refer to an item on disk (a file or folder): as an alias, as a file reference, as a file specification, and as a file URL:

'alis' => :alias, # common
'fsrf' => :file_ref, # common, older
'fss ' => :file_specification, # very old, rare, deprecated, inadequate
'furl' => :file_url, # common, newer

Of these, the first is mapped by rb-appscript to MacTypes::Alias and the others are all mapped to MacTypes::FileURL. However, a MacTypes::FileURL is still a :file_ref, a :file_specification, or a :file_url under the surface. When you form a MacTypes::FileURL, you form a :file_url, but when you receive a MacTypes::FileURL from a scriptable application, it may be of one of the other two types under surface, and will still be that type if you subsequently send it to a scriptable application. You can find out which class_ underlies a MacTypes::FileURL instance by sending it desc.type.

Both MacTypes::Alias and MacTypes::FileURL support three useful instance methods for transforming them to a string:

To make a new MacTypes::Alias or MacTypes::FileURL instance from a pathname string, use these same names as class methods. You have to use one of them, not new, so that rb-appscript knows which kind of pathname string you’re supplying.

f = MacTypes::FileURL.path("/Users/mattleopard/Desktop/my pix")
 # ... or we could have said MacTypes::Alias.path(...)
puts f.path #=> /Users/mattleopard/Desktop/my pix
puts f.hfs_path #=> hume:Users:mattleopard:Desktop:my pix
puts f.url #=> file://localhost/Users/mattleopard/Desktop/my%20pix

Both types also support instance methods for generating a new instance of the same or the other type, to_alias and to_file_url.

There are two chief differences, from a functional point of view, between an alias, on the one hand, and the other types. First, an alias has the remarkable property that it continues to point to the item on disk even if the item is subsequently moved (not copied).

require 'fileutils'
f = MacTypes::Alias.path("/Users/mattleopard/Desktop/mytiff.tiff")
p f.path #=> "/Users/mattleopard/Desktop/mytiff.tiff"
FileUtils.mv("/Users/mattleopard/Desktop/mytiff.tiff", "/Users/mattleopard/mytiff.tiff")
p f.path #=> "/Users/mattleopard/mytiff.tiff"

Second, an alias cannot be formed to point to a non-existent item.

f = MacTypes::Alias.path("/Users/mattleopard/Desktop/nosuchfile")
#=> MacTypes::FileNotFoundError: File "/Users/mattleopard/Desktop/nosuchfile" not found

The other types can point to a non-existent item, though they cannot generally be used unless all the containing folders do exist. For an example, see earlier in this chapter, where we used a MacTypes::FileURL to tell AppleWorks where to save a document (and this works even if the document has never been saved).

If you need to send a MacTypes::FileURL to a scriptable application, it should not usually matter which of the three underlying types it is, since the scriptable application can call a system-level routine to coerce to a different type if necessary. However, in the unlikely event that a scriptable application fails to do this and absolutely refuses to accept the underlying type, you can obtain a MacTypes::FileURL instance with a different underlying type by performing the same coercion yourself. To do so, send desc.coerce to the MacTypes::FileURL instance, with parameter KAE::TypeFSRef, KAE::TypeFSS, or KAE::TypeFileURL.

7. Lists

A list ('list' => :list) is an ordered collection; it corresponds exactly to an Array, and is mapped to it. A reply very often consists of multiple values, and since a reply must in fact be itself a single value with a single type, a list is used to wrap them; many examples appear in Chapter 6.

Often a reply will be a list even if it consists of just one item, because the situation is such that there might have been multiple values. Unfortunately, you can never predict what a scriptable application will do. For example:

f = Appscript.app("Finder.app")
p f.disks.get #=> app("/System/Library/CoreServices/Finder.app").startup_disk

If I had multiple disks, the reply would have been a list (an Array); but since I have just one disk, the reply is a single Appscript::Reference object. This can be a frequent source of bugs in your scripts. A useful technique is to feed the reply to the Array() method immediately, so that it will be an Array no matter what, and the value can be treated consistently.

f = Appscript.app("Finder.app")
disks = Array(f.disks.get)

A number of other class_ types are actually some sort of list. For example, a :window will typically have a position property, which is a :point, and a bounds property, which is a :bounding_rectangle. The former is a list of two numbers; the latter is a list of four numbers.

f = Appscript.app("Finder.app")
p f.windows[1].position.get #=> [582, 112]
p f.windows[1].bounds.get #=> [582, 112, 1382, 520]

Similarly, a color (:RGB_color) is a list of three numbers.

f = Appscript.app("Finder.app")
p f.windows[1].icon_view_options.background_color.get 
#=> [65535, 65535, 65535], i.e. white (boring)

Of the various built-in list-of-numbers types, only those that I’ve just mentioned are common; the rest are so rare as to be dead letters.

'QDpt' => :point, # common
'qdrt' => :bounding_rectangle, # common 
'fpnt' => :fixed_point,
'frct' => :fixed_rectangle,
'lpnt' => :long_point,
'lrct' => :long_rectangle,
'lfpt' => :long_fixed_point,
'lfrc' => :long_fixed_rectangle,
'cRGB' => :RGB_color, # common
'tr16' => :RGB16_color,
'tr96' => :RGB96_color,

8. Records

A record ('reco' => :record) is an unordered collection of named values; it corresponds roughly to a Hash, and is mapped to it. The keys are symbols.

A bunch of values with names — hmm, that sounds like a class_ with properties. And indeed, a scriptable application will frequently use a singleton class_ in just that way. The problem is that, in a case like that, it is the scriptable application that “owns” the values; you have to keep going back to the scriptable application, with a new Apple event, every time you want a value:

f = Appscript.app("Finder.app")
p = f.Finder_preferences
p p.desktop_shows_connected_servers.get #=> false; an Apple event
p p.new_windows_open_in_column_view.get #=> false; another Apple event

If we want to know all about the Finder’s preferences, this could drive us (and the Finder) crazy. For this very reason, most class_ types support a properties_ property that returns a record of all its property names and values, in a single Apple event:

f = Appscript.app("Finder.app")
p = f.Finder_preferences.properties_.get
p p #=> {:desktop_shows_hard_disks=>false, :delay_before_springing=>0.668, ...}

I’m displaying only a few items of the resulting hash. The point is that now we have all the values, and can learn any of them by interrogating the hash, without sending an Apple event to the Finder. Do keep in mind, however, that this hash is static, with no magic connection to the Finder; if a value changes back in the Finder, our hash won’t reflect the change, and of course in order to change a value ourselves we still must send the Finder a set command.

Several scripting addition commands work the same way. The info_for command provides a hash of useful facts about an item on disk:

require 'osax'
p OSAX.osax.info_for( Appscript.app("Finder.app").files[1].get(:result_type => :alias) )
#=> {:long_version=>"", :displayed_name=>"add.png", :package_folder=>false, ...}

Similarly, the clipboard contents are sometimes reported as a hash:

require 'osax'
p OSAX.osax("StandardAdditions", "Finder.app").the_clipboard
#=> {:TIFF_picture=>#<AE::AEDesc type="TIFF" size=366812>}

A few applications use records in a similar way to reply to commands. A good example is BBEdit’s find_tag command, which looks for an HTML tag in the frontmost document and reports information about it:

bb = Appscript.app("BBEdit")
p bb.find_tag("title", :start_offset => 0)
#=> {:class_=>:tag_result, :found_tag=>true, :tag=>{:class_=>:tag_info, :name=>"title", ...}}

Notice the use of the :class_ key to label one of the items of the hash (and one of the items of the :tag hash within the hash). This seems odd, and to make it even odder, BBEdit’s dictionary reports that find_tag returns a :tag_result, as if :tag_result really were a class_. We might think of this sort of record as a kind of pseudo-class; it lets a scriptable application return a bunch of named values along with a :class_ key saying what bunch of values it is. (We know, for example, that this hash will have a :found_tag key, because a :tag_result hash does have a :found_tag key.) It’s a hash whose keys are typologically predictable. This is much the same impulse that would lead a Rubyist to make a Struct (see Chapter 2).

Records (hashes) are occasionally used in the other direction, when sending a command to a scriptable application; a very common case is the :with_properties parameter of the make command (see Chapter 6 for an example).

9. Units of Measurement

A whole bunch of measurement units are implemented as built-in class_ types:

'cmtr' => :centimeters,
'metr' => :meters,
'kmtr' => :kilometers,
'inch' => :inches,
'feet' => :feet,
'yard' => :yards,
'mile' => :miles,

'sqrm' => :square_meters,
'sqkm' => :square_kilometers,
'sqft' => :square_feet,
'sqyd' => :square_yards,
'sqmi' => :square_miles,

'ccmt' => :cubic_centimeters,
'cmet' => :cubic_meters,
'cuin' => :cubic_inches,
'cfet' => :cubic_feet,
'cyrd' => :cubic_yards,

'litr' => :liters,
'qrts' => :quarts,
'galn' => :gallons,

'gram' => :grams,
'kgrm' => :kilograms,
'ozs ' => :ounces,
'lbs ' => :pounds,

'degc' => :degrees_Celsius,
'degf' => :degrees_Fahrenheit,
'degk' => :degrees_Kelvin,

Their purpose is to allow unit specification and conversion in AppleScript. Their implementation as class_ types may seem a little odd, but it’s actually rather ingenious, because it means a value can be converted from one unit to another in AppleScript merely by coercion from one class_ to another.

You can communicate unit type objects to and from a scriptable application through the MacTypes::Units class. (Some scriptable applications even define additional unit types. I believe that Adobe Photoshop is an example.) Create an instance with new, providing two parameters, the value and the unit class_ name (a symbol). The value method returns the amount, and the type method returns the unit name.

yds = MacTypes::Units.new(2, :yards)
p yds.value #=> 2
p yds.type #=> :yards

If you really want to, you can also use these types to perform unit conversion, just as AppleScript would do. Why you would do this instead of using the built-in units Unix command beats me, but here goes:

packer = UnitTypeCodecs.new
yds = MacTypes::Units.new(2, :yards)
ok, desc = packer.pack(yds)
if ok
  ok, feet = packer.unpack(desc.coerce(KAE::TypeFeet))
  if ok
    p feet.value #=> 6.0
  end
end

Prev: Commands
Next: The Dictionary
Contents

You’re looking at a draft of a chapter from a work in progress, tentatively titled Scripting Mac Applications With Ruby: An AppleScript Alternative, by Matt Neuburg.
Covers rb-appscript 0.6.1. Last revised Jun 23, 2012. All content ©2012 by the author, all rights reserved.

This book took time and effort to write, and no traditional publisher would accept it. If it has been useful to you, please consider a small donation to my PayPal account (matt at tidbits dot com). Thanks!