deskbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ extern crate autopilot;
2
+ use std::collections::HashMap;
3
+
4
+ fn button(symbol: String) -> autopilot::mouse::Button {
5
+ match symbol.as_str() {
6
+ "left" => autopilot::mouse::Button::Left,
7
+ "middle" => autopilot::mouse::Button::Middle,
8
+ "right" => autopilot::mouse::Button::Right,
9
+ _ => panic!("Invalid button"),
10
+ }
11
+ }
12
+
13
+ fn scroll_direction(symbol: String) -> autopilot::mouse::ScrollDirection {
14
+ match symbol.as_str() {
15
+ "up" => autopilot::mouse::ScrollDirection::Up,
16
+ "down" => autopilot::mouse::ScrollDirection::Down,
17
+ _ => panic!("Invalid scroll direction"),
18
+ }
19
+ }
20
+
21
+ pub fn location() -> HashMap<String, f64> {
22
+ let point = autopilot::mouse::location();
23
+ return HashMap::from([("x".to_string(), point.x), ("y".to_string(), point.y)]);
24
+ }
25
+
26
+ pub fn move_to(x: f64, y: f64) -> bool {
27
+ let command = autopilot::mouse::move_to(autopilot::geometry::Point::new(x, y));
28
+ match command {
29
+ Ok(_) => true,
30
+ Err(_) => false,
31
+ }
32
+ }
33
+
34
+ pub fn smooth_move(x: f64, y: f64, duration: Option<f64>) -> bool {
35
+ let command = autopilot::mouse::smooth_move(autopilot::geometry::Point::new(x, y), duration);
36
+
37
+ match command {
38
+ Ok(_) => true,
39
+ Err(_) => false,
40
+ }
41
+ }
42
+
43
+ pub fn toggle(_button: String, down: bool) -> () {
44
+ let button = button(_button);
45
+ autopilot::mouse::toggle(button, down);
46
+ }
47
+
48
+ pub fn click(_button: String, delay_ms: Option<u64>) -> () {
49
+ let button = button(_button);
50
+ autopilot::mouse::click(button, delay_ms);
51
+ }
52
+
53
+ pub fn scroll(direction: String, clicks: u32) -> () {
54
+ autopilot::mouse::scroll(scroll_direction(direction), clicks);
55
+ }
@@ -0,0 +1,34 @@
1
+ extern crate autopilot;
2
+ use std::collections::HashMap;
3
+
4
+ pub fn get_color(x: f64, y: f64) -> Option<Vec<u8>> {
5
+ let color = autopilot::screen::get_color(autopilot::geometry::Point::new(x, y));
6
+ match color {
7
+ Ok(color) => Some(color.0.to_vec()),
8
+ Err(_) => None
9
+ }
10
+ }
11
+
12
+ pub fn size() -> HashMap<String, f64> {
13
+ let size = autopilot::screen::size();
14
+
15
+ HashMap::from([
16
+ ("width".to_string(), size.width),
17
+ ("height".to_string(), size.height)
18
+ ])
19
+ }
20
+
21
+ pub fn scale() -> f64 {
22
+ autopilot::screen::scale()
23
+ }
24
+
25
+ pub fn is_point_visible(x: f64, y: f64) -> bool {
26
+ let point = autopilot::geometry::Point::new(x, y);
27
+ autopilot::screen::is_point_visible(point)
28
+ }
29
+
30
+ pub fn is_rect_visible(x: f64, y: f64, width: f64, height: f64) -> bool {
31
+ let point = autopilot::geometry::Point::new(x, y);
32
+ let size = autopilot::geometry::Size::new(width, height);
33
+ autopilot::screen::is_rect_visible(autopilot::geometry::Rect::new(point, size))
34
+ }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Area < Dry::Struct
5
+ attribute :x, Types::Float
6
+ attribute :y, Types::Float
7
+ attribute :width, Types::Float
8
+ attribute :height, Types::Float
9
+
10
+ transform_keys(&:to_sym)
11
+ end
12
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Bitmap
5
+ include Dry::Monads[:result]
6
+ include Dry::Matcher.for(
7
+ :find_color,
8
+ :find,
9
+ with: Dry::Matcher::ResultMatcher
10
+ )
11
+
12
+ Rgba = Types::Array
13
+ .of(Types::Integer.constrained(gteq: 0, lteq: 255))
14
+ .constrained(size: 4)
15
+
16
+ def initialize(provider)
17
+ @provider = provider
18
+ end
19
+
20
+ def save(image_path)
21
+ @provider.save(Types::String[image_path])
22
+ end
23
+
24
+ def bounds
25
+ Area.new(@provider.bounds)
26
+ end
27
+
28
+ def color_at(x, y) # rubocop:disable Naming/MethodParameterName
29
+ red, green, blue, alpha = @provider.color_at(x, y)
30
+ Color.new(red:, green:, blue:, alpha:)
31
+ end
32
+
33
+ def eql?(image_path, tolerance: nil)
34
+ @provider.bitmap_eq(
35
+ Types::String[image_path],
36
+ Types::Float.optional[tolerance]
37
+ )
38
+ end
39
+
40
+ def find(image_path, tolerance: nil)
41
+ result = @provider.find(
42
+ Types::String[image_path],
43
+ Types::Float.optional[tolerance]
44
+ )
45
+
46
+ if result
47
+ Success(Point.new(result))
48
+ else
49
+ Failure(:not_found)
50
+ end
51
+ end
52
+
53
+ def find_color(color, tolerance: nil)
54
+ found = @provider.find_color(
55
+ Rgba[color],
56
+ Types::Float.optional[tolerance]
57
+ )
58
+
59
+ if found
60
+ Success(Point.new(found))
61
+ else
62
+ Failure(:not_found)
63
+ end
64
+ end
65
+
66
+ def all(image_path, tolerance: nil)
67
+ @provider.all(
68
+ Types::String[image_path],
69
+ Types::Float.optional[tolerance]
70
+ )
71
+ .map { |point| Point.new(point) }
72
+ end
73
+
74
+ def all_color(color, tolerance: nil)
75
+ @provider.all_color(
76
+ Rgba[color],
77
+ Types::Float.optional[tolerance]
78
+ )
79
+ .map { |point| Point.new(point) }
80
+ end
81
+
82
+ def count(image_path, tolerance: nil)
83
+ @provider.count(
84
+ Types::String[image_path],
85
+ Types::Float.optional[tolerance]
86
+ )
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Color < Dry::Struct
5
+ attribute :red, Types::Integer
6
+ attribute :green, Types::Integer
7
+ attribute :blue, Types::Integer
8
+ attribute :alpha, Types::Integer
9
+
10
+ transform_keys do |key|
11
+ {
12
+ "r" => :red,
13
+ "g" => :green,
14
+ "b" => :blue,
15
+ "a" => :alpha
16
+ # rubocop:enable Style/StringHashKeys
17
+ }.fetch(key, key)
18
+ end
19
+
20
+ def self.from_hex(hex)
21
+ red, green, blue, alpha = hex
22
+ .match(/^#(..)(..)(..)(..)?$/)
23
+ .captures
24
+ .map do |hex_pair|
25
+ hex_pair&.hex
26
+ end
27
+
28
+ new(
29
+ red:,
30
+ green:,
31
+ blue:,
32
+ alpha: alpha || 255
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Point < Dry::Struct
5
+ attribute :x, Types::Float
6
+ attribute :y, Types::Float
7
+
8
+ transform_keys(&:to_sym)
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ module Providers
5
+ module Autopilot
6
+ class Bitmap # rubocop:disable Lint/EmptyClass
7
+ # Holds rust autopilot bitmap bindings
8
+ # See ext/deskbot/src/bitmap.rs for more details
9
+ #
10
+ # def bounds
11
+ # def color_at
12
+ # def bitmap_eq
13
+ # def find
14
+ # def find_color
15
+ # def all
16
+ # def all_color
17
+ # def count
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ module Providers
5
+ module Autopilot
6
+ # Holds rust autopilot bindings
7
+ # See ext/deskbot/src/lib.rs for more details
8
+ #
9
+ # def type
10
+ # def alert
11
+ # def toggle_key
12
+ # def tap_key
13
+ # def mouse_location
14
+ # def move_mouse
15
+ # def smooth_move_mouse
16
+ # def toggle_mouse
17
+ # def scroll
18
+ # def click
19
+ # def color_at
20
+ # def screen_size
21
+ # def screen_scale
22
+ # def is_point_visible
23
+ # def is_area_visible
24
+ # def capture_screen
25
+ # def capture_screen_portion
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Screen
5
+ # This is the API for any provider
6
+
7
+ def initialize(provider)
8
+ @provider = provider
9
+ end
10
+
11
+ def alert(message)
12
+ @provider.alert(Types::String[message])
13
+ end
14
+
15
+ def type(text, flags: [], wpm: 60.0, noise: 0.0)
16
+ @provider.type(
17
+ Types::String[text],
18
+ Types::Flags[flags],
19
+ Types::Float[wpm],
20
+ Types::Float[noise]
21
+ )
22
+ end
23
+
24
+ def toggle_key(key, down: true, flags: [], modifier_delay_ms: 0.0)
25
+ @provider.toggle_key(
26
+ Types::Character[key],
27
+ Types::Bool[down],
28
+ Types::Flags[flags],
29
+ Types::Float[modifier_delay_ms]
30
+ )
31
+ end
32
+
33
+ def tap_key(key, flags: [], delay_ms: 0.0, modifier_delay_ms: 0.0)
34
+ @provider.tap_key(
35
+ Types::Character[key],
36
+ Types::Flags[flags],
37
+ Types::Float[delay_ms],
38
+ Types::Float[modifier_delay_ms]
39
+ )
40
+ end
41
+
42
+ def mouse_location
43
+ Point.new(@provider.mouse_location)
44
+ end
45
+
46
+ def move_mouse(x, y) # rubocop:disable Naming/MethodParameterName
47
+ @provider.move_mouse(
48
+ Types::Coercible::Float[x],
49
+ Types::Coercible::Float[y]
50
+ )
51
+ end
52
+
53
+ def smooth_move_mouse(x, y, duration: 1) # rubocop:disable Naming/MethodParameterName
54
+ @provider.smooth_move_mouse(
55
+ Types::Coercible::Float[x],
56
+ Types::Coercible::Float[y],
57
+ Types::Coercible::Float[duration]
58
+ )
59
+ end
60
+
61
+ def toggle_mouse(button = "left", down: true)
62
+ @provider.toggle_mouse(
63
+ Types::Button[button],
64
+ Types::Bool[down]
65
+ )
66
+ end
67
+
68
+ def click(button = "left", delay_ms: nil)
69
+ @provider.click(
70
+ Types::Button[button],
71
+ Types::Float.optional[delay_ms]
72
+ )
73
+ end
74
+
75
+ def scroll(direction = "up", clicks: 1)
76
+ @provider.scroll(
77
+ Types::ScrollDirection[direction],
78
+ Types::Integer[clicks]
79
+ )
80
+ end
81
+
82
+ def color_at(x, y) # rubocop:disable Naming/MethodParameterName
83
+ red, green, blue, alpha = @provider.color_at(
84
+ Types::Coercible::Float[x],
85
+ Types::Coercible::Float[y]
86
+ )
87
+
88
+ Color.new(red:, green:, blue:, alpha:)
89
+ end
90
+
91
+ def size
92
+ Size.new(@provider.screen_size)
93
+ end
94
+
95
+ def scale
96
+ @provider.screen_scale
97
+ end
98
+
99
+ def point_visible?(x, y) # rubocop:disable Naming/MethodParameterName
100
+ @provider.is_point_visible(
101
+ Types::Coercible::Float[x],
102
+ Types::Coercible::Float[y]
103
+ )
104
+ end
105
+
106
+ def area_visible?(x:, y:, width:, height:) # rubocop:disable Naming/MethodParameterName
107
+ @provider.is_area_visible(
108
+ Types::Coercible::Float[x],
109
+ Types::Coercible::Float[y],
110
+ Types::Coercible::Float[width],
111
+ Types::Coercible::Float[height]
112
+ )
113
+ end
114
+
115
+ def capture
116
+ bitmap = @provider.capture_screen
117
+ Deskbot::Bitmap.new(bitmap)
118
+ end
119
+
120
+ def capture_area(x:, y:, width:, height:) # rubocop:disable Naming/MethodParameterName
121
+ bitmap = @provider.capture_screen_area(
122
+ Types::Coercible::Float[x],
123
+ Types::Coercible::Float[y],
124
+ Types::Coercible::Float[width],
125
+ Types::Coercible::Float[height]
126
+ )
127
+
128
+ Deskbot::Bitmap.new(bitmap)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ class Size < Dry::Struct
5
+ attribute :width, Types::Float
6
+ attribute :height, Types::Float
7
+
8
+ transform_keys(&:to_sym)
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ module Types
5
+ include Dry.Types(default: :strict)
6
+
7
+ Flag = Types::Coercible::String.enum(
8
+ "shift",
9
+ "control",
10
+ "alt",
11
+ "meta",
12
+ "help"
13
+ )
14
+
15
+ Button = Types::Coercible::String.enum(
16
+ "left",
17
+ "middle",
18
+ "right"
19
+ )
20
+
21
+ Flags = Types::Array.of(Flag)
22
+
23
+ Character = Types::Coercible::String.constrained(size: 1)
24
+
25
+ ScrollDirection = Types::Coercible::String.enum(
26
+ "up",
27
+ "down"
28
+ )
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deskbot
4
+ VERSION = "0.1.0"
5
+ end
data/lib/deskbot.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+ require "dry/struct"
5
+ require "dry/monads"
6
+ require "dry/matcher/result_matcher"
7
+
8
+ require_relative "deskbot/version"
9
+ require_relative "deskbot/deskbot"
10
+ require_relative "deskbot/types"
11
+ require_relative "deskbot/point"
12
+ require_relative "deskbot/color"
13
+ require_relative "deskbot/size"
14
+ require_relative "deskbot/area"
15
+ require_relative "deskbot/bitmap"
16
+
17
+ require "deskbot/providers/autopilot/bitmap"
18
+ require "deskbot/providers/autopilot"
19
+
20
+ require "deskbot/screen"
21
+
22
+ module Deskbot
23
+ class Error < StandardError; end
24
+
25
+ def self.screen(provider: Providers::Autopilot)
26
+ @screen ||= Screen.new(provider)
27
+ end
28
+ end
data/sig/deskbot.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Deskbot
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deskbot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nolan J Tait
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-05-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-matcher
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-monads
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-struct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-types
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ description: Ruby desktop automation
70
+ email:
71
+ - nolanjtait@gmail.com
72
+ executables: []
73
+ extensions:
74
+ - ext/deskbot/Cargo.toml
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - CHANGELOG.md
80
+ - Cargo.lock
81
+ - Cargo.toml
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - ext/deskbot/Cargo.toml
86
+ - ext/deskbot/extconf.rb
87
+ - ext/deskbot/src/alert.rs
88
+ - ext/deskbot/src/bitmap.rs
89
+ - ext/deskbot/src/keys.rs
90
+ - ext/deskbot/src/lib.rs
91
+ - ext/deskbot/src/mouse.rs
92
+ - ext/deskbot/src/screen.rs
93
+ - lib/deskbot.rb
94
+ - lib/deskbot/area.rb
95
+ - lib/deskbot/bitmap.rb
96
+ - lib/deskbot/color.rb
97
+ - lib/deskbot/point.rb
98
+ - lib/deskbot/providers/autopilot.rb
99
+ - lib/deskbot/providers/autopilot/bitmap.rb
100
+ - lib/deskbot/screen.rb
101
+ - lib/deskbot/size.rb
102
+ - lib/deskbot/types.rb
103
+ - lib/deskbot/version.rb
104
+ - sig/deskbot.rbs
105
+ homepage: https://github.com/nolantait/desktbot
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ homepage_uri: https://github.com/nolantait/desktbot
110
+ source_code_uri: https://github.com/nolantait/desktbot
111
+ changelog_uri: https://github.com/nolantait/desktbot
112
+ rubygems_mfa_required: 'true'
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '3.3'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 3.3.11
127
+ requirements: []
128
+ rubygems_version: 3.5.9
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Ruby dekstop automation
132
+ test_files: []