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.
- data/Copyright.txt +29 -0
- data/History.txt +9 -0
- data/Manifest.txt +31 -0
- data/README.txt +161 -0
- data/Rakefile +19 -0
- data/bin/castanaut +7 -0
- data/cbin/osxautomation +0 -0
- data/lib/castanaut.rb +64 -0
- data/lib/castanaut/exceptions.rb +32 -0
- data/lib/castanaut/keys.rb +43 -0
- data/lib/castanaut/main.rb +20 -0
- data/lib/castanaut/movie.rb +428 -0
- data/lib/castanaut/plugin.rb +26 -0
- data/lib/plugins/ishowu.rb +94 -0
- data/lib/plugins/mousepose.rb +38 -0
- data/lib/plugins/safari.rb +124 -0
- data/lib/plugins/textmate.rb +39 -0
- data/scripts/coords.js +48 -0
- data/scripts/gebys.js +612 -0
- data/spec/castanaut_spec.rb +9 -0
- data/spec/spec_helper.rb +18 -0
- data/tasks/ann.rake +77 -0
- data/tasks/annotations.rake +22 -0
- data/tasks/doc.rake +49 -0
- data/tasks/gem.rake +110 -0
- data/tasks/manifest.rake +50 -0
- data/tasks/post_load.rake +18 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +221 -0
- data/tasks/spec.rake +43 -0
- data/tasks/svn.rake +44 -0
- metadata +96 -0
data/Copyright.txt
ADDED
|
@@ -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.
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
|
@@ -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
|
data/README.txt
ADDED
|
@@ -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.
|
data/Rakefile
ADDED
|
@@ -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'
|
data/bin/castanaut
ADDED
data/cbin/osxautomation
ADDED
|
Binary file
|
data/lib/castanaut.rb
ADDED
|
@@ -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
|