rb-appscript 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/CHANGES +243 -0
  2. data/LICENSE +1 -0
  3. data/README +42 -0
  4. data/TODO +31 -0
  5. data/doc/aem-manual/01_introduction.html +48 -0
  6. data/doc/aem-manual/02_apioverview.html +89 -0
  7. data/doc/aem-manual/03_packingandunpackingdata.html +98 -0
  8. data/doc/aem-manual/04_references.html +401 -0
  9. data/doc/aem-manual/05_targettingapplications.html +133 -0
  10. data/doc/aem-manual/06_buildingandsendingevents.html +175 -0
  11. data/doc/aem-manual/07_findapp.html +54 -0
  12. data/doc/aem-manual/08_examples.html +85 -0
  13. data/doc/aem-manual/09_notes.html +41 -0
  14. data/doc/aem-manual/aemreferenceinheritance.gif +0 -0
  15. data/doc/aem-manual/full.css +21 -0
  16. data/doc/aem-manual/index.html +43 -0
  17. data/doc/appscript-manual/01_introduction.html +82 -0
  18. data/doc/appscript-manual/02_aboutappscripting.html +244 -0
  19. data/doc/appscript-manual/03_quicktutorial.html +154 -0
  20. data/doc/appscript-manual/04_gettinghelp.html +101 -0
  21. data/doc/appscript-manual/05_keywordconversion.html +91 -0
  22. data/doc/appscript-manual/06_classesandenums.html +174 -0
  23. data/doc/appscript-manual/07_applicationobjects.html +181 -0
  24. data/doc/appscript-manual/08_realvsgenericreferences.html +86 -0
  25. data/doc/appscript-manual/09_referenceforms.html +232 -0
  26. data/doc/appscript-manual/10_referenceexamples.html +142 -0
  27. data/doc/appscript-manual/11_applicationcommands.html +204 -0
  28. data/doc/appscript-manual/12_commandexamples.html +129 -0
  29. data/doc/appscript-manual/13_performanceissues.html +115 -0
  30. data/doc/appscript-manual/14_problemapps.html +193 -0
  31. data/doc/appscript-manual/15_notes.html +84 -0
  32. data/doc/appscript-manual/application_architecture.gif +0 -0
  33. data/doc/appscript-manual/application_architecture2.gif +0 -0
  34. data/doc/appscript-manual/finder_to_textedit_event.gif +0 -0
  35. data/doc/appscript-manual/full.css +21 -0
  36. data/doc/appscript-manual/index.html +49 -0
  37. data/doc/appscript-manual/relationships_example.gif +0 -0
  38. data/doc/appscript-manual/ruby_to_itunes_event.gif +0 -0
  39. data/doc/index.html +30 -0
  40. data/doc/mactypes-manual/index.html +216 -0
  41. data/doc/osax-manual/index.html +169 -0
  42. data/extconf.rb +54 -0
  43. data/misc/adobeunittypes.rb +14 -0
  44. data/misc/dump.rb +72 -0
  45. data/rb-appscript.gemspec +20 -0
  46. data/sample/AB_list_people_with_emails.rb +8 -0
  47. data/sample/Create_daily_iCal_todos.rb +72 -0
  48. data/sample/Hello_world.rb +9 -0
  49. data/sample/List_iTunes_playlist_names.rb +7 -0
  50. data/sample/Make_Mail_message.rb +29 -0
  51. data/sample/Open_file_in_TextEdit.rb +9 -0
  52. data/sample/Organize_Mail_messages.rb +57 -0
  53. data/sample/Print_folder_tree.rb +12 -0
  54. data/sample/Select_all_HTML_files.rb +8 -0
  55. data/sample/Set_iChat_status.rb +20 -0
  56. data/sample/Simple_Finder_GUI_Scripting.rb +14 -0
  57. data/sample/Stagger_Finder_windows.rb +21 -0
  58. data/sample/TextEdit_demo.rb +126 -0
  59. data/sample/iTunes_top40_to_html.rb +64 -0
  60. data/src/lib/_aem/aemreference.rb +1006 -0
  61. data/src/lib/_aem/codecs.rb +617 -0
  62. data/src/lib/_aem/connect.rb +100 -0
  63. data/src/lib/_aem/findapp.rb +83 -0
  64. data/src/lib/_aem/mactypes.rb +228 -0
  65. data/src/lib/_aem/send.rb +257 -0
  66. data/src/lib/_aem/typewrappers.rb +57 -0
  67. data/src/lib/_appscript/defaultterminology.rb +245 -0
  68. data/src/lib/_appscript/referencerenderer.rb +132 -0
  69. data/src/lib/_appscript/reservedkeywords.rb +107 -0
  70. data/src/lib/_appscript/terminology.rb +314 -0
  71. data/src/lib/aem.rb +216 -0
  72. data/src/lib/appscript.rb +830 -0
  73. data/src/lib/kae.rb +1484 -0
  74. data/src/lib/osax.rb +171 -0
  75. data/src/rbae.c +766 -0
  76. data/test/README +1 -0
  77. data/test/test_aemreference.rb +112 -0
  78. data/test/test_appscriptreference.rb +102 -0
  79. data/test/test_codecs.rb +159 -0
  80. data/test/test_findapp.rb +24 -0
  81. data/test/test_mactypes.rb +67 -0
  82. data/test/testall.sh +9 -0
  83. metadata +143 -0
@@ -0,0 +1,314 @@
1
+ #!/usr/local/bin/ruby
2
+ # Copyright (C) 2006 HAS.
3
+ # Released under MIT License.
4
+
5
+ ######################################################################
6
+ # TERMINOLOGY PARSER
7
+ ######################################################################
8
+
9
+ module TerminologyParser
10
+
11
+ require "_appscript/reservedkeywords" # names of all existing methods on ASReference::Application
12
+
13
+ class BigEndianParser
14
+ @@_name_cache = {}
15
+ LegalFirst = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
16
+ LegalRest = LegalFirst + '0123456789'
17
+
18
+ def initialize
19
+ @enumerators = {}
20
+ @properties = {}
21
+ @commands = {}
22
+ @_plural_class_names = {}
23
+ @_singular_class_names = {}
24
+ end
25
+
26
+ def _integer
27
+ @_ptr += 2
28
+ return @_str[@_ptr - 2, 2].unpack('S')[0]
29
+ end
30
+
31
+ def _word
32
+ @_ptr += 4
33
+ return @_str[@_ptr - 4, 4]
34
+ end
35
+
36
+ def _name
37
+ count = @_str[@_ptr]
38
+ @_ptr += 1 + count
39
+ s = @_str[@_ptr - count, count]
40
+ if not @@_name_cache.has_key?(s)
41
+ legal = LegalFirst
42
+ res = ''
43
+ s.split(//).each do |c|
44
+ if legal[c]
45
+ res += c
46
+ else
47
+ case c
48
+ when ' ', '-', '/'
49
+ res += '_'
50
+ when '&'
51
+ res += 'and'
52
+ else
53
+ if res == ''
54
+ res = '_'
55
+ end
56
+ res += "0x#{c.unpack('HXh')}"
57
+ end
58
+ end
59
+ legal = LegalRest
60
+ end
61
+ if res[0, 3] == 'AS_' or ReservedKeywords.include?(res) or res[0, 1] == '_'
62
+ res += '_'
63
+ end
64
+ @@_name_cache[s] = res
65
+ end
66
+ return @@_name_cache[s]
67
+ end
68
+
69
+ ##
70
+
71
+ def parse_command
72
+ name = _name
73
+ @_ptr += 1 + @_str[@_ptr]
74
+ @_ptr += @_ptr & 1
75
+ code = _word + _word
76
+ # skip result
77
+ @_ptr += 4
78
+ @_ptr += 1 + @_str[@_ptr]
79
+ @_ptr += @_ptr & 1
80
+ @_ptr += 2
81
+ # skip direct
82
+ @_ptr += 4
83
+ @_ptr += 1 + @_str[@_ptr]
84
+ @_ptr += @_ptr & 1
85
+ @_ptr += 2
86
+ #
87
+ current_command_args = []
88
+ @commands[code] = [name, code, current_command_args]
89
+ # args
90
+ _integer.times do
91
+ parameter_name = _name
92
+ @_ptr += @_ptr & 1
93
+ parameter_code = _word
94
+ @_ptr += 4
95
+ @_ptr += 1 + @_str[@_ptr]
96
+ @_ptr += @_ptr & 1
97
+ @_ptr += 2
98
+ current_command_args.push([parameter_name, parameter_code])
99
+ end
100
+ end
101
+
102
+ def parse_class
103
+ name = _name
104
+ @_ptr += @_ptr & 1
105
+ code = _word
106
+ @_ptr += 1 + @_str[@_ptr]
107
+ @_ptr += @_ptr & 1
108
+ is_plural = false
109
+ _integer.times do
110
+ propname = _name
111
+ @_ptr += @_ptr & 1
112
+ propcode = _word
113
+ @_ptr += 4
114
+ @_ptr += 1 + @_str[@_ptr]
115
+ @_ptr += @_ptr & 1
116
+ flags = _integer
117
+ if propcode != 'c@#^'
118
+ if flags & 1 == 1
119
+ is_plural = true
120
+ else
121
+ @properties[propcode] = [propname, propcode]
122
+ end
123
+ end
124
+ end
125
+ _integer.times do
126
+ @_ptr += 4
127
+ count = _integer
128
+ @_ptr += 4 * count
129
+ end
130
+ if is_plural
131
+ @_plural_class_names[code] = [name, code]
132
+ else
133
+ @_singular_class_names[code] = [name, code]
134
+ end
135
+ end
136
+
137
+ def parse_comparison
138
+ @_ptr += 1 + @_str[@_ptr]
139
+ @_ptr += @_ptr & 1
140
+ @_ptr += 4
141
+ @_ptr += 1 + @_str[@_ptr]
142
+ @_ptr += @_ptr & 1
143
+ end
144
+
145
+ def parse_enumeration
146
+ @_ptr += 4
147
+ _integer.times do
148
+ name = _name
149
+ @_ptr += @_ptr & 1
150
+ code = _word
151
+ @_ptr += 1 + @_str[@_ptr]
152
+ @_ptr += @_ptr & 1
153
+ @enumerators[code + name] = [name, code]
154
+ end
155
+ end
156
+
157
+ def parse_suite
158
+ @_ptr += 1 + @_str[@_ptr]
159
+ @_ptr += 1 + @_str[@_ptr]
160
+ @_ptr += @_ptr & 1
161
+ @_ptr += 4
162
+ @_ptr += 4
163
+ _integer.times { parse_command }
164
+ _integer.times { parse_class }
165
+ _integer.times { parse_comparison }
166
+ _integer.times { parse_enumeration }
167
+ end
168
+
169
+ def parse(aetes)
170
+ aetes.each do |aete|
171
+ @_str = aete.data
172
+ @_ptr = 6
173
+ _integer.times { parse_suite }
174
+ if not @_ptr == @_str.length
175
+ raise RuntimeError, "aete was not fully parsed."
176
+ end
177
+ end
178
+ classes = @_plural_class_names.clone
179
+ classes.update(@_singular_class_names)
180
+ elements = @_singular_class_names.clone
181
+ elements.update(@_plural_class_names)
182
+ return [classes, @enumerators, @properties, elements, @commands].map! { |d| d.values }
183
+ end
184
+ end
185
+
186
+
187
+ class LittleEndianParser < BigEndianParser
188
+ def _word
189
+ return super.reverse
190
+ end
191
+ end
192
+
193
+
194
+ #######
195
+ # Public
196
+
197
+ def TerminologyParser.build_tables_for_aetes(aetes)
198
+ if [1].pack('S') == "\000\001"
199
+ return BigEndianParser.new.parse(aetes)
200
+ else
201
+ return LittleEndianParser.new.parse(aetes)
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+
208
+ ######################################################################
209
+ # TERMINOLOGY TABLES BUILDER
210
+ ######################################################################
211
+
212
+ module Terminology
213
+
214
+ require "aem"
215
+ require "_appscript/defaultterminology"
216
+
217
+ @@_terminology_cache = {}
218
+
219
+ def Terminology._make_type_table(classes, enums, properties)
220
+ type_by_code = DefaultTerminology::TypeByCode.clone
221
+ type_by_name = DefaultTerminology::TypeByName.clone
222
+ [[AEM::AEType, properties], [AEM::AEEnum, enums], [AEM::AEType, classes]].each do |klass, table|
223
+ table.each do |name, code|
224
+ if DefaultTerminology::TypeByName.has_key?(name) and \
225
+ DefaultTerminology::TypeByName[name].code != code
226
+ name += '_'
227
+ end
228
+ type_by_code[code] = name.intern
229
+ type_by_name[name.intern] = klass.new(code)
230
+ end
231
+ end
232
+ return [type_by_code, type_by_name]
233
+ end
234
+
235
+ def Terminology._make_reference_table(properties, elements, commands)
236
+ reference_by_code = DefaultTerminology::ReferenceByCode.clone
237
+ reference_by_name = DefaultTerminology::ReferenceByName.clone
238
+ [[:element, elements, 'e'], [:property, properties, 'p']].each do |kind, table, prefix|
239
+ # note: if property and element names are same (e.g. 'file' in BBEdit), will pack as property specifier unless it's a special case (i.e. see :text below). Note that there is currently no way to override this, i.e. to force appscript to pack it as an all-elements specifier instead (in AS, this would be done by prepending the 'every' keyword), so clients would need to use aem for that (but could add an 'all' method to Reference class if there was demand for a built-in workaround)
240
+ table.each do |name, code|
241
+ reference_by_code[prefix + code] = name
242
+ reference_by_name[name.intern] = [kind, code]
243
+ end
244
+ end
245
+ if reference_by_name.has_key?(:text) # special case: AppleScript always packs 'text of...' as all-elements specifier
246
+ reference_by_name[:text][0] = :element
247
+ end
248
+ commands.reverse.each do |name, code, args|
249
+ if DefaultTerminology::DefaultCommands.has_key?(name) and \
250
+ code != DefaultTerminology::DefaultCommands[name]
251
+ name += '_'
252
+ end
253
+ dct = {}
254
+ args.each { |arg_name, arg_code| dct[arg_name.intern] = arg_code }
255
+ reference_by_name[name.intern] = [:command, [code, dct]]
256
+ end
257
+ return reference_by_code, reference_by_name
258
+ end
259
+
260
+ #######
261
+ # public
262
+
263
+ def Terminology.default_tables
264
+ return _make_type_table([], [], []) + _make_reference_table([], [], [])
265
+ end
266
+
267
+ def Terminology.tables_for_aetes(aetes)
268
+ classes, enums, properties, elements, commands = TerminologyParser.build_tables_for_aetes(aetes.delete_if { |aete| not (aete.is_a?(AE::AEDesc) and aete.type == 'aete') })
269
+ return _make_type_table(classes, enums, properties) + _make_reference_table(properties, elements, commands)
270
+ end
271
+
272
+ ##
273
+
274
+ def Terminology.tables_for_module(terms)
275
+ if terms::Version != 1.1
276
+ raise RuntimeError, "Unsupported terminology module version: #{terms::Version} (requires version 1.1)."
277
+ end
278
+ return _make_type_table(terms::Classes, terms::Enumerators, terms::Properties) \
279
+ + _make_reference_table(terms::Properties, terms::Elements, terms::Commands)
280
+ end
281
+
282
+ def Terminology.tables_for_app(path, pid, url)
283
+ if not @@_terminology_cache.has_key?([path, pid, url])
284
+ begin
285
+ if path
286
+ app = AEM::Application.by_path(path)
287
+ elsif pid
288
+ app = AEM::Application.by_pid(pid)
289
+ elsif url
290
+ app = AEM::Application.by_url(url)
291
+ else
292
+ app = AEM::Application.current
293
+ end
294
+ begin
295
+ aetes = app.event('ascrgdte', {'----' => 0}).send(30 * 60)
296
+ rescue AEM::CommandError => e
297
+ if e.number == -192 # aete resource not found
298
+ aetes = []
299
+ else
300
+ raise
301
+ end
302
+ end
303
+ if not aetes.is_a?(Array)
304
+ aetes = [aetes]
305
+ end
306
+ rescue => err
307
+ raise RuntimeError, "Can't get terminology for application (#{path or pid or url}): #{err}"
308
+ end
309
+ @@_terminology_cache[[path, pid, url]] = Terminology.tables_for_aetes(aetes)
310
+ end
311
+ return @@_terminology_cache[[path, pid, url]]
312
+ end
313
+ end
314
+
data/src/lib/aem.rb ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/local/bin/ruby
2
+ # Copyright (C) 2006 HAS.
3
+ # Released under MIT License.
4
+
5
+ require "ae"
6
+ require "kae"
7
+ require "_aem/findapp"
8
+ require "_aem/mactypes"
9
+
10
+ module AEM
11
+
12
+ # Mid-level wrapper for building and sending Apple events to local and remote applications.
13
+
14
+ require "_aem/codecs"
15
+ require "_aem/aemreference"
16
+ require "_aem/typewrappers"
17
+ require "_aem/connect"
18
+ require "_aem/send"
19
+
20
+ #######
21
+ # Constants
22
+
23
+ Codecs = Codecs
24
+ DefaultCodecs = DefaultCodecs
25
+ MacOSError = AE::MacOSError
26
+
27
+ AEDesc = AE::AEDesc
28
+
29
+ AETypeBase = TypeWrappers::AETypeBase
30
+ AEType = TypeWrappers::AEType
31
+ AEEnum = TypeWrappers::AEEnum
32
+ AEProp = TypeWrappers::AEProp
33
+ AEKey = TypeWrappers::AEKey
34
+
35
+ CommandError = Send::CommandError
36
+
37
+ #######
38
+ # Reference roots
39
+
40
+ def AEM.app
41
+ return AEMReference::App
42
+ end
43
+
44
+ def AEM.con
45
+ return AEMReference::Con
46
+ end
47
+
48
+ def AEM.its
49
+ return AEMReference::Its
50
+ end
51
+
52
+ #######
53
+ # Application class
54
+
55
+ class Application
56
+ # Identifies an application and provides an #event method for constructing Apple events targetted at it.
57
+
58
+ require "weakref"
59
+
60
+ private_class_method :new
61
+ attr_reader :hash, :identity
62
+ protected :identity
63
+
64
+ #######
65
+ # Workaround for lack of proper destructors in Ruby; see #initialize method.
66
+
67
+ @@_app_number_count = 0
68
+ @@_transaction_ids_by_app_no = {}
69
+
70
+ #######
71
+
72
+ Event = Send::Event # Application subclasses can override this class constant (usually with a subclass of Send::Event) to modify how Apple events are created and/or sent.
73
+
74
+ #######
75
+
76
+ def initialize(path, address_desc, identity)
77
+ # called by constructor method
78
+ # path is used by #reconnect
79
+ # address_desc is an AEAddressDesc identifying the target application
80
+ # identity is used by #inspect, #hash, #==
81
+ @_transaction = KAE::KAnyTransactionID
82
+ @_path = path
83
+ @_address = address_desc
84
+ @identity = identity
85
+ @hash = identity.hash
86
+ # workaround for lack of proper destructors; if a transaction is still open when Application instance is garbage collected, the following finalizer will automatically close it. Note: object IDs were different for some reason, so class maintains its own unique ids.
87
+ @app_number = app_number = (@@_app_number_count += 1)
88
+ @@_transaction_ids_by_app_no[app_number] = @_transaction
89
+ ObjectSpace.define_finalizer(WeakRef.new(self), proc do
90
+ transaction_id = @@_transaction_ids_by_app_no.delete(app_number)
91
+ if transaction_id != KAE::KAnyTransactionID
92
+ self.class::Event.new(@_address, 'miscendt', {}, {}, transaction_id).send(60, KAE::KAENoReply)
93
+ end
94
+ end)
95
+ end
96
+
97
+ #######
98
+ # utility class methods; placed here for convenience
99
+
100
+ def Application.launch(path)
101
+ # Launches a local application without sending it the usual 'run' event (aevtoapp).
102
+ Connect.launch_app(path)
103
+ end
104
+
105
+ def Application.is_running?(path)
106
+ # Checks if a local application is running.
107
+ return Connect.is_running?(path)
108
+ end
109
+
110
+ #######
111
+ # constructors
112
+
113
+ def Application.by_path(path)
114
+ # path : string -- full path to local application
115
+ #
116
+ # Note: application will be launched if not already running.
117
+ return new(path, Connect.local_app(path), [:path, path])
118
+ end
119
+
120
+ def Application.by_url(url)
121
+ # url : string -- eppc URL for remote process
122
+ return new(nil, Connect.remote_app(url), [:url, url])
123
+ end
124
+
125
+ def Application.by_pid(pid)
126
+ # pid : integer -- Unix process id
127
+ return new(nil, Connect.local_app_by_pid(pid), [:pid, pid])
128
+ end
129
+
130
+ def Application.by_desc(desc)
131
+ # desc : AEDesc -- an AEAddressDesc
132
+ return new(nil, desc, [:desc, desc.type, desc.data])
133
+ end
134
+
135
+ def Application.current
136
+ return new(nil, Connect::CurrentApp, [:current])
137
+ end
138
+
139
+ #######
140
+ # methods
141
+
142
+ def inspect
143
+ if @identity[0] == :current
144
+ return 'AEM::Application.current'
145
+ else
146
+ con_name = {:path => 'by_path', :url => 'by_url', :pid => 'by_pid', :desc => 'by_desc'}[@identity[0]]
147
+ return "AEM::Application.#{con_name}(#{@identity[1].inspect})"
148
+ end
149
+ end
150
+
151
+ alias_method :to_s, :inspect
152
+
153
+ def ==(val)
154
+ return (self.class == val.class and @identity == val.identity)
155
+ end
156
+
157
+ alias_method :eql?, :==
158
+
159
+ # (hash method is provided by attr_reader :hash)
160
+
161
+ def reconnect
162
+ # If application has quit since this Application object was created, its AEAddressDesc
163
+ # is no longer valid so this Application object will not work even when application is restarted.
164
+ # #reconnect will update this Application object's AEAddressDesc so it's valid again.
165
+ #
166
+ # Note that this only works for Application objects created via the by_path constructor.
167
+ # Also note that any Event objects created prior to calling #reconnect will still be invalid.
168
+ if @_path
169
+ @_address = Connect.local_app(@_path)
170
+ end
171
+ return
172
+ end
173
+
174
+ def event(event, params={}, atts={}, return_id=KAE::KAutoGenerateReturnID, codecs=DefaultCodecs)
175
+ # Construct an Apple event targetted at this application.
176
+ # event : string -- 8-letter code indicating event's class, e.g. 'coregetd'
177
+ # params : hash -- a dict of form {AE_code:anything,...} containing zero or more event parameters (message arguments)
178
+ # atts : hash -- a dict of form {AE_code:anything,...} containing zero or more event attributes (event info)
179
+ # return_id : integer -- reply event's ID
180
+ # codecs : Codecs -- codecs object to use when packing/unpacking this event
181
+ return self.class::Event.new(@_address, event, params, atts, @_transaction, return_id, codecs)
182
+ end
183
+
184
+ def start_transaction(session=nil)
185
+ # Start a new transaction.
186
+ if @_transaction != KAE::KAnyTransactionID
187
+ raise RuntimeError, "Transaction is already active."
188
+ end
189
+ @_transaction = self.class::Event.new(@_address, 'miscbegi', session != nil ? {'----' => session} : {}).send
190
+ @@_transaction_ids_by_app_no[@app_number] = @_transaction
191
+ return
192
+ end
193
+
194
+ def abort_transaction
195
+ # Abort the current transaction.
196
+ if @_transaction == KAE::KAnyTransactionID
197
+ raise RuntimeError, "No transaction is active."
198
+ end
199
+ self.class::Event.new(@_address, 'miscttrm', {}, {}, @_transaction).send
200
+ @_transaction = KAE::KAnyTransactionID
201
+ @@_transaction_ids_by_app_no[@app_number] = KAE::KAnyTransactionID
202
+ return
203
+ end
204
+
205
+ def end_transaction
206
+ # End the current transaction.
207
+ if @_transaction == KAE::KAnyTransactionID
208
+ raise RuntimeError, "No transaction is active."
209
+ end
210
+ self.class::Event.new(@_address, 'miscendt', {}, {}, @_transaction).send
211
+ @_transaction = KAE::KAnyTransactionID
212
+ @@_transaction_ids_by_app_no[@app_number] = KAE::KAnyTransactionID
213
+ return
214
+ end
215
+ end
216
+ end