x_do 0.1.0
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/.document +5 -0
- data/.project +17 -0
- data/.rspec +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +31 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +27 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/lib/x_do.rb +13 -0
- data/lib/x_do/context.rb +101 -0
- data/lib/x_do/ffi_autogen.rb +37 -0
- data/lib/x_do/ffi_functions.rb +63 -0
- data/lib/x_do/ffi_lib.rb +45 -0
- data/lib/x_do/ffi_lib_ext.rb +62 -0
- data/lib/x_do/keyboard.rb +51 -0
- data/lib/x_do/mouse.rb +85 -0
- data/lib/x_do/window.rb +182 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/stdlib.rb +2 -0
- data/spec/x_do/context_spec.rb +127 -0
- data/spec/x_do/ffi_lib_ext_spec.rb +126 -0
- data/spec/x_do/keyboard_spec.rb +32 -0
- data/spec/x_do/mouse_spec.rb +53 -0
- data/spec/x_do/window_spec.rb +141 -0
- data/tasks/ffi_codegen.rb +108 -0
- metadata +252 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe XDo::FFILib do
|
4
|
+
describe 'XDoSearch#from_options' do
|
5
|
+
describe 'with no flag' do
|
6
|
+
let(:search) do
|
7
|
+
XDo::FFILib::XDoSearch.from_options
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should set flags to 0' do
|
11
|
+
search[:searchmask].should == 0
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should set require to AND' do
|
15
|
+
search[:require].should == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should set maximum depth to -1' do
|
19
|
+
search[:max_depth].should == -1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'with a set maximum depth' do
|
24
|
+
let(:search) do
|
25
|
+
XDo::FFILib::XDoSearch.from_options :depth => 42
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should not set any flags' do
|
29
|
+
search[:searchmask].should == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should set field correctly' do
|
33
|
+
search[:max_depth].should == 42
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'with require set to :any' do
|
38
|
+
let(:search) do
|
39
|
+
XDo::FFILib::XDoSearch.from_options :require => :any
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should not set any flags' do
|
43
|
+
search[:searchmask].should == 0
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should set require correctly' do
|
47
|
+
search[:require].should == 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'with 1 boolean flag' do
|
52
|
+
let(:search) do
|
53
|
+
XDo::FFILib::XDoSearch.from_options :visible => true
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should set flags correctly' do
|
57
|
+
search[:searchmask].should == XDo::FFILib::Consts::SEARCH_ONLYVISIBLE
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should set fields correctly' do
|
61
|
+
search[:only_visible].should == 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'with 1 int flag' do
|
66
|
+
let(:search) do
|
67
|
+
XDo::FFILib::XDoSearch.from_options :screen => 42
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should set flags correctly' do
|
71
|
+
search[:searchmask].should == XDo::FFILib::Consts::SEARCH_SCREEN
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should set field correctly' do
|
75
|
+
search[:screen].should == 42
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'with 1 string flag' do
|
80
|
+
let(:search) do
|
81
|
+
XDo::FFILib::XDoSearch.from_options :class => 'Normal'
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should set flags correctly' do
|
85
|
+
search[:searchmask].should == XDo::FFILib::Consts::SEARCH_CLASS
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should set fields correctly' do
|
89
|
+
search[:winclass].get_string(0).should == 'Normal'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'with 2 string flags' do
|
94
|
+
let(:search) do
|
95
|
+
XDo::FFILib::XDoSearch.from_options :title => 'Terminal',
|
96
|
+
:name => 'gnome-terminal'
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should set flags correctly' do
|
100
|
+
search[:searchmask].should == XDo::FFILib::Consts::SEARCH_NAME |
|
101
|
+
XDo::FFILib::Consts::SEARCH_TITLE
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should set fields correctly' do
|
105
|
+
search[:winname].get_string(0).should == 'gnome-terminal'
|
106
|
+
search[:title].get_string(0).should == 'Terminal'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'with mixed flags' do
|
111
|
+
let(:search) do
|
112
|
+
XDo::FFILib::XDoSearch.from_options :class_name => 'rbx', :pid => 42
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should set flags correctly' do
|
116
|
+
search[:searchmask].should == XDo::FFILib::Consts::SEARCH_CLASSNAME |
|
117
|
+
XDo::FFILib::Consts::SEARCH_PID
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should set fields correctly' do
|
121
|
+
search[:winclassname].get_string(0).should == 'rbx'
|
122
|
+
search[:pid].should == 42
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe XDo::Keyboard do
|
4
|
+
let(:xdo) { XDo.new }
|
5
|
+
let(:keyboard) { xdo.keyboard }
|
6
|
+
|
7
|
+
describe 'after pressing Alt+Tab' do
|
8
|
+
before do
|
9
|
+
@old_active_window = xdo.focused_window
|
10
|
+
keyboard.type_keysequence 'Alt_L+Tab'
|
11
|
+
sleep 0.1
|
12
|
+
end
|
13
|
+
after do
|
14
|
+
keyboard.type_keysequence 'Alt_L+Tab'
|
15
|
+
sleep 0.1
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should switch the focused window' do
|
19
|
+
xdo.focused_window.should_not == @old_active_window
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'after typing injected' do
|
24
|
+
before do
|
25
|
+
keyboard.type_string "injected\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should reflect the string in gets' do
|
29
|
+
$stdin.gets.should == "injected\n"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe XDo::Mouse do
|
4
|
+
let(:xdo) { XDo.new }
|
5
|
+
let(:mouse) { xdo.mouse }
|
6
|
+
|
7
|
+
describe 'location' do
|
8
|
+
let(:location) { mouse.location }
|
9
|
+
|
10
|
+
it 'should have 3 coordinates' do
|
11
|
+
location.should have(3).coordinates
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should have non-negative coordinates' do
|
15
|
+
location[0].should >= 0
|
16
|
+
location[1].should >= 0
|
17
|
+
location[2].should >= 0
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'after moving' do
|
21
|
+
let(:new_location) { [location[0] + 20, location[1] + 20, location[2]] }
|
22
|
+
before do
|
23
|
+
mouse.move(*new_location)
|
24
|
+
mouse.wait_for_move_from location[0], location[1]
|
25
|
+
end
|
26
|
+
after { mouse.move(*location) }
|
27
|
+
|
28
|
+
it 'should change to the move arguments' do
|
29
|
+
mouse.location.should == new_location
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should not block wait_for_move_to' do
|
33
|
+
lambda {
|
34
|
+
mouse.wait_for_move_to new_location[0], new_location[1]
|
35
|
+
}.should_not raise_error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'after relative moving' do
|
40
|
+
let(:new_location) { [location[0] + 20, location[1] + 20, location[2]] }
|
41
|
+
before do
|
42
|
+
new_location
|
43
|
+
mouse.move_relative 20, 20
|
44
|
+
mouse.wait_for_move_from location[0], location[1]
|
45
|
+
end
|
46
|
+
after { mouse.move(*location) }
|
47
|
+
|
48
|
+
it 'should change to the move arguments' do
|
49
|
+
mouse.location.should == new_location
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe XDo::Window do
|
4
|
+
let(:xdo) { XDo.new }
|
5
|
+
let(:all) { xdo.find_windows }
|
6
|
+
|
7
|
+
describe 'last window' do
|
8
|
+
let(:window) { all.last }
|
9
|
+
|
10
|
+
it 'should == another instance with the same window id' do
|
11
|
+
window.should == XDo::Window.new(xdo, window._window)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should hash to the same value as another instance with the same window id' do
|
15
|
+
window.hash.should == XDo::Window.new(xdo, window._window).hash
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should not == an instance with a different id' do
|
19
|
+
window.should_not == XDo::Window.new(xdo, window._window + 1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should not == a string' do
|
23
|
+
window.should_not == "window"
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should not == its id' do
|
27
|
+
window.should_not == window._window
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'active window' do
|
32
|
+
let(:window) { xdo.active_window }
|
33
|
+
it 'should have a non-zero pid' do
|
34
|
+
window.pid.should_not == 0
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'location' do
|
38
|
+
let(:location) { window.location }
|
39
|
+
|
40
|
+
it 'should have 2 coordinates' do
|
41
|
+
location.should have(2).coordinates
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should have non-negative coordinates' do
|
45
|
+
location.first.should >= 0
|
46
|
+
location.last.should >= 0
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'after moving' do
|
50
|
+
let(:new_location) { [location.first + 200, location.last + 200] }
|
51
|
+
before { window.move(*new_location) }
|
52
|
+
after { window.move(*location) }
|
53
|
+
|
54
|
+
it 'should change to the move arguments' do
|
55
|
+
window.location.should == new_location
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'size' do
|
61
|
+
let(:size) { window.size }
|
62
|
+
|
63
|
+
it 'should have 2 dimensions' do
|
64
|
+
size.should have(2).dimensions
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should have positive dimensions' do
|
68
|
+
size.first.should > 0
|
69
|
+
size.last.should > 0
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'after resizing' do
|
73
|
+
let(:new_size) { [size.first + 100, size.last + 100] }
|
74
|
+
|
75
|
+
before { window.resize(*new_size) }
|
76
|
+
after { window.resize(*size) }
|
77
|
+
|
78
|
+
it 'should change to approximately the resize arguments' do
|
79
|
+
window.size.first.should be_within(10).of(new_size.first)
|
80
|
+
window.size.last.should be_within(50).of(new_size.last)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'after mouse move in window center' do
|
86
|
+
before do
|
87
|
+
@old_mouse_location = xdo.mouse.location
|
88
|
+
size = window.size
|
89
|
+
middle = [size.first / 2, size.last / 2]
|
90
|
+
window.move_mouse(*middle)
|
91
|
+
end
|
92
|
+
after do
|
93
|
+
xdo.mouse.move(*@old_mouse_location)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should have moved the mouse cursor' do
|
97
|
+
xdo.mouse.location.should_not == @old_mouse_location
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'after right click' do
|
101
|
+
before do
|
102
|
+
@old_window_count = xdo.find_windows.length
|
103
|
+
window.click_mouse 3
|
104
|
+
sleep 0.1
|
105
|
+
end
|
106
|
+
|
107
|
+
after do
|
108
|
+
xdo.mouse.move_relative -20, -20
|
109
|
+
xdo.mouse.click 1
|
110
|
+
sleep 0.1
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should have popped up a context menu' do
|
114
|
+
xdo.find_windows.length.should_not == @old_window_count
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'after left click outside menu' do
|
118
|
+
before do
|
119
|
+
xdo.mouse.move_relative -20, -20
|
120
|
+
xdo.mouse.click 3
|
121
|
+
sleep 0.1
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should have dismissed context menu' do
|
125
|
+
xdo.find_windows.length.should == @old_window_count
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'after typing injected in the window' do
|
132
|
+
before do
|
133
|
+
window.type_string "injected\n"
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should reflect the string in gets' do
|
137
|
+
$stdin.gets.should == "injected\n"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
require 'ffi/tools/const_generator'
|
3
|
+
require 'ffi/tools/struct_generator'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
desc 'Regenerate ffi_autogen.rb'
|
7
|
+
task :ffi_header do
|
8
|
+
XDo::Tasks.generate_ffi_header
|
9
|
+
end
|
10
|
+
|
11
|
+
# :nodoc: namespace
|
12
|
+
module XDo
|
13
|
+
|
14
|
+
module Tasks
|
15
|
+
def self.resolve_constants(pattern, format)
|
16
|
+
if FFI::Platform.mac?
|
17
|
+
header_path = '/System/Library/Frameworks/PCSC.framework/Headers/'
|
18
|
+
else
|
19
|
+
header_path = '/usr/include/'
|
20
|
+
end
|
21
|
+
headers = Dir.glob(header_path + 'xdo*.h').map { |f| File.basename f }
|
22
|
+
|
23
|
+
consts = Set.new
|
24
|
+
headers.each do |header|
|
25
|
+
contents = File.read("#{header_path}#{header}")
|
26
|
+
contents.each_line do |line|
|
27
|
+
tokens = line.split
|
28
|
+
next unless tokens[0] == '#define'
|
29
|
+
next if tokens[1].index '('
|
30
|
+
consts << tokens[1] if pattern =~ tokens[1]
|
31
|
+
end
|
32
|
+
contents.scan /enum\s*\{([^}]*)\}/ do |match|
|
33
|
+
match[0].split(',').each do |enum_item|
|
34
|
+
enum_name = enum_item.split('=').first.strip
|
35
|
+
consts << enum_name if pattern =~ enum_name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
const_gen = FFI::ConstGenerator.new(nil,
|
41
|
+
:cppflags => "-w -I#{header_path}") do |g|
|
42
|
+
headers.each { |header| g.include header }
|
43
|
+
consts.each { |const| g.const const, format }
|
44
|
+
end
|
45
|
+
|
46
|
+
const_gen.constants
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.output_constants(constants, f)
|
50
|
+
constants.each do |name, const|
|
51
|
+
value = const.to_ruby
|
52
|
+
value += 'nil' if value.strip[-1] == ?=
|
53
|
+
f.write " #{value}\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.output_enum(enum_name, constants, name_regexp, f)
|
58
|
+
f.write " #{enum_name} = enum [\n"
|
59
|
+
|
60
|
+
re = Regexp.new('^' + name_regexp + '$')
|
61
|
+
constants.each do |name, const|
|
62
|
+
ruby_name = re.match(name)[1]
|
63
|
+
f.write " :#{ruby_name.downcase}, Consts::#{name},\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
f.write " ]\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.generate_ffi_header
|
70
|
+
File.open('lib/x_do/ffi_autogen.rb', 'wb') do |f|
|
71
|
+
f.write "# Automatically generated by tasks/ffi_codegen.rb\n\n"
|
72
|
+
|
73
|
+
# Return codes.
|
74
|
+
stat_consts = resolve_constants(/^XDO_.*$/, '%d')
|
75
|
+
# Search flags.
|
76
|
+
search_consts = resolve_constants(/^SEARCH_/, '0x%04X')
|
77
|
+
# Window sizing flags.
|
78
|
+
sizing_consts = resolve_constants(/^SIZE_/, '%d')
|
79
|
+
# Search direction.
|
80
|
+
direction_consts = resolve_constants(/^XDO_FIND_/, '%d')
|
81
|
+
|
82
|
+
f.write "# :nodoc: namespace\n"
|
83
|
+
f.write "class XDo\n\n"
|
84
|
+
f.write "# :nodoc: namespace\n"
|
85
|
+
f.write "module FFILib\n"
|
86
|
+
|
87
|
+
f.write " # Constant values extracted from headers.\n"
|
88
|
+
f.write " module Consts\n"
|
89
|
+
output_constants stat_consts, f
|
90
|
+
output_constants search_consts, f
|
91
|
+
output_constants sizing_consts, f
|
92
|
+
output_constants direction_consts, f
|
93
|
+
f.write " end # module XDo::FFILib::Consts\n\n"
|
94
|
+
|
95
|
+
f.write " # Status returned by libxdo functions.\n"
|
96
|
+
output_enum 'Status', stat_consts, 'XDO_(.*)', f
|
97
|
+
f.write "\n"
|
98
|
+
|
99
|
+
f.write " # Search directions.\n"
|
100
|
+
output_enum 'Direction', direction_consts, 'XDO_FIND_(.*)', f
|
101
|
+
f.write "\n"
|
102
|
+
|
103
|
+
f.write "end # namespace XDo::FFILib\n"
|
104
|
+
f.write "end # namespace XDo\n"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end # namespace XDo::Tasks
|
108
|
+
end # namespace XDo
|