topfunky-castanaut 1.0.1

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,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