castanaut 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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