topfunky-castanaut 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ == Castanaut
2
+
3
+ Copyright (C) 2008 Inventive Labs.
4
+
5
+ This program is free software. It comes without any warranty, to
6
+ the extent permitted by applicable law. You can redistribute it
7
+ and/or modify it under the terms of the Do What The Fuck You Want
8
+ To Public License, Version 2, as published by Sam Hocevar. See
9
+ http://sam.zoy.org/wtfpl/COPYING for more details.
10
+
11
+ === DomQuery
12
+
13
+ The DomQuery implementation is included from the Ext JS distribution, which
14
+ uses the MIT license requiring the following copyright and permission
15
+ notices. These pertain only to the script/gebys.js file.
16
+
17
+ Copyright (c) 2006-2007 Ext JS, LLC.
18
+
19
+ Permission is hereby granted, free of charge, to any person
20
+ obtaining a copy of this software and associated documentation
21
+ files (the "Software"), to deal in the Software without
22
+ restriction, including without limitation the rights to use,
23
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
24
+ copies of the Software, and to permit persons to whom the
25
+ Software is furnished to do so, subject to the following
26
+ conditions:
27
+
28
+ The above copyright notice and this permission notice shall be
29
+ included in all copies or substantial portions of the Software.
@@ -0,0 +1,9 @@
1
+ === 1.0.1 / 2008-09-10
2
+
3
+ * Added click_menu_item function. [topfunky]
4
+ * Added basic TextMate plugin. Name is textmate to comply with Castanaut expectations. [topfunky]
5
+ * Added gemspec.
6
+
7
+ === 1.0.0 / 2008-02-21
8
+
9
+ * Initial release.
@@ -0,0 +1,31 @@
1
+ Copyright.txt
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/castanaut
7
+ cbin/osxautomation
8
+ lib/castanaut.rb
9
+ lib/castanaut/exceptions.rb
10
+ lib/castanaut/keys.rb
11
+ lib/castanaut/main.rb
12
+ lib/castanaut/movie.rb
13
+ lib/castanaut/plugin.rb
14
+ lib/plugins/ishowu.rb
15
+ lib/plugins/mousepose.rb
16
+ lib/plugins/safari.rb
17
+ lib/plugins/textmate.rb
18
+ scripts/coords.js
19
+ scripts/gebys.js
20
+ spec/castanaut_spec.rb
21
+ spec/spec_helper.rb
22
+ tasks/ann.rake
23
+ tasks/annotations.rake
24
+ tasks/doc.rake
25
+ tasks/gem.rake
26
+ tasks/manifest.rake
27
+ tasks/post_load.rake
28
+ tasks/rubyforge.rake
29
+ tasks/setup.rb
30
+ tasks/spec.rake
31
+ tasks/svn.rake
@@ -0,0 +1,161 @@
1
+ = Castanaut: Automate your screencasts.
2
+
3
+ Author: Joseph Pearson
4
+ http://gadgets.inventivelabs.com.au/castanaut
5
+
6
+ == DESCRIPTION:
7
+
8
+ Castanaut lets you write executable scripts for your screencasts. With a
9
+ simple dictionary of stage directions, you can create complex interactions
10
+ with a variety of applications. Currently, and for the foreseeable future,
11
+ Castanaut supports Mac OS X 10.5 only.
12
+
13
+ == SYNOPSIS:
14
+
15
+ === Writing screenplays
16
+
17
+ You write your screenplays as Ruby files. Castanaut has been designed to
18
+ read fairly naturally to the non-technical, within Ruby's constraints.
19
+
20
+ Here's a simple screenplay:
21
+
22
+ launch "Safari", at(10, 10, 800, 600)
23
+ type "http://www.inventivelabs.com.au"
24
+ hit Enter
25
+ pause 2
26
+ move to(100, 100)
27
+ move to(200, 100)
28
+ move to(200, 200)
29
+ move to(100, 200)
30
+ move to(100, 100)
31
+ say "I drew a square!"
32
+
33
+ With any luck we don't need to explain to you what this screenplay
34
+ does. The only thing that might need some explanation is "say" -- this has a
35
+ robotic voice speak the given string. (Also: all numbers are pixel
36
+ co-ordinates).
37
+
38
+ About the robot: no, we don't recommend you use this in real screencasts for
39
+ a large audience. Most people find it a little offputting.
40
+ You are free to contravene our recommendation though. You
41
+ can tweak the robot in the Mac OS X Speech Preferences pane.
42
+
43
+ === Running your screenplay
44
+
45
+ Simply give the screenplay to the castanaut command, like this:
46
+
47
+ castanaut test.screenplay
48
+
49
+ This assumes you have a screenplay file called "test.screenplay" in the
50
+ directory where you are running the command.
51
+
52
+ Of course, it isn't always convenient to drop to the terminal to run your
53
+ screenplay. So there's also a method of executing your screenplays directly.
54
+ You need to add this line (the "shebang" line) at the top of your screenplay:
55
+
56
+ #!/usr/bin/env castanaut
57
+
58
+ Then you need to set the screenplay to be executable by running this command
59
+ on it:
60
+
61
+ chmod a+x test.screenplay
62
+
63
+ Again, substitute "test.screenplay" for your screenplay's filename.
64
+
65
+ At this point, you should be able to double-click the screenplay, or invoke
66
+ it with Quicksilver, or run it any other way that floats your boat.
67
+
68
+ === Stopping the screenplay
69
+
70
+ If you want to abruptly terminate execution before the end of the screenplay,
71
+ you just need to run the 'castanaut' command again -- with or without any
72
+ arguments.
73
+
74
+ Of course, that might be easier said than done, if you haven't got full
75
+ control of the mouse or keyboard at the time. One recommendation is to assign
76
+ a system hot-key to invoke castanaut. I use a Quicksilver trigger for this,
77
+ assigned to Shift-F1, that calls castanaut. You'll need the full path to
78
+ the command for this, which is usually /usr/bin/castanaut, but you can check
79
+ it with the following command:
80
+
81
+ which "castanaut"
82
+
83
+
84
+ === What stage directions can I make?
85
+
86
+ Out of the box, Castanaut performs mouse actions, keyboard actions,
87
+ robot speech and application launches.
88
+
89
+ For a complete overview of the built-in stage directions, see the
90
+ Castanaut::Movie class.
91
+
92
+ === Using plugins
93
+
94
+ Of course, just using the built-in stage directions is a little bit awkward
95
+ and verbose. Plugins allow you to extend the available dictionary with
96
+ some additional convenience actions. Typically a plugin is specific to an
97
+ application.
98
+
99
+ Castanaut comes with several plugins, including Castanaut::Plugin::Safari for
100
+ interacting with the contents of web-pages, and Castanaut::Plugin::Ishowu for
101
+ recording screencasts using the iShowU application from Shiny White Box.
102
+
103
+ To use a plugin, simply declare it:
104
+
105
+ plugin "safari"
106
+
107
+ launch "Safari", at(32, 32, 800, 600)
108
+ url "http://www.google.com"
109
+ pause 4
110
+ move to_element('input[name="q"]')
111
+ click
112
+ type "Castanaut"
113
+ move to_element('input[type="submit"]')
114
+ click
115
+ pause 4
116
+ say "Oh. I was hoping for more results."
117
+
118
+
119
+ In the example above, we use the two methods provided by the Safari module:
120
+ url, which causes Safari to navigate to the given url, and to_element, which
121
+ returns the co-ordinates of a page element (using CSS selectors) relative to
122
+ the screen.
123
+
124
+ === Creating your own plugins
125
+
126
+ Advanced users can create their own plugins. Put them in a directory
127
+ called "plugins" below the directory containing the screenplays that use
128
+ the plugin.
129
+
130
+ Take a look at the plugins that Castanaut comes with for examples on creating
131
+ your own.
132
+
133
+ == REQUIREMENTS:
134
+
135
+ * Mac OS X 10.5
136
+
137
+ == INSTALL:
138
+
139
+ Run the following command to install Castanaut
140
+
141
+ sudo gem install castanaut
142
+
143
+ Once installed, you should run the following command for two reasons:
144
+
145
+ castanaut
146
+
147
+ Reason 1 is to confirm that it is installed correctly. Reason 2 is to set up
148
+ the permissions on the utility that controls your mouse and keyboard during
149
+ Castanaut movies. You may be asked for a password here.
150
+
151
+ If you just see a "ScreenplayNotFound" exception here, everything's good.
152
+
153
+ == LICENSE:
154
+
155
+ Copyright (C) 2008 Inventive Labs.
156
+
157
+ Released under the WTFPL: http://sam.zoy.org/wtfpl.
158
+
159
+ Portions released under the MIT License.
160
+
161
+ See Copyright.txt for full licensing details.
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/castanaut.rb'
6
+
7
+ task :default => 'spec:run'
8
+
9
+ Hoe.new('castanaut', Castanaut::VERSION) do |p|
10
+ p.developer('Joseph Pearson', 'joseph@inventivelabs.com.au')
11
+ p.description = "Automate your screencasts."
12
+ p.summary = "Automate your screencasts."
13
+ p.url = "http://castanaut.rubyforge.org"
14
+ p.remote_rdoc_dir = 'doc'
15
+
16
+ end
17
+
18
+ # PROJ.exclude += ['^spec\/*', '^test\/*']
19
+ # PROJ.spec_opts << '--color'
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), '..', 'lib', 'castanaut')
5
+ )
6
+
7
+ Castanaut::Main.run ARGV
Binary file
@@ -0,0 +1,64 @@
1
+ # $Id$
2
+
3
+ # Equivalent to a header guard in C/C++
4
+ # Used to prevent the class/module from being loaded more than once
5
+ unless defined? Castanaut
6
+
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.1'
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.
49
+ #
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__
61
+
62
+ end # unless defined?
63
+
64
+ # EOF
@@ -0,0 +1,32 @@
1
+ module Castanaut
2
+ # All Castanaut errors are defined within this module. If you are creating
3
+ # a plugin, you should re-open this module in your plugin script file to
4
+ # add any plugin-specific exceptions (it's also a good idea to have them
5
+ # descend from CastanautError).
6
+ module Exceptions
7
+ # The abstract parent class of all Castanaut errors.
8
+ class CastanautError < RuntimeError
9
+ end
10
+
11
+ # Raised if Castanaut was invoked with no screenplay argument, or one
12
+ # pointing to a non-existent file.
13
+ class ScreenplayNotFound < CastanautError
14
+ end
15
+
16
+ # If Castanaut::Movie#run sees a non-zero exit status from the shell
17
+ # process, this error will be raised.
18
+ class ExternalActionError < CastanautError
19
+ end
20
+
21
+ # If the FILE_RUNNING flag file is deleted or moved during the execution
22
+ # of a movie, it will terminate and raise this exception.
23
+ class AbortedByUser < CastanautError
24
+ end
25
+
26
+ # Despite asking for permission, the osxautomation utility in cbin cannot
27
+ # be executed. This is pretty fatal to our intentions, so we abort with
28
+ # this exception.
29
+ class OSXAutomationPermissionError < CastanautError
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ module Castanaut
2
+
3
+ # Some standard keys (for use with 'hit'), presumably only for US keyboards.
4
+
5
+ #
6
+ Return = "0x24"
7
+ Enter = "0x4C"
8
+ Tab = "0x30"
9
+ Space = "0x31"
10
+ Backspace = "0x33"
11
+ Esc = "0x35"
12
+
13
+ Shift = "0x38"
14
+ CapsLock = "0x39"
15
+ Alt = "0x3A"
16
+ Ctrl = "0x3B"
17
+
18
+ LArrow = "0x7B"
19
+ RArrow = "0x7C"
20
+ DArrow = "0x7D"
21
+ UArrow = "0x7E"
22
+
23
+ Insert = "0x72"
24
+ Home = "0x73"
25
+ PageUp = "0x74"
26
+ Delete = "0x75"
27
+ End = "0x77"
28
+ PageDown = "0x79"
29
+
30
+ F1 = "0x7A"
31
+ F2 = "0x78"
32
+ F3 = "0x63"
33
+ F4 = "0x76"
34
+ F5 = "0x60"
35
+ F6 = "0x61"
36
+ F7 = "0x62"
37
+ F8 = "0x64"
38
+ F9 = "0x65"
39
+ F10 = "0x6D"
40
+ F11 = "0x67"
41
+ F12 = "0x6F"
42
+
43
+ end
@@ -0,0 +1,20 @@
1
+ module Castanaut
2
+
3
+ # When running the Castanaut library as an executable, this class manages
4
+ # the invocation of the user-specified screenplay.
5
+ class Main
6
+
7
+ # If Castanaut is not running, this runs the movie specified as the first
8
+ # argument. If it *is* already running, this nixes the flag file, which
9
+ # should cause Castanaut to stop.
10
+ def self.run(args)
11
+ if File.exists?(Castanaut::FILE_RUNNING)
12
+ File.unlink(Castanaut::FILE_RUNNING)
13
+ else
14
+ Castanaut::Movie.new(args.shift)
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,428 @@
1
+ module Castanaut
2
+ # The movie class is the containing context within which screenplays are
3
+ # invoked. It provides a number of basic stage directions for your
4
+ # screenplays, and can be extended with plugins.
5
+ class Movie
6
+
7
+ # Runs the "screenplay", which is a file containing Castanaut instructions.
8
+ #
9
+ def initialize(screenplay)
10
+ perms_test
11
+
12
+ if !screenplay || !File.exists?(screenplay)
13
+ raise Castanaut::Exceptions::ScreenplayNotFound
14
+ end
15
+ @screenplay_path = screenplay
16
+
17
+ File.open(FILE_RUNNING, 'w') {|f| f.write('')}
18
+
19
+ begin
20
+ # We run the movie in a separate thread; in the main thread we
21
+ # continue to check the "running" file flag and kill the movie if
22
+ # it is removed.
23
+ movie = Thread.new do
24
+ begin
25
+ eval(IO.read(@screenplay_path), binding)
26
+ rescue => e
27
+ @e = e
28
+ ensure
29
+ File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
30
+ end
31
+ end
32
+
33
+ while File.exists?(FILE_RUNNING)
34
+ sleep 0.5
35
+ break unless movie.alive?
36
+ end
37
+
38
+ if movie.alive?
39
+ movie.kill
40
+ raise Castanaut::Exceptions::AbortedByUser
41
+ end
42
+
43
+ raise @e if @e
44
+ rescue => e
45
+ puts "ABNORMAL EXIT: #{e.message}\n" + e.backtrace.join("\n")
46
+ ensure
47
+ roll_credits
48
+ File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
49
+ end
50
+ end
51
+
52
+ # Launch the application matching the string given in the first argument.
53
+ # (This resolution is handled by Applescript.)
54
+ #
55
+ # If the options hash is given, it should contain the co-ordinates for
56
+ # the window (top, left, width, height). The to method will format these
57
+ # co-ordinates appropriately.
58
+ #
59
+ def launch(app_name, *options)
60
+ options = combine_options(*options)
61
+
62
+ ensure_window = ""
63
+ case app_name.downcase
64
+ when "safari"
65
+ ensure_window = "if (count(windows)) < 1 then make new document"
66
+ end
67
+
68
+ positioning = ""
69
+ if options[:to]
70
+ pos = "#{options[:to][:left]}, #{options[:to][:top]}"
71
+ dims = "#{options[:to][:left] + options[:to][:width]}, " +
72
+ "#{options[:to][:top] + options[:to][:height]}"
73
+ if options[:to][:width]
74
+ positioning = "set bounds of front window to {#{pos}, #{dims}}"
75
+ else
76
+ positioning = "set position of front window to {#{pos}}"
77
+ end
78
+ end
79
+
80
+ execute_applescript(%Q`
81
+ tell application "#{app_name}"
82
+ activate
83
+ #{ensure_window}
84
+ #{positioning}
85
+ end tell
86
+ `)
87
+ end
88
+
89
+ # Move the mouse cursor to the specified co-ordinates.
90
+ #
91
+ def cursor(*options)
92
+ options = combine_options(*options)
93
+ apply_offset(options)
94
+ @cursor_loc ||= {}
95
+ @cursor_loc[:x] = options[:to][:left]
96
+ @cursor_loc[:y] = options[:to][:top]
97
+ automatically "mousemove #{@cursor_loc[:x]} #{@cursor_loc[:y]}"
98
+ end
99
+
100
+ alias :move :cursor
101
+
102
+ # Send a mouse-click at the current mouse location.
103
+ #
104
+ def click(btn = 'left')
105
+ automatically "mouseclick #{mouse_button_translate(btn)}"
106
+ end
107
+
108
+ # Send a double-click at the current mouse location.
109
+ #
110
+ def doubleclick(btn = 'left')
111
+ automatically "mousedoubleclick #{mouse_button_translate(btn)}"
112
+ end
113
+
114
+ # Send a triple-click at the current mouse location.
115
+ #
116
+ def tripleclick(btn = 'left')
117
+ automatically "mousetripleclick #{mouse_button_translate(btn)}"
118
+ end
119
+
120
+ # Press the button down at the current mouse location. Does not
121
+ # release the button until the mouseup method is invoked.
122
+ #
123
+ def mousedown(btn = 'left')
124
+ automatically "mousedown #{mouse_button_translate(btn)}"
125
+ end
126
+
127
+ # Releases the mouse button pressed by a previous mousedown.
128
+ #
129
+ def mouseup(btn = 'left')
130
+ automatically "mouseup #{mouse_button_translate(btn)}"
131
+ end
132
+
133
+ # "Drags" the mouse by (effectively) issuing a mousedown at the current
134
+ # mouse location, then moving the mouse to the specified coordinates, then
135
+ # issuing a mouseup.
136
+ #
137
+ def drag(*options)
138
+ options = combine_options(*options)
139
+ apply_offset(options)
140
+ automatically "mousedrag #{options[:to][:left]} #{options[:to][:top]}"
141
+ end
142
+
143
+ # Sends the characters into the active control in the active window.
144
+ #
145
+ def type(str)
146
+ automatically "type #{str}"
147
+ end
148
+
149
+ # Sends the keycode (a hex value) to the active control in the active
150
+ # window. For more about keycode values, see Mac Developer documentation.
151
+ #
152
+ def hit(key)
153
+ automatically "hit #{key}"
154
+ end
155
+
156
+ # Don't do anything for the specified number of seconds (can be portions
157
+ # of a second).
158
+ #
159
+ def pause(seconds)
160
+ sleep seconds
161
+ end
162
+
163
+ # Use Leopard's native text-to-speech functionality to emulate a human
164
+ # voice saying the narrative text.
165
+ #
166
+ def say(narrative)
167
+ run(%Q`say "#{escape_dq(narrative)}"`)
168
+ end
169
+
170
+ ##
171
+ # Click a menu item in any application.
172
+ #
173
+ # The name of the application should be the first argument.
174
+ #
175
+ # Three dots will be automatically replaced by the appropriate ellipsis.
176
+ #
177
+ # click_menu_item("TextMate", "Navigation", "Go to Symbol...")
178
+
179
+ def click_menu_item(*items)
180
+ items_as_applescript_array = items.map {|i| i.gsub!('...', "…"); %("#{i}")}.join(", ")
181
+ ascript = %Q(
182
+ -- menu_click, by Jacob Rus, September 2006
183
+ -- http://www.macosxhints.com/article.php?story=20060921045743404
184
+ --
185
+ -- Accepts a list of form: `{"Finder", "View", "Arrange By", "Date"}`
186
+ -- Execute the specified menu item. In this case, assuming the Finder
187
+ -- is the active application, arranging the frontmost folder by date.
188
+
189
+ on menu_click(mList)
190
+ local appName, topMenu, r
191
+
192
+ -- Validate our input
193
+ if mList's length < 3 then error "Menu list is not long enough"
194
+
195
+ -- Set these variables for clarity and brevity later on
196
+ set {appName, topMenu} to (items 1 through 2 of mList)
197
+ set r to (items 3 through (mList's length) of mList)
198
+
199
+ -- This overly-long line calls the menu_recurse function with
200
+ -- two arguments: r, and a reference to the top-level menu
201
+ tell application "System Events" to my menu_click_recurse(r, ((process appName)'s ¬
202
+ (menu bar 1)'s (menu bar item topMenu)'s (menu topMenu)))
203
+ end menu_click
204
+
205
+ on menu_click_recurse(mList, parentObject)
206
+ local f, r
207
+
208
+ -- `f` = first item, `r` = rest of items
209
+ set f to item 1 of mList
210
+ if mList's length > 1 then set r to (items 2 through (mList's length) of mList)
211
+
212
+ -- either actually click the menu item, or recurse again
213
+ tell application "System Events"
214
+ if mList's length is 1 then
215
+ click parentObject's menu item f
216
+ else
217
+ my menu_click_recurse(r, (parentObject's (menu item f)'s (menu f)))
218
+ end if
219
+ end tell
220
+ end menu_click_recurse
221
+
222
+
223
+ menu_click({#{items_as_applescript_array}})
224
+ )
225
+ execute_applescript(ascript)
226
+ end
227
+
228
+ ##
229
+ # Warning: FLAKY
230
+ #
231
+ # Hit a command key combo.
232
+ #
233
+ # Use lowercase for normal, or uppercase if shift should be used also.
234
+ #
235
+ # Option and Ctrl aren't currently supported.
236
+
237
+ def keystroke(character)
238
+ execute_applescript(%Q'
239
+ tell application "System Events"
240
+ keystroke "#{character}"
241
+ end tell
242
+ ')
243
+ end
244
+
245
+ # Starts saying the narrative text, and simultaneously begins executing
246
+ # the given block. Waits until both are finished.
247
+ #
248
+ def while_saying(narrative)
249
+ if block_given?
250
+ fork { say(narrative) }
251
+ yield
252
+ Process.wait
253
+ else
254
+ say(narrative)
255
+ end
256
+ end
257
+
258
+ # Get a hash representing specific screen co-ordinates. Use in combination
259
+ # with cursor, drag, launch, and similar methods.
260
+ #
261
+ def to(l, t, w = nil, h = nil)
262
+ result = {
263
+ :to => {
264
+ :left => l,
265
+ :top => t
266
+ }
267
+ }
268
+ result[:to][:width] = w if w
269
+ result[:to][:height] = h if h
270
+ result
271
+ end
272
+
273
+ alias :at :to
274
+
275
+ # Get a hash representing specific screen co-ordinates *relative to the
276
+ # current mouse location.
277
+ #
278
+ def by(x, y)
279
+ unless @cursor_loc
280
+ @cursor_loc = automatically("mouselocation").strip.split(' ')
281
+ @cursor_loc = {:x => @cursor_loc[0].to_i, :y => @cursor_loc[1].to_i}
282
+ end
283
+ to(@cursor_loc[:x] + x, @cursor_loc[:y] + y)
284
+ end
285
+
286
+ # The result of this method can be added +to+ a co-ordinates hash,
287
+ # offsetting the top and left values by the given margins.
288
+ #
289
+ def offset(x, y)
290
+ { :offset => { :x => x, :y => y } }
291
+ end
292
+
293
+
294
+ # Returns a region hash describing the entire screen area. (May be wonky
295
+ # for multi-monitor set-ups.)
296
+ #
297
+ def screen_size
298
+ coords = execute_applescript(%Q`
299
+ tell application "Finder"
300
+ get bounds of window of desktop
301
+ end tell
302
+ `)
303
+ coords = coords.split(", ").collect {|c| c.to_i}
304
+ to(*coords)
305
+ end
306
+
307
+ # Runs a shell command, performing fairly naive (but effective!) exit
308
+ # status handling. Returns the stdout result of the command.
309
+ #
310
+ def run(cmd)
311
+ #puts("Executing: #{cmd}")
312
+ result = `#{cmd}`
313
+ raise Castanaut::Exceptions::ExternalActionError if $?.exitstatus > 0
314
+ result
315
+ end
316
+
317
+ # Adds custom methods to this movie instance, allowing you to perform
318
+ # additional actions. See the README.txt for more information.
319
+ #
320
+ def plugin(str)
321
+ str.downcase!
322
+ begin
323
+ require File.join(File.dirname(@screenplay_path),"plugins","#{str}.rb")
324
+ rescue LoadError
325
+ require File.join(LIBPATH, "plugins", "#{str}.rb")
326
+ end
327
+ extend eval("Castanaut::Plugin::#{str.capitalize}")
328
+ end
329
+
330
+ # Loads a script from a file into a string, looking first in the
331
+ # scripts directory beneath the path where Castanaut was executed,
332
+ # and falling back to Castanaut's gem path.
333
+ #
334
+ def script(filename)
335
+ @cached_scripts ||= {}
336
+ unless @cached_scripts[filename]
337
+ fpath = File.join(File.dirname(@screenplay_path), "scripts", filename)
338
+ scpt = nil
339
+ if File.exists?(fpath)
340
+ scpt = IO.read(fpath)
341
+ else
342
+ scpt = IO.read(File.join(PATH, "scripts", filename))
343
+ end
344
+ @cached_scripts[filename] = scpt
345
+ end
346
+
347
+ @cached_scripts[filename]
348
+ end
349
+
350
+ # This stage direction is slightly different to the other ones. It collects
351
+ # a set of directions to be executed when the movie ends, or when it is
352
+ # aborted by the user. Mostly, it's used for cleaning up stuff. Here's
353
+ # an example:
354
+ #
355
+ # ishowu_start_recording
356
+ # at_end_of_movie do
357
+ # ishowu_stop_recording
358
+ # end
359
+ # move to(100, 100) # ... et cetera
360
+ #
361
+ # You can use this multiple times in your screenplay -- remember that if
362
+ # the movie is aborted by the user before this direction is used, its
363
+ # contents won't be executed. So in general, create an at_end_of_movie
364
+ # block after every action that you want to revert (like in the example
365
+ # above).
366
+ def at_end_of_movie(&blk)
367
+ @end_credits ||= []
368
+ @end_credits << blk
369
+ end
370
+
371
+ protected
372
+ def execute_applescript(scpt)
373
+ File.open(FILE_APPLESCRIPT, 'w') {|f| f.write(scpt)}
374
+ result = run("osascript #{FILE_APPLESCRIPT}")
375
+ File.unlink(FILE_APPLESCRIPT)
376
+ result
377
+ end
378
+
379
+ def automatically(cmd)
380
+ run("#{osxautomation_path} \"#{cmd}\"")
381
+ end
382
+
383
+ def escape_dq(str)
384
+ str.gsub(/\\/,'\\\\\\').gsub(/"/, '\"')
385
+ end
386
+
387
+ def combine_options(*args)
388
+ options = args.inject({}) { |result, option| result.update(option) }
389
+ end
390
+
391
+ private
392
+ def osxautomation_path
393
+ File.join(PATH, "cbin", "osxautomation")
394
+ end
395
+
396
+ def perms_test
397
+ return if File.executable?(osxautomation_path)
398
+ puts "IMPORTANT: Castanaut has recently been installed or updated. " +
399
+ "You need to give it the right to control mouse and keyboard " +
400
+ "input during screenplays."
401
+
402
+ run("sudo chmod a+x #{osxautomation_path}")
403
+
404
+ if File.executable?(osxautomation_path)
405
+ puts "Permission granted. Thanks."
406
+ else
407
+ raise Castanaut::Exceptions::OSXAutomationPermissionError
408
+ end
409
+ end
410
+
411
+ def apply_offset(options)
412
+ return unless options[:to] && options[:offset]
413
+ options[:to][:left] += options[:offset][:x] || 0
414
+ options[:to][:top] += options[:offset][:y] || 0
415
+ end
416
+
417
+ def mouse_button_translate(btn)
418
+ return btn if btn.is_a?(Integer)
419
+ {"left" => 1, "right" => 2, "middle" => 3}[btn]
420
+ end
421
+
422
+ def roll_credits
423
+ return unless @end_credits && @end_credits.any?
424
+ @end_credits.each {|credit| credit.call}
425
+ end
426
+
427
+ end
428
+ end