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