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.
Chapter 7: Datatypes
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).
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
Booleans ('bool' => :boolean
) correspond to Ruby booleans (true
or false
).
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.
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.)
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.
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:
path: a POSIX pathname
hfs_path: a legacy Macintosh pathname, with colon separators
url: a file://
URL
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
.
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,
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).
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
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!