castanaut 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,336 @@
1
+ module Castanaut; module OS; module MacOSX
2
+
3
+ # This class is intended to work on machines running Mac OS X 10.5.x or
4
+ # greater.
5
+ #
6
+ # KNOWN LIMITATIONS
7
+ #
8
+ # Partially working:
9
+ # * type - does not support the :speed option
10
+ # * hit - only works with special keys (those in keys.rb) not
11
+ # other characters (like 'a'), and does not support modifier keys
12
+ # (you can use keystroke instead, perhaps)
13
+ #
14
+ class Movie < Castanaut::Movie
15
+
16
+ register("Mac OS X 10.5 or greater")
17
+
18
+
19
+ # Returns true if the current platform is Mac OS X 10.5 or greater.
20
+ #
21
+ def self.platform_supported?
22
+ vers = `/usr/bin/sw_vers -productVersion`.match(/10\.(\d)\.\d+/)
23
+ vers[1].to_i >= 5
24
+ rescue
25
+ false
26
+ end
27
+
28
+
29
+ #--------------------------------------------------------------------------
30
+ # KEYBOARD INPUT DIRECTIONS
31
+ #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
32
+
33
+ # Does not support modifiers (shift, ctrl, etc)
34
+ def hit(key, *modifiers)
35
+ not_supported "modifier keys for 'hit'" unless modifiers.empty?
36
+ automatically "hit #{key}"
37
+ end
38
+
39
+
40
+ # Hit a command key combo toward the currently active application.
41
+ #
42
+ # Use any combination of "command", "option", "control", "shift".
43
+ # ("command" is the default).
44
+ #
45
+ # Case matters! It's easiest to use lowercase, then "shift" if needed.
46
+ #
47
+ # keystroke "t" # COMMAND-t
48
+ # keystroke "k", "control", "shift" # A combo
49
+ #
50
+ def keystroke(character, *special_keys)
51
+ special_keys = ["command"] if special_keys.length == 0
52
+ special_keys_as_applescript_array = special_keys.map { |k|
53
+ "#{k} down"
54
+ }.join(", ")
55
+ execute_applescript(%Q'
56
+ tell application "System Events"
57
+ set frontApp to name of first item of (processes whose frontmost is true)
58
+ tell application frontApp
59
+ keystroke "#{character}" using {#{special_keys_as_applescript_array}}
60
+ end
61
+ end tell
62
+ ')
63
+ end
64
+
65
+
66
+ # If you pass :applescript => true, the AppleScript technique for typing
67
+ # will be used. In this way you can use the :speed option —
68
+ # it's not supported by the main (osxautomation) technique.
69
+ #
70
+ def type(str, opts = {})
71
+ if opts.delete(:applescript)
72
+ type_via_applescript(str, opts)
73
+ else
74
+ automatically "type #{str}"
75
+ end
76
+ end
77
+
78
+
79
+ # The alternative typing method for Mac OS X - lets you set the
80
+ # typomatic rate with the :speed option.
81
+ #
82
+ def type_via_applescript(str, opts = {})
83
+ opts[:speed] = 50 unless !opts[:speed].nil?
84
+ opts[:speed] = opts[:speed] / 1000.0
85
+
86
+ full_str = ""
87
+ str.split("").each do |a|
88
+ a.gsub!(/"/, '\"')
89
+ full_str += "delay #{opts[:speed]}\n" if !full_str.empty?
90
+ full_str += "keystroke \"#{a}\"\n"
91
+ end
92
+ cmd = %Q'
93
+ tell application "System Events"
94
+ set frontApp to name of first item of (processes whose frontmost is true)
95
+ tell application frontApp
96
+ #{full_str}
97
+ end
98
+ end tell
99
+ '
100
+ execute_applescript cmd
101
+ str
102
+ end
103
+
104
+
105
+ #---------------------------------------------------------------------------
106
+ # MOUSE INPUT DIRECTIONS
107
+ #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
108
+
109
+ def cursor(*options)
110
+ options = combine_options(*options)
111
+
112
+ apply_offset(options)
113
+ @cursor_loc ||= {}
114
+ @cursor_loc[:x] = options[:to][:left]
115
+ @cursor_loc[:y] = options[:to][:top]
116
+
117
+ automatically "mousemove #{@cursor_loc[:x]} #{@cursor_loc[:y]}"
118
+ end
119
+
120
+ alias :move :cursor
121
+
122
+
123
+ def cursor_location
124
+ loc = automatically("mouselocation").strip.split(' ')
125
+ {:x => loc[0].to_i, :y => loc[1].to_i}
126
+ end
127
+
128
+
129
+ def click(btn = "left")
130
+ automatically "mouseclick #{mouse_button_translate(btn)}"
131
+ end
132
+
133
+
134
+ def doubleclick(btn = "left")
135
+ automatically "mousedoubleclick #{mouse_button_translate(btn)}"
136
+ end
137
+
138
+
139
+ def tripleclick(btn = "left")
140
+ automatically "mousetripleclick #{mouse_button_translate(btn)}"
141
+ end
142
+
143
+
144
+ def mousedown(btn = "left")
145
+ automatically "mousedown #{mouse_button_translate(btn)}"
146
+ end
147
+
148
+
149
+ def mouseup(btn = "left")
150
+ automatically "mouseup #{mouse_button_translate(btn)}"
151
+ end
152
+
153
+
154
+ def drag(*options)
155
+ options = combine_options(*options)
156
+ apply_offset(options)
157
+ automatically "mousedrag #{options[:to][:left]} #{options[:to][:top]}"
158
+ end
159
+
160
+
161
+ #--------------------------------------------------------------------------
162
+ # WINDOWS AND APPLICATIONS DIRECTIONS
163
+ #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
164
+
165
+ # The method will also look for application-specific commands for
166
+ # ensuring that a window is open & positioning that window.
167
+ # These methods should be named +ensure_window_for_app_name+
168
+ # and +positioning_for_app_name+ respectively. So, if you launch
169
+ # the "Address Book" application, the +ensure_window_for_address_book+
170
+ # and +positioning_for_address_book+ methods will be used.
171
+ #
172
+ # See Plugin::Safari#ensure_window_for_safari for an example.
173
+ #
174
+ def launch(app_name, *options)
175
+ options = combine_options(*options)
176
+
177
+ ensure_window = nil
178
+ begin
179
+ ensure_window = send("ensure_window_for_#{ app_name.downcase }")
180
+ rescue
181
+ end
182
+ ensure_window ||= ""
183
+
184
+ positioning = nil
185
+ begin
186
+ positioning = send("positioning_for_#{ app_name.downcase }")
187
+ rescue
188
+ end
189
+ unless positioning
190
+ if options[:to]
191
+ pos = "#{options[:to][:left]}, #{options[:to][:top]}"
192
+ dims = "#{options[:to][:left] + options[:to][:width]}, " +
193
+ "#{options[:to][:top] + options[:to][:height]}"
194
+ if options[:to][:width]
195
+ positioning = "set bounds of front window to {#{pos}, #{dims}}"
196
+ else
197
+ positioning = "set position of front window to {#{pos}}"
198
+ end
199
+ end
200
+ end
201
+
202
+ execute_applescript(%Q`
203
+ tell application "#{app_name}"
204
+ activate
205
+ #{ensure_window}
206
+ #{positioning}
207
+ end tell
208
+ `)
209
+ end
210
+
211
+ alias :activate :launch
212
+
213
+
214
+ # Returns a region hash describing the entire screen area.
215
+ # (May be wonky for multi-monitor set-ups.)
216
+ #
217
+ def screen_size
218
+ coords = execute_applescript(%Q`
219
+ tell application "Finder"
220
+ get bounds of window of desktop
221
+ end tell
222
+ `)
223
+ coords = coords.split(", ").collect {|c| c.to_i}
224
+ to(*coords)
225
+ end
226
+
227
+
228
+ # Click a menu item in any application. The name of the application
229
+ # should be the first argument.
230
+ #
231
+ # Three dots will be automatically replaced by the appropriate ellipsis.
232
+ #
233
+ # click_menu_item("TextMate", "Navigation", "Go to Symbol...")
234
+ #
235
+ # Based on menu_click, by Jacob Rus, September 2006:
236
+ # http://www.macosxhints.com/article.php?story=20060921045743404
237
+ #
238
+ def click_menu_item(*items)
239
+ items_as_applescript_array = items.map { |i|
240
+ %("#{i.gsub('...', "\342\200\246")}")
241
+ }.join(", ")
242
+
243
+ ascript = %Q`
244
+ on menu_click(mList)
245
+ local appName, topMenu, r
246
+ if mList's length < 3 then error "Menu list is not long enough"
247
+
248
+ set {appName, topMenu} to (items 1 through 2 of mList)
249
+ set r to (items 3 through (mList's length) of mList)
250
+
251
+ tell application "System Events" to my menu_click_recurse(r, ((process appName)'s (menu bar 1)'s (menu bar item topMenu)'s (menu topMenu)))
252
+ end menu_click
253
+
254
+ on menu_click_recurse(mList, parentObject)
255
+ local f, r
256
+
257
+ set f to item 1 of mList
258
+ if mList's length > 1 then set r to (items 2 through (mList's length) of mList)
259
+
260
+ tell application "System Events"
261
+ if mList's length is 1 then
262
+ click parentObject's menu item f
263
+ else
264
+ my menu_click_recurse(r, (parentObject's (menu item f)'s (menu f)))
265
+ end if
266
+ end tell
267
+ end menu_click_recurse
268
+
269
+ menu_click({#{items_as_applescript_array}})
270
+ `
271
+ execute_applescript(ascript)
272
+ end
273
+
274
+
275
+ #--------------------------------------------------------------------------
276
+ # USEFUL UTILITIES
277
+ #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
278
+
279
+ # Runs an applescript from a string.
280
+ # Returns the result.
281
+ #
282
+ def execute_applescript(scpt)
283
+ File.open(FILE_APPLESCRIPT, 'w') {|f| f.write(scpt)}
284
+ result = run("osascript #{FILE_APPLESCRIPT}")
285
+ File.unlink(FILE_APPLESCRIPT)
286
+ result
287
+ end
288
+
289
+
290
+ # Use MacOS's native text-to-speech functionality to emulate a human
291
+ # voice saying the narrative text.
292
+ #
293
+ def say(narrative)
294
+ run(%Q`say "#{escape_dq(narrative)}"`) unless ENV['SHHH']
295
+ end
296
+
297
+
298
+ protected
299
+
300
+ def automatically(cmd)
301
+ perms_test
302
+ run("\"#{osxautomation_path}\" \"#{cmd}\"")
303
+ end
304
+
305
+
306
+ private
307
+
308
+ def perms_test
309
+ return if File.executable?(osxautomation_path)
310
+ puts "IMPORTANT: Castanaut has recently been installed or updated. " +
311
+ "You need to give it the right to control mouse and keyboard " +
312
+ "input during screenplays."
313
+
314
+ run("sudo chmod a+x #{osxautomation_path}")
315
+
316
+ if File.executable?(osxautomation_path)
317
+ puts "Permission granted. Thanks."
318
+ else
319
+ raise Castanaut::Exceptions::OSXAutomationPermissionError
320
+ end
321
+ end
322
+
323
+
324
+ def osxautomation_path
325
+ File.join(PATH, "cbin", "osxautomation")
326
+ end
327
+
328
+
329
+ def mouse_button_translate(btn)
330
+ return btn if btn.is_a?(Integer)
331
+ {"left" => 1, "right" => 2, "middle" => 3}[btn]
332
+ end
333
+
334
+ end
335
+
336
+ end; end; end
@@ -0,0 +1,223 @@
1
+ module Castanaut; module OS; module MacOSX
2
+
3
+ # The TigerMovie class is intended to work on machines running
4
+ # Mac OS X 10.4.x. In order for it to work correctly, the Extras Suites
5
+ # application must be installed.
6
+ #
7
+ # Get it at <http://www.kanzu.com/main.html#extrasuites>
8
+ #
9
+ # KNOWN LIMITATIONS
10
+ #
11
+ # Not supported:
12
+ # * Movie#mousedown
13
+ # * Movie#mouseup
14
+ # * Movie#drag
15
+ #
16
+ # Partially supported:
17
+ # * click - only work with left-clicks
18
+ #
19
+ class TigerMovie < Castanaut::OS::MacOSX::Movie
20
+
21
+ register("Mac OS X 10.4")
22
+
23
+
24
+ # Returns true if the current platform is Mac OS X 10.5 or greater.
25
+ #
26
+ def self.platform_supported?
27
+ vers = `/usr/bin/sw_vers -productVersion`.match(/10\.(\d)\.\d+/)
28
+ vers[1].to_i == 4
29
+ rescue
30
+ false
31
+ end
32
+
33
+
34
+ #--------------------------------------------------------------------------
35
+ # KEYBOARD INPUT DIRECTIONS
36
+ #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
37
+
38
+ def hit(key, *modifiers)
39
+ script = ''
40
+ if key == '"'
41
+ type(key)
42
+ return
43
+ elsif key.index('0x') == 0
44
+ script = hit_with_system_events(key, *modifiers)
45
+ else
46
+ script = hit_with_extra_suites(key, *modifiers)
47
+ end
48
+ execute_applescript(script)
49
+ end
50
+
51
+
52
+ def keystroke(*args)
53
+ not_supported("keystroke")
54
+ end
55
+
56
+
57
+ def type(str, opts = {})
58
+ type_via_applescript(str, opts)
59
+ end
60
+
61
+
62
+ #---------------------------------------------------------------------------
63
+ # MOUSE INPUT DIRECTIONS
64
+ #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
65
+
66
+ def cursor(*options)
67
+ options = combine_options(*options)
68
+
69
+ apply_offset(options)
70
+ @cursor_loc ||= {}
71
+ @cursor_loc[:x] = options[:to][:left]
72
+ @cursor_loc[:y] = options[:to][:top]
73
+
74
+
75
+ start_arr ||= execute_applescript(%Q`
76
+ tell application "Extra Suites"
77
+ ES mouse location
78
+ end tell
79
+ `).to_s.split(',').collect{|p| p.to_s.to_i}
80
+ start_loc = {:x=>start_arr[0], :y=>start_arr[1]}
81
+ dist = {
82
+ :x=>(start_loc[:x] - @cursor_loc[:x]),
83
+ :y=>(start_loc[:y] - @cursor_loc[:y])
84
+ }
85
+ steps = dist.values.collect{|p| p.to_s.to_i.abs}.max / 10.0
86
+
87
+ dist = {:x=>dist[:x] / BigDecimal.new(steps.to_s), :y=>dist[:y] / BigDecimal.new(steps.to_s)}
88
+
89
+ execute_applescript(%Q`
90
+ tell application "Extra Suites"
91
+ set x to #{start_loc[:x]}
92
+ set y to #{start_loc[:y]}
93
+ repeat while x #{dist[:x] > 0 ? '>' : '<'} #{@cursor_loc[:x]} or y #{dist[:y] > 0 ? '>' : '<'} #{@cursor_loc[:y]}
94
+ ES move mouse {x, y}
95
+ set x to x - #{dist[:x].round(2)}
96
+ set y to y - #{dist[:y].round(2)}
97
+ delay 1.0E-6
98
+ end repeat
99
+ ES move mouse {#{@cursor_loc[:x]}, #{@cursor_loc[:y]}}
100
+ end tell
101
+ `)
102
+ end
103
+
104
+
105
+ def cursor_location
106
+ loc = execute_applescript(%Q`
107
+ tell application "Extra Suites"
108
+ ES mouse location
109
+ end tell
110
+ `).split(/\D+/)
111
+ {:x => loc[0].to_i, :y => loc[1].to_i}
112
+ end
113
+
114
+
115
+ def click(btn = "left")
116
+ not_supported "anything other than left clicking" unless btn == 'left'
117
+ execute_applescript(%Q`
118
+ tell application "Extra Suites"
119
+ ES click mouse
120
+ end tell
121
+ `)
122
+ end
123
+
124
+
125
+ def doubleclick(btn = "left")
126
+ not_supported "anything other than left clicking" unless btn == 'left'
127
+ execute_applescript(%Q`
128
+ tell application "Extra Suites"
129
+ ES click mouse with double click
130
+ end tell
131
+ `)
132
+ end
133
+
134
+
135
+ def tripleclick(btn = "left")
136
+ not_supported "anything other than left clicking" unless btn == 'left'
137
+ execute_applescript(%Q`
138
+ tell application "Extra Suites"
139
+ ES click mouse
140
+ ES click mouse
141
+ ES click mouse
142
+ end tell
143
+ `)
144
+ end
145
+
146
+
147
+ def mousedown(*args)
148
+ not_supported("mousedown")
149
+ end
150
+
151
+
152
+ def mouseup(*args)
153
+ not_supported("mouseup")
154
+ end
155
+
156
+
157
+ def drag(*options)
158
+ not_supported("drag")
159
+ end
160
+
161
+
162
+ private
163
+
164
+ def hit_with_extra_suites(key, *modifiers)
165
+ str = %Q{"#{ key }"}
166
+ if !modifiers.empty?
167
+ modifiers = modifiers.collect do |mod|
168
+ case mod
169
+ when Castanaut::Command
170
+ "command"
171
+ when Castanaut::Ctrl
172
+ "control"
173
+ when Castanaut::Alt
174
+ "option"
175
+ when Castanaut::Shift
176
+ "shift"
177
+ else
178
+ nil
179
+ end
180
+ end.select{ |mod| !mod.nil? }
181
+
182
+ str += modifiers.empty? ? "" : " with #{ modifiers.join(' and ') }"
183
+ end
184
+
185
+ %Q`
186
+ tell application "Extra Suites"
187
+ ES type key #{ str }
188
+ end tell
189
+ `
190
+ end
191
+
192
+
193
+ def hit_with_system_events(key, *modifiers)
194
+ str = key.hex.to_s
195
+ if !modifiers.empty?
196
+ modifiers = modifiers.collect do |mod|
197
+ case mod
198
+ when Castanaut::Command
199
+ "command down"
200
+ when Castanaut::Ctrl
201
+ "control down"
202
+ when Castanaut::Alt
203
+ "option down"
204
+ when Castanaut::Shift
205
+ "shift down"
206
+ else
207
+ nil
208
+ end
209
+ end.select{ |mod| !mod.nil? }
210
+ str += modifiers.empty? ? "" : " using {#{ modifiers.join(', ') }}"
211
+ end
212
+
213
+ %Q`
214
+ tell application "System Events"
215
+ key code #{ str }
216
+ end tell
217
+ `
218
+ end
219
+
220
+
221
+ end
222
+
223
+ end; end; end
@@ -16,8 +16,8 @@ module Castanaut
16
16
  # end
17
17
  # end
18
18
  # end
19
- #
20
- # The script must exist in a sub-directory of the screenplay's location
19
+ #
20
+ # The script must exist in a sub-directory of the screenplay's location
21
21
  # called "plugins", and must be called (in this case): foo.rb.
22
22
  #
23
23
  module Plugin
data/lib/castanaut.rb CHANGED
@@ -1,64 +1,60 @@
1
- # $Id$
2
-
3
1
  # Equivalent to a header guard in C/C++
4
2
  # Used to prevent the class/module from being loaded more than once
5
3
  unless defined? Castanaut
6
4
 
7
- # The Castanaut module. For orienting yourself within the code, it's
8
- # recommended you begin with the documentation for the Movie class,
9
- # which is the big one.
10
- #
11
- # Execution typically begins with the Main class.
12
- module Castanaut
13
-
14
- # :stopdoc:
15
- VERSION = '1.0.0'
16
- LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
17
- PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
18
-
19
- FILE_RUNNING = "/tmp/castanaut.running"
20
- FILE_APPLESCRIPT = "/tmp/castanaut.scpt"
21
- # :startdoc:
22
-
23
- # Returns the version string for the library.
24
- #
25
- def self.version
26
- VERSION
27
- end
28
-
29
- # Returns the library path for the module. If any arguments are given,
30
- # they will be joined to the end of the libray path using
31
- # <tt>File.join</tt>.
32
- #
33
- def self.libpath( *args )
34
- args.empty? ? LIBPATH : ::File.join(LIBPATH, *args)
35
- end
36
-
37
- # Returns the lpath for the module. If any arguments are given,
38
- # they will be joined to the end of the path using
39
- # <tt>File.join</tt>.
40
- #
41
- def self.path( *args )
42
- args.empty? ? PATH : ::File.join(PATH, *args)
43
- end
44
-
45
- # Utility method used to rquire all files ending in .rb that lie in the
46
- # directory below this file that has the same name as the filename passed
47
- # in. Optionally, a specific _directory_ name can be passed in such that
48
- # the _filename_ does not have to be equivalent to the directory.
5
+ # The Castanaut module. For orienting yourself within the code, it's
6
+ # recommended you begin with the documentation for the Movie class,
7
+ # which is the big one.
49
8
  #
50
- def self.require_all_libs_relative_to( fname, dir = nil )
51
- dir ||= ::File.basename(fname, '.*')
52
- search_me = ::File.expand_path(
53
- ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
54
-
55
- Dir.glob(search_me).sort.each {|rb| require rb}
56
- end
57
-
58
- end # module Castanaut
59
-
60
- Castanaut.require_all_libs_relative_to __FILE__
9
+ # Execution typically begins with the Main class.
10
+ module Castanaut
11
+
12
+ # :stopdoc:
13
+ VERSION = '1.1.0'
14
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
15
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
16
+
17
+ FILE_RUNNING = "/tmp/castanaut.running"
18
+ FILE_APPLESCRIPT = "/tmp/castanaut.scpt"
19
+ # :startdoc:
20
+
21
+ # Returns the version string for the library.
22
+ #
23
+ def self.version
24
+ VERSION
25
+ end
26
+
27
+ # Returns the library path for the module. If any arguments are given,
28
+ # they will be joined to the end of the libray path using
29
+ # <tt>File.join</tt>.
30
+ #
31
+ def self.libpath( *args )
32
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, *args)
33
+ end
34
+
35
+ # Returns the lpath for the module. If any arguments are given,
36
+ # they will be joined to the end of the path using
37
+ # <tt>File.join</tt>.
38
+ #
39
+ def self.path( *args )
40
+ args.empty? ? PATH : ::File.join(PATH, *args)
41
+ end
42
+
43
+ # Utility method used to rquire all files ending in .rb that lie in the
44
+ # directory below this file that has the same name as the filename passed
45
+ # in. Optionally, a specific _directory_ name can be passed in such that
46
+ # the _filename_ does not have to be equivalent to the directory.
47
+ #
48
+ def self.require_all_libs_relative_to( fname, dir = nil )
49
+ dir ||= ::File.basename(fname, '.*')
50
+ search_me = ::File.expand_path(
51
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
52
+
53
+ Dir.glob(search_me).sort.each {|rb| require rb}
54
+ end
55
+
56
+ end # module Castanaut
57
+
58
+ Castanaut.require_all_libs_relative_to __FILE__
61
59
 
62
60
  end # unless defined?
63
-
64
- # EOF