deskbot 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/Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/deskbot"]
7
+ resolver = "2"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Nolan J Tait
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # Deskbot
2
+
3
+ This is a dekstop automation library written using
4
+ [autopilot-rs](https://github.com/autopilot-rs) and
5
+ [rust extensions](https://bundler.io/blog/2023/01/31/rust-gem-skeleton.html)
6
+ for Ruby.
7
+
8
+ This library uses [dry-types](https://dry-rb.org/gems/dry-types/1.7/) so the
9
+ arguments should be well documented in
10
+ the `lib/deskbot/screen.rb` and `lib/deskbot/bitmap.rb` files
11
+
12
+ The default implementation of finding images with autopilot was unbearably slow,
13
+ so I've rewritten matching templates to use
14
+ [opencv](https://github.com/twistedfall/opencv-rust). So ensure you have opencv
15
+ [installed](https://github.com/twistedfall/opencv-rust/blob/master/INSTALL.md).
16
+
17
+ ## Installation
18
+
19
+ Install the gem and add to the application's Gemfile by executing:
20
+
21
+ $ bundle add deskbot
22
+
23
+ If bundler is not being used to manage dependencies, install the gem by executing:
24
+
25
+ $ gem install deskbot
26
+
27
+ ## Usage
28
+
29
+ To start you can require the gem in your script and initialize a screen:
30
+
31
+ ```ruby
32
+ require "deskbot"
33
+
34
+ screen = Deskbot.screen
35
+ ```
36
+
37
+ ### Typing
38
+
39
+ Type something on the keyboard
40
+
41
+ ```ruby
42
+ # Type SOMETHING at 60 words per minute
43
+ screen.type("something", flags: [:shift], wpm: 60.0, noise: 0.0)
44
+ ```
45
+
46
+ You can also tap a key:
47
+
48
+ ```ruby
49
+ # Tap shift + a after a 1 second delay
50
+ screen.tap_key("a", flags: [:shift], delay_ms: 1.0)
51
+ ```
52
+
53
+ And even more primitively you can toggle a key:
54
+
55
+ ```ruby
56
+ # Press the "a" key down
57
+ screen.toggle_key("a", down: true)
58
+ ```
59
+
60
+ ### Alerts
61
+
62
+ You can make alerts
63
+
64
+ ```ruby
65
+ screen.alert("Hello")
66
+ ```
67
+
68
+ ### Mouse movement
69
+
70
+ You can teleport your mouse somewhere on the screen:
71
+
72
+ ```ruby
73
+ # Teleport the mouse to coordinates x: 100, y: 100
74
+ screen.move_mouse(100, 100)
75
+ ```
76
+
77
+ You can also move the mouse smoothly somewhere:
78
+
79
+ ```ruby
80
+ # Move the mouse smoothly to x: 100, y: 100 over 2 seconds
81
+ screen.smooth_move_mouse(100, 100, duration: 2)
82
+ ```
83
+
84
+ You can click:
85
+
86
+ ```ruby
87
+ # Left click
88
+ screen.click
89
+
90
+ # Right click
91
+ screen.click(:right)
92
+ ```
93
+
94
+ Or even scroll:
95
+
96
+ ```ruby
97
+ # Scroll 1 click up
98
+ screen.scroll
99
+
100
+ # Scroll 5 clicks up
101
+ screen.scroll(clicks: 5)
102
+
103
+ # Scroll 5 clicks down
104
+ screen.scroll(:down, clicks: 5)
105
+ ```
106
+
107
+ ### Screen introspection
108
+
109
+ You can query the color of a specific pixel:
110
+
111
+ ```ruby
112
+ # Get the color of a specific pixel at x: 100, y: 100
113
+ color = screen.color_at(100, 100)
114
+ ```
115
+
116
+ This returns a `Deskbot::Color` object with `red`, `green`, `blue` and `alpha`
117
+ attributes.
118
+
119
+ You can query the size of the screen:
120
+
121
+ ```ruby
122
+ size = screen.size
123
+ scale = screen.scale
124
+ ```
125
+
126
+ The size would be a `Deskbot::Size` with `width` and `height` attributes. The
127
+ scale would simply be a float.
128
+
129
+ You can query if a point is visible on the screen:
130
+
131
+ ```ruby
132
+ screen.point_visible?(100, 100)
133
+ screen.area_visible?(x: 100, y: 100, width: 400, height: 400)
134
+ ```
135
+
136
+ ### Bitmaps
137
+
138
+ You can capture your screen:
139
+
140
+ ```ruby
141
+ # We can capture the whole screen
142
+ bitmap = screen.capture
143
+
144
+ # Or we can capture part of our screen
145
+ bitmap = screen.capture_area(x: 100, y: 100, width: 400, height: 400)
146
+ ```
147
+
148
+ This returns a `Deskbot::Bitmap` which you can use to find areas that match
149
+ images you provide.
150
+
151
+ You can query the bounds of the bitmap:
152
+
153
+ ```ruby
154
+ bitmap.bounds
155
+ ```
156
+
157
+ Which would return a `Deskbot::Area` with `x`, `y`, `width` and `height`
158
+ attributes.
159
+
160
+ We can check for the colors of points on the bitmap:
161
+
162
+ ```ruby
163
+ bitmap.color_at(100, 100)
164
+ ```
165
+
166
+ Which returns a `Deskbot::Color`.
167
+
168
+ ### Comparing images
169
+
170
+ You can compare images to your bitmap with a given tolerance (optional):
171
+
172
+ ```ruby
173
+ bitmap.find("images/test.jpg")
174
+ bitmap.all("images/test.jpg", tolerance: 4.0)
175
+ ```
176
+
177
+ `find` and `find_color` both return `Dry::Monad::Result` objects meaning you
178
+ need to unwrap their values:
179
+
180
+ ```ruby
181
+ result = bitmap.find("images/test.jpg")
182
+ point = result.success
183
+ ```
184
+
185
+ They are also wrapped with an optional matcher so you can use a much nicer
186
+ syntax to grab their values:
187
+
188
+ ```ruby
189
+ bitmap.find("images/test.jpg") do |on|
190
+ on.success do |point|
191
+ # Do something with the point, no need to call point.success
192
+ end
193
+
194
+ on.failure do
195
+ # Handle you error here
196
+ end
197
+ end
198
+ ```
199
+
200
+ These return `Deskbot::Point` objects with `x` and `y` attributes.
201
+
202
+ You can ask higher level questions about the image such as:
203
+
204
+ ```ruby
205
+ bitmap.eql?("images/test.jpg", tolerance: 2.0)
206
+ bitmap.count("images/test.jpg")
207
+ ```
208
+
209
+ You can also query for positions of colors:
210
+
211
+ ```ruby
212
+ bitmap.find_color([255, 255, 255, 0], tolerance: 0.0)
213
+ bitmap.all_color([255, 255, 255, 0])
214
+ ```
215
+
216
+ These will return `Deskbot::Point` objects with `x` and `y` attributes.
217
+
218
+ ## Development
219
+
220
+ After checking out the repo, run `bin/setup` to install dependencies.
221
+ Then, run `rake spec` to run the tests. You can also run `bin/console` for
222
+ an interactive prompt that will allow you to experiment.
223
+
224
+ To install this gem onto your local machine, run `bundle exec rake install`.
225
+ To release a new version, update the version number in `version.rb`, and then
226
+ run `bundle exec rake release`, which will create a git tag for the version,
227
+ push git commits and the created tag, and push the `.gem` file
228
+ to [rubygems.org](https://rubygems.org).
229
+
230
+ ## Contributing
231
+
232
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nolantait/deskbot.
233
+
234
+ ## License
235
+
236
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rb_sys/extensiontask"
9
+
10
+ task build: :compile
11
+
12
+ GEMSPEC = Gem::Specification.load("deskbot.gemspec")
13
+
14
+ RbSys::ExtensionTask.new("deskbot", GEMSPEC) do |ext|
15
+ ext.lib_dir = "lib/deskbot"
16
+ end
17
+
18
+ task default: %i[compile spec]
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "deskbot"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Nolan J Tait <nolanjtait@gmail.com>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ opencv = { version = "0.91.3", features = ["clang-runtime"] }
14
+ magnus = { version = "0.6.2" }
15
+ autopilot = { version = "0.4.0" }
16
+ image = { version = "0.22.4" }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("deskbot/deskbot")
@@ -0,0 +1,5 @@
1
+ extern crate autopilot;
2
+
3
+ pub fn alert(message: String) -> () {
4
+ autopilot::alert::alert(&message.as_str(), None, None, None);
5
+ }
@@ -0,0 +1,268 @@
1
+ extern crate autopilot;
2
+
3
+ use image::open;
4
+ use magnus::exception;
5
+ use std::collections::HashMap;
6
+
7
+ extern crate opencv;
8
+
9
+ use opencv::{
10
+ core::{self, Mat, MatTraitConst, Rect, Scalar},
11
+ imgproc, Result,
12
+ };
13
+
14
+ #[magnus::wrap(class = "Deskbot::Providers::Autopilot::Bitmap")]
15
+ pub struct Bitmap(autopilot::bitmap::Bitmap);
16
+
17
+ impl Bitmap {
18
+ fn new(bitmap: autopilot::bitmap::Bitmap) -> Self {
19
+ Bitmap(bitmap)
20
+ }
21
+
22
+ pub fn save(&self, path: String) -> Result<bool, magnus::Error> {
23
+ match self.0.image.save(path) {
24
+ Ok(_) => Ok(true),
25
+ Err(_) => Err(magnus::Error::new(
26
+ exception::runtime_error(),
27
+ "Could not save the image",
28
+ )),
29
+ }
30
+ }
31
+
32
+ pub fn bounds(&self) -> HashMap<String, f64> {
33
+ let bounds = self.0.bounds();
34
+
35
+ HashMap::from([
36
+ ("x".to_string(), bounds.origin.x),
37
+ ("y".to_string(), bounds.origin.y),
38
+ ("width".to_string(), bounds.size.width),
39
+ ("height".to_string(), bounds.size.height),
40
+ ])
41
+ }
42
+
43
+ pub fn get_pixel(&self, x: f64, y: f64) -> Vec<u8> {
44
+ let point = autopilot::geometry::Point::new(x, y);
45
+ let value = self.0.get_pixel(point).0;
46
+ // For some reason I have to convert to a Vec<u8>. Magnus doesn't like [u8; 4]
47
+ return value.to_vec();
48
+ }
49
+
50
+ pub fn find_color(
51
+ &self,
52
+ color: [u8; 4],
53
+ tolerance: Option<f64>,
54
+ ) -> Option<HashMap<String, f64>> {
55
+ if let Some(found) = self.0.find_color(image::Rgba(color), tolerance, None, None) {
56
+ return Some(HashMap::from([
57
+ ("x".to_string(), found.x),
58
+ ("y".to_string(), found.y),
59
+ ]));
60
+ }
61
+ None
62
+ }
63
+
64
+ pub fn find(&self, image_path: String, tolerance: Option<f64>) -> Option<HashMap<String, f64>> {
65
+ let src = match opencv::imgcodecs::imread(
66
+ "./tmp/last-screenshot.png",
67
+ opencv::imgcodecs::IMREAD_COLOR,
68
+ ) {
69
+ Ok(src) => src,
70
+ Err(error) => panic!("Could not read the image: {}", error),
71
+ };
72
+ let templ = match opencv::imgcodecs::imread(&image_path, opencv::imgcodecs::IMREAD_COLOR) {
73
+ Ok(templ) => templ,
74
+ Err(error) => panic!("Could not read the image: {}", error),
75
+ };
76
+
77
+ // Create Mat for the result
78
+ let mut dst = Mat::default();
79
+ let mask = Mat::default();
80
+
81
+ // Perform template matching
82
+ match imgproc::match_template(
83
+ &src,
84
+ &templ,
85
+ &mut dst,
86
+ imgproc::TemplateMatchModes::TM_SQDIFF_NORMED.into(),
87
+ &mask,
88
+ ) {
89
+ Ok(_) => {}
90
+ Err(error) => panic!("Could not match the template: {}", error),
91
+ }
92
+
93
+ // Find the location of the best match
94
+ let (mut min_val, mut max_val) = (0.0, 0.0);
95
+ let mut min_point = core::Point { x: 0, y: 0 };
96
+ let mut max_point = core::Point { x: 0, y: 0 };
97
+
98
+ match core::min_max_loc(
99
+ &dst,
100
+ Some(&mut min_val),
101
+ Some(&mut max_val),
102
+ Some(&mut min_point),
103
+ Some(&mut max_point),
104
+ &mask,
105
+ ) {
106
+ Ok(_) => {}
107
+ Err(error) => panic!("Could not find the min max loc: {}", error),
108
+ }
109
+
110
+ if min_val > tolerance.unwrap_or(0.0) {
111
+ return None;
112
+ }
113
+
114
+ Some(HashMap::from([
115
+ ("x".to_string(), min_point.x as f64),
116
+ ("y".to_string(), min_point.y as f64),
117
+ ]))
118
+ }
119
+
120
+ pub fn all_color(&self, color: [u8; 4], tolerance: Option<f64>) -> Vec<HashMap<String, f64>> {
121
+ let mut results = vec![];
122
+ for found in self
123
+ .0
124
+ .find_every_color(image::Rgba(color), tolerance, None, None)
125
+ {
126
+ results.push(HashMap::from([
127
+ ("x".to_string(), found.x),
128
+ ("y".to_string(), found.y),
129
+ ]));
130
+ }
131
+ results
132
+ }
133
+
134
+ pub fn all(&self, image_path: String, tolerance: Option<f64>) -> Vec<HashMap<String, f64>> {
135
+ let mut results = vec![];
136
+
137
+ let mut image = self.load_image("./tmp/last-screenshot.png");
138
+ let template_image = self.load_image(&image_path);
139
+ let mut matches: Vec<core::Point> = Vec::new();
140
+
141
+ // Fake out min_val for first run through loop
142
+ loop {
143
+ match self.match_template_and_replace(
144
+ &mut image,
145
+ &template_image,
146
+ tolerance.unwrap_or(0.5)
147
+ ) {
148
+ Some(point) => {
149
+ matches.push(point);
150
+ }
151
+ None => break,
152
+ }
153
+ }
154
+
155
+ matches.iter().for_each(|point| {
156
+ results.push(HashMap::from([
157
+ ("x".to_string(), point.x as f64),
158
+ ("y".to_string(), point.y as f64),
159
+ ]));
160
+ });
161
+
162
+ results
163
+ }
164
+
165
+ pub fn count(&self, image_path: String, tolerance: Option<f64>) -> u64 {
166
+ if let Ok(image) = open(image_path) {
167
+ let needle = autopilot::bitmap::Bitmap::new(image, None);
168
+ return self.0.count_of_bitmap(&needle, tolerance, None, None);
169
+ }
170
+ 0
171
+ }
172
+
173
+ fn minimum_point(&self, mat: &Mat) -> (core::Point, f64) {
174
+ let mut min_val = 1.;
175
+ let mut min_point = core::Point { x: 0, y: 0 };
176
+
177
+ match core::min_max_loc(
178
+ &mat,
179
+ Some(&mut min_val),
180
+ None,
181
+ Some(&mut min_point),
182
+ None,
183
+ &core::no_array(),
184
+ ) {
185
+ Ok(_) => {}
186
+ Err(error) => panic!("Could not find the min max loc: {}", error),
187
+ }
188
+
189
+ (min_point, min_val)
190
+ }
191
+
192
+ fn load_image(&self, path: &str) -> Mat {
193
+ match opencv::imgcodecs::imread(path, opencv::imgcodecs::IMREAD_COLOR) {
194
+ Ok(src) => src,
195
+ Err(error) => panic!("Could not read the image: {}", error),
196
+ }
197
+ }
198
+
199
+ // fn write_image(&self, path: &str, image: &Mat) {
200
+ // match opencv::imgcodecs::imwrite(path, image, &core::Vector::<i32>::new()) {
201
+ // Ok(_) => {}
202
+ // Err(error) => panic!("Could not write the image: {}", error),
203
+ // }
204
+ // }
205
+
206
+ fn match_template(&self, image: &Mat, template: &Mat, threshold: f64) -> Option<core::Point> {
207
+ let mut result = Mat::default();
208
+
209
+ imgproc::match_template_def(image, &template, &mut result, imgproc::TM_SQDIFF_NORMED)
210
+ .unwrap();
211
+
212
+ let (min_point, min_val) = self.minimum_point(&result);
213
+
214
+ if min_val > threshold {
215
+ return None;
216
+ }
217
+
218
+ Some(min_point)
219
+ }
220
+
221
+ fn match_template_and_replace(
222
+ &self,
223
+ image: &mut Mat,
224
+ template: &Mat,
225
+ threshold: f64,
226
+ ) -> Option<core::Point> {
227
+ let min_point = self.match_template(image, template, threshold)?;
228
+ let template_size = template.size().unwrap();
229
+
230
+ let rect = Rect {
231
+ x: min_point.x,
232
+ y: min_point.y,
233
+ width: template_size.width as i32 + 1,
234
+ height: template_size.height as i32 + 1,
235
+ };
236
+
237
+ imgproc::rectangle(image, rect, Scalar::new(0.0, 0.0, 0.0, 0.0), -1, 8, 0).unwrap();
238
+
239
+ Some(min_point)
240
+ }
241
+ }
242
+
243
+ pub fn capture_screen_portion(x: f64, y: f64, width: f64, height: f64) -> Option<Bitmap> {
244
+ let point = autopilot::geometry::Point::new(x, y);
245
+ let size = autopilot::geometry::Size::new(width, height);
246
+ let rect = autopilot::geometry::Rect::new(point, size);
247
+ let image = autopilot::bitmap::capture_screen_portion(rect);
248
+
249
+ match image {
250
+ Ok(image) => {
251
+ image.image.save("./tmp/last-screenshot.png").unwrap();
252
+ Some(Bitmap::new(image))
253
+ }
254
+ Err(_) => None,
255
+ }
256
+ }
257
+
258
+ pub fn capture_screen() -> Option<Bitmap> {
259
+ let image = autopilot::bitmap::capture_screen();
260
+
261
+ match image {
262
+ Ok(image) => {
263
+ image.image.save("./tmp/last-screenshot.png").unwrap();
264
+ Some(Bitmap::new(image))
265
+ }
266
+ Err(_) => None,
267
+ }
268
+ }
@@ -0,0 +1,38 @@
1
+ use magnus::RArray;
2
+ extern crate autopilot;
3
+
4
+ fn key_flags(symbols: Vec<String>) -> Vec<autopilot::key::Flag> {
5
+ symbols
6
+ .iter()
7
+ .filter_map(|symbol| key_flag(symbol))
8
+ .collect::<Vec<autopilot::key::Flag>>()
9
+ }
10
+
11
+ fn key_flag(symbol: &String) -> Option<autopilot::key::Flag> {
12
+ match symbol.as_str() {
13
+ "shift" => Some(autopilot::key::Flag::Shift),
14
+ "control" => Some(autopilot::key::Flag::Control),
15
+ "alt" => Some(autopilot::key::Flag::Alt),
16
+ "meta" => Some(autopilot::key::Flag::Meta),
17
+ "help" => Some(autopilot::key::Flag::Help),
18
+ _ => None,
19
+ }
20
+ }
21
+
22
+ pub fn type_string(string: String, _flag: RArray, wpm: f64, noise: f64) -> () {
23
+ let _flags = _flag.to_vec::<String>().unwrap();
24
+ let flags = key_flags(_flags);
25
+ autopilot::key::type_string(&string, &flags, wpm, noise);
26
+ }
27
+
28
+ pub fn toggle_key(_key: char, down: bool, _flags: RArray, modifier_delay_ms: u64) -> () {
29
+ let flags = key_flags(_flags.to_vec::<String>().unwrap());
30
+ let key = autopilot::key::Character(_key);
31
+ autopilot::key::toggle(&key, down, &flags, modifier_delay_ms);
32
+ }
33
+
34
+ pub fn tap_key(_key: char, _flags: RArray, delay_ms: u64, modifier_delay_ms: u64) -> () {
35
+ let flags = key_flags(_flags.to_vec::<String>().unwrap());
36
+ let key = autopilot::key::Character(_key);
37
+ autopilot::key::tap(&key, &flags, delay_ms, modifier_delay_ms);
38
+ }
@@ -0,0 +1,49 @@
1
+ use magnus::{class, function, method, prelude::*, Error, Ruby};
2
+ extern crate autopilot;
3
+
4
+ mod bitmap;
5
+ mod keys;
6
+ mod mouse;
7
+ mod screen;
8
+ mod alert;
9
+
10
+ #[magnus::init]
11
+ fn init(ruby: &Ruby) -> Result<(), Error> {
12
+ let main_module = ruby.define_module("Deskbot")?.define_module("Providers")?;
13
+ let module = main_module.define_module("Autopilot")?;
14
+ module.define_singleton_method("type", function!(keys::type_string, 4))?;
15
+ module.define_singleton_method("toggle_key", function!(keys::toggle_key, 4))?;
16
+ module.define_singleton_method("tap_key", function!(keys::tap_key, 4))?;
17
+
18
+ module.define_singleton_method("mouse_location", function!(mouse::location, 0))?;
19
+ module.define_singleton_method("move_mouse", function!(mouse::move_to, 2))?;
20
+ module.define_singleton_method("toggle_mouse", function!(mouse::toggle, 2))?;
21
+ module.define_singleton_method("click", function!(mouse::click, 2))?;
22
+ module.define_singleton_method("scroll", function!(mouse::scroll, 2))?;
23
+ module.define_singleton_method("smooth_move_mouse", function!(mouse::smooth_move, 3))?;
24
+
25
+ module.define_singleton_method("color_at", function!(screen::get_color, 2))?;
26
+ module.define_singleton_method("screen_size", function!(screen::size, 0))?;
27
+ module.define_singleton_method("screen_scale", function!(screen::scale, 0))?;
28
+ module.define_singleton_method("is_point_visible", function!(screen::is_point_visible, 2))?;
29
+ module.define_singleton_method("is_area_visible", function!(screen::is_rect_visible, 4))?;
30
+
31
+ module.define_singleton_method("alert", function!(alert::alert, 1))?;
32
+
33
+ module.define_singleton_method(
34
+ "capture_screen_area",
35
+ function!(bitmap::capture_screen_portion, 4),
36
+ )?;
37
+ module.define_singleton_method("capture_screen", function!(bitmap::capture_screen, 0))?;
38
+
39
+ let bitmap = module.define_class("Bitmap", class::object())?;
40
+ bitmap.define_method("bounds", method!(bitmap::Bitmap::bounds, 0))?;
41
+ bitmap.define_method("find", method!(bitmap::Bitmap::find, 2))?;
42
+ bitmap.define_method("all", method!(bitmap::Bitmap::all, 2))?;
43
+ bitmap.define_method("count", method!(bitmap::Bitmap::count, 2))?;
44
+ bitmap.define_method("find_color", method!(bitmap::Bitmap::find_color, 2))?;
45
+ bitmap.define_method("all_color", method!(bitmap::Bitmap::all_color, 2))?;
46
+ bitmap.define_method("color_at", method!(bitmap::Bitmap::get_pixel, 2))?;
47
+ bitmap.define_method("save", method!(bitmap::Bitmap::save, 1))?;
48
+ Ok(())
49
+ }