deskbot 0.1.0

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