├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── bundle ├── console ├── lint ├── rspec └── setup ├── deskbot.gemspec ├── ext └── deskbot │ ├── Cargo.toml │ ├── extconf.rb │ └── src │ ├── alert.rs │ ├── bitmap.rs │ ├── keys.rs │ ├── lib.rs │ ├── mouse.rs │ └── screen.rs ├── lib ├── deskbot.rb └── deskbot │ ├── area.rb │ ├── bitmap.rb │ ├── color.rb │ ├── point.rb │ ├── providers │ ├── autopilot.rb │ └── autopilot │ │ └── bitmap.rb │ ├── screen.rb │ ├── size.rb │ ├── types.rb │ └── version.rb ├── sig └── deskbot.rbs └── spec ├── deskbot ├── bitmap_spec.rb ├── color_spec.rb └── screen_spec.rb ├── deskbot_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.bundle 10 | *.so 11 | *.o 12 | *.a 13 | mkmf.log 14 | target/ 15 | 16 | # rspec failure tracking 17 | .rspec_status 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-inhouse 3 | 4 | inherit_gem: 5 | rubocop-inhouse: 6 | - config/default.yml 7 | 8 | AllCops: 9 | TargetRubyVersion: 3.3 10 | 11 | Style/StringHashKeys: 12 | Enabled: false 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.1.0] - 2024-05-22 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler32" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.3.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 25 | 26 | [[package]] 27 | name = "autopilot" 28 | version = "0.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3a2fd0359d0cd30b15419bcce7e6641a3cbe2046c612b3900216dc64bc989558" 31 | dependencies = [ 32 | "cocoa", 33 | "core-foundation", 34 | "core-graphics", 35 | "image", 36 | "libc", 37 | "pkg-config", 38 | "rand", 39 | "scopeguard", 40 | "winapi", 41 | "x11", 42 | ] 43 | 44 | [[package]] 45 | name = "bindgen" 46 | version = "0.69.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 49 | dependencies = [ 50 | "bitflags 2.5.0", 51 | "cexpr", 52 | "clang-sys", 53 | "itertools", 54 | "lazy_static", 55 | "lazycell", 56 | "proc-macro2 1.0.83", 57 | "quote 1.0.36", 58 | "regex", 59 | "rustc-hash", 60 | "shlex", 61 | "syn 2.0.65", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "1.3.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "2.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 75 | 76 | [[package]] 77 | name = "block" 78 | version = "0.1.6" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 81 | 82 | [[package]] 83 | name = "byteorder" 84 | version = "1.5.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 87 | 88 | [[package]] 89 | name = "cc" 90 | version = "1.0.98" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 93 | dependencies = [ 94 | "jobserver", 95 | "libc", 96 | "once_cell", 97 | ] 98 | 99 | [[package]] 100 | name = "cexpr" 101 | version = "0.6.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 104 | dependencies = [ 105 | "nom", 106 | ] 107 | 108 | [[package]] 109 | name = "cfg-if" 110 | version = "1.0.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 113 | 114 | [[package]] 115 | name = "clang" 116 | version = "2.0.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "84c044c781163c001b913cd018fc95a628c50d0d2dfea8bca77dad71edb16e37" 119 | dependencies = [ 120 | "clang-sys", 121 | "libc", 122 | ] 123 | 124 | [[package]] 125 | name = "clang-sys" 126 | version = "1.7.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" 129 | dependencies = [ 130 | "glob", 131 | "libc", 132 | "libloading", 133 | ] 134 | 135 | [[package]] 136 | name = "cocoa" 137 | version = "0.20.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" 140 | dependencies = [ 141 | "bitflags 1.3.2", 142 | "block", 143 | "core-foundation", 144 | "core-graphics", 145 | "foreign-types", 146 | "libc", 147 | "objc", 148 | ] 149 | 150 | [[package]] 151 | name = "color_quant" 152 | version = "1.1.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 155 | 156 | [[package]] 157 | name = "core-foundation" 158 | version = "0.7.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" 161 | dependencies = [ 162 | "core-foundation-sys", 163 | "libc", 164 | ] 165 | 166 | [[package]] 167 | name = "core-foundation-sys" 168 | version = "0.7.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" 171 | 172 | [[package]] 173 | name = "core-graphics" 174 | version = "0.19.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" 177 | dependencies = [ 178 | "bitflags 1.3.2", 179 | "core-foundation", 180 | "foreign-types", 181 | "libc", 182 | ] 183 | 184 | [[package]] 185 | name = "crc32fast" 186 | version = "1.4.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 189 | dependencies = [ 190 | "cfg-if", 191 | ] 192 | 193 | [[package]] 194 | name = "crossbeam-deque" 195 | version = "0.8.5" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 198 | dependencies = [ 199 | "crossbeam-epoch", 200 | "crossbeam-utils", 201 | ] 202 | 203 | [[package]] 204 | name = "crossbeam-epoch" 205 | version = "0.9.18" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 208 | dependencies = [ 209 | "crossbeam-utils", 210 | ] 211 | 212 | [[package]] 213 | name = "crossbeam-utils" 214 | version = "0.8.20" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 217 | 218 | [[package]] 219 | name = "deflate" 220 | version = "0.7.20" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" 223 | dependencies = [ 224 | "adler32", 225 | "byteorder", 226 | ] 227 | 228 | [[package]] 229 | name = "deskbot" 230 | version = "0.1.0" 231 | dependencies = [ 232 | "autopilot", 233 | "image", 234 | "magnus", 235 | "opencv", 236 | ] 237 | 238 | [[package]] 239 | name = "dunce" 240 | version = "1.0.4" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" 243 | 244 | [[package]] 245 | name = "either" 246 | version = "1.12.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 249 | 250 | [[package]] 251 | name = "foreign-types" 252 | version = "0.3.2" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 255 | dependencies = [ 256 | "foreign-types-shared", 257 | ] 258 | 259 | [[package]] 260 | name = "foreign-types-shared" 261 | version = "0.1.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 264 | 265 | [[package]] 266 | name = "getrandom" 267 | version = "0.1.16" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 270 | dependencies = [ 271 | "cfg-if", 272 | "libc", 273 | "wasi", 274 | ] 275 | 276 | [[package]] 277 | name = "gif" 278 | version = "0.10.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" 281 | dependencies = [ 282 | "color_quant", 283 | "lzw", 284 | ] 285 | 286 | [[package]] 287 | name = "glob" 288 | version = "0.3.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 291 | 292 | [[package]] 293 | name = "image" 294 | version = "0.22.5" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "08ed2ada878397b045454ac7cfb011d73132c59f31a955d230bd1f1c2e68eb4a" 297 | dependencies = [ 298 | "byteorder", 299 | "gif", 300 | "jpeg-decoder", 301 | "num-iter", 302 | "num-rational", 303 | "num-traits", 304 | "png", 305 | "scoped_threadpool", 306 | "tiff", 307 | ] 308 | 309 | [[package]] 310 | name = "inflate" 311 | version = "0.4.5" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" 314 | dependencies = [ 315 | "adler32", 316 | ] 317 | 318 | [[package]] 319 | name = "itertools" 320 | version = "0.12.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 323 | dependencies = [ 324 | "either", 325 | ] 326 | 327 | [[package]] 328 | name = "jobserver" 329 | version = "0.1.31" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" 332 | dependencies = [ 333 | "libc", 334 | ] 335 | 336 | [[package]] 337 | name = "jpeg-decoder" 338 | version = "0.1.22" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 341 | dependencies = [ 342 | "rayon", 343 | ] 344 | 345 | [[package]] 346 | name = "lazy_static" 347 | version = "1.4.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 350 | 351 | [[package]] 352 | name = "lazycell" 353 | version = "1.3.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 356 | 357 | [[package]] 358 | name = "libc" 359 | version = "0.2.155" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 362 | 363 | [[package]] 364 | name = "libloading" 365 | version = "0.8.3" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 368 | dependencies = [ 369 | "cfg-if", 370 | "windows-targets", 371 | ] 372 | 373 | [[package]] 374 | name = "lzw" 375 | version = "0.10.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" 378 | 379 | [[package]] 380 | name = "magnus" 381 | version = "0.6.4" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "b1597ef40aa8c36be098249e82c9a20cf7199278ac1c1a1a995eeead6a184479" 384 | dependencies = [ 385 | "magnus-macros", 386 | "rb-sys", 387 | "rb-sys-env", 388 | "seq-macro", 389 | ] 390 | 391 | [[package]] 392 | name = "magnus-macros" 393 | version = "0.6.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3" 396 | dependencies = [ 397 | "proc-macro2 1.0.83", 398 | "quote 1.0.36", 399 | "syn 2.0.65", 400 | ] 401 | 402 | [[package]] 403 | name = "malloc_buf" 404 | version = "0.0.6" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 407 | dependencies = [ 408 | "libc", 409 | ] 410 | 411 | [[package]] 412 | name = "memchr" 413 | version = "2.7.2" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 416 | 417 | [[package]] 418 | name = "minimal-lexical" 419 | version = "0.2.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 422 | 423 | [[package]] 424 | name = "nom" 425 | version = "7.1.3" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 428 | dependencies = [ 429 | "memchr", 430 | "minimal-lexical", 431 | ] 432 | 433 | [[package]] 434 | name = "num-derive" 435 | version = "0.2.5" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2" 438 | dependencies = [ 439 | "proc-macro2 0.4.30", 440 | "quote 0.6.13", 441 | "syn 0.15.44", 442 | ] 443 | 444 | [[package]] 445 | name = "num-integer" 446 | version = "0.1.46" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 449 | dependencies = [ 450 | "num-traits", 451 | ] 452 | 453 | [[package]] 454 | name = "num-iter" 455 | version = "0.1.45" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 458 | dependencies = [ 459 | "autocfg", 460 | "num-integer", 461 | "num-traits", 462 | ] 463 | 464 | [[package]] 465 | name = "num-rational" 466 | version = "0.2.4" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" 469 | dependencies = [ 470 | "autocfg", 471 | "num-integer", 472 | "num-traits", 473 | ] 474 | 475 | [[package]] 476 | name = "num-traits" 477 | version = "0.2.19" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 480 | dependencies = [ 481 | "autocfg", 482 | ] 483 | 484 | [[package]] 485 | name = "objc" 486 | version = "0.2.7" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 489 | dependencies = [ 490 | "malloc_buf", 491 | ] 492 | 493 | [[package]] 494 | name = "once_cell" 495 | version = "1.19.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 498 | 499 | [[package]] 500 | name = "opencv" 501 | version = "0.91.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "e64b5a733be752a917aa27981c1100fbc7836cf2cfe3fb06f5cac691339837b7" 504 | dependencies = [ 505 | "cc", 506 | "dunce", 507 | "jobserver", 508 | "libc", 509 | "num-traits", 510 | "once_cell", 511 | "opencv-binding-generator", 512 | "pkg-config", 513 | "semver", 514 | "shlex", 515 | "vcpkg", 516 | "windows", 517 | ] 518 | 519 | [[package]] 520 | name = "opencv-binding-generator" 521 | version = "0.89.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "81af1298c489597ffdbadc7710673ec26f8a2440228992681ffa3b8f00233bd9" 524 | dependencies = [ 525 | "clang", 526 | "clang-sys", 527 | "dunce", 528 | "once_cell", 529 | "percent-encoding", 530 | "regex", 531 | ] 532 | 533 | [[package]] 534 | name = "percent-encoding" 535 | version = "2.3.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 538 | 539 | [[package]] 540 | name = "pkg-config" 541 | version = "0.3.30" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 544 | 545 | [[package]] 546 | name = "png" 547 | version = "0.15.3" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283" 550 | dependencies = [ 551 | "bitflags 1.3.2", 552 | "crc32fast", 553 | "deflate", 554 | "inflate", 555 | ] 556 | 557 | [[package]] 558 | name = "ppv-lite86" 559 | version = "0.2.17" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 562 | 563 | [[package]] 564 | name = "proc-macro2" 565 | version = "0.4.30" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 568 | dependencies = [ 569 | "unicode-xid", 570 | ] 571 | 572 | [[package]] 573 | name = "proc-macro2" 574 | version = "1.0.83" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" 577 | dependencies = [ 578 | "unicode-ident", 579 | ] 580 | 581 | [[package]] 582 | name = "quote" 583 | version = "0.6.13" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 586 | dependencies = [ 587 | "proc-macro2 0.4.30", 588 | ] 589 | 590 | [[package]] 591 | name = "quote" 592 | version = "1.0.36" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 595 | dependencies = [ 596 | "proc-macro2 1.0.83", 597 | ] 598 | 599 | [[package]] 600 | name = "rand" 601 | version = "0.7.3" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 604 | dependencies = [ 605 | "getrandom", 606 | "libc", 607 | "rand_chacha", 608 | "rand_core", 609 | "rand_hc", 610 | ] 611 | 612 | [[package]] 613 | name = "rand_chacha" 614 | version = "0.2.2" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 617 | dependencies = [ 618 | "ppv-lite86", 619 | "rand_core", 620 | ] 621 | 622 | [[package]] 623 | name = "rand_core" 624 | version = "0.5.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 627 | dependencies = [ 628 | "getrandom", 629 | ] 630 | 631 | [[package]] 632 | name = "rand_hc" 633 | version = "0.2.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 636 | dependencies = [ 637 | "rand_core", 638 | ] 639 | 640 | [[package]] 641 | name = "rayon" 642 | version = "1.10.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 645 | dependencies = [ 646 | "either", 647 | "rayon-core", 648 | ] 649 | 650 | [[package]] 651 | name = "rayon-core" 652 | version = "1.12.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 655 | dependencies = [ 656 | "crossbeam-deque", 657 | "crossbeam-utils", 658 | ] 659 | 660 | [[package]] 661 | name = "rb-sys" 662 | version = "0.9.97" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "47d30bcad206b51f2f66121190ca678dce1fdf3a2eae0ac5d838d1818b19bdf5" 665 | dependencies = [ 666 | "rb-sys-build", 667 | ] 668 | 669 | [[package]] 670 | name = "rb-sys-build" 671 | version = "0.9.97" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "3cbd92f281615f3c2dcb9dcb0f0576624752afbf9a7f99173b37c4b55b62dd8a" 674 | dependencies = [ 675 | "bindgen", 676 | "lazy_static", 677 | "proc-macro2 1.0.83", 678 | "quote 1.0.36", 679 | "regex", 680 | "shell-words", 681 | "syn 2.0.65", 682 | ] 683 | 684 | [[package]] 685 | name = "rb-sys-env" 686 | version = "0.1.2" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb" 689 | 690 | [[package]] 691 | name = "regex" 692 | version = "1.10.4" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 695 | dependencies = [ 696 | "aho-corasick", 697 | "memchr", 698 | "regex-automata", 699 | "regex-syntax", 700 | ] 701 | 702 | [[package]] 703 | name = "regex-automata" 704 | version = "0.4.6" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 707 | dependencies = [ 708 | "aho-corasick", 709 | "memchr", 710 | "regex-syntax", 711 | ] 712 | 713 | [[package]] 714 | name = "regex-syntax" 715 | version = "0.8.3" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 718 | 719 | [[package]] 720 | name = "rustc-hash" 721 | version = "1.1.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 724 | 725 | [[package]] 726 | name = "scoped_threadpool" 727 | version = "0.1.9" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 730 | 731 | [[package]] 732 | name = "scopeguard" 733 | version = "1.2.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 736 | 737 | [[package]] 738 | name = "semver" 739 | version = "1.0.23" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 742 | 743 | [[package]] 744 | name = "seq-macro" 745 | version = "0.3.5" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" 748 | 749 | [[package]] 750 | name = "shell-words" 751 | version = "1.1.0" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 754 | 755 | [[package]] 756 | name = "shlex" 757 | version = "1.3.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 760 | 761 | [[package]] 762 | name = "syn" 763 | version = "0.15.44" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 766 | dependencies = [ 767 | "proc-macro2 0.4.30", 768 | "quote 0.6.13", 769 | "unicode-xid", 770 | ] 771 | 772 | [[package]] 773 | name = "syn" 774 | version = "2.0.65" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" 777 | dependencies = [ 778 | "proc-macro2 1.0.83", 779 | "quote 1.0.36", 780 | "unicode-ident", 781 | ] 782 | 783 | [[package]] 784 | name = "tiff" 785 | version = "0.3.1" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "d7b7c2cfc4742bd8a32f2e614339dd8ce30dbcf676bb262bd63a2327bc5df57d" 788 | dependencies = [ 789 | "byteorder", 790 | "lzw", 791 | "num-derive", 792 | "num-traits", 793 | ] 794 | 795 | [[package]] 796 | name = "unicode-ident" 797 | version = "1.0.12" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 800 | 801 | [[package]] 802 | name = "unicode-xid" 803 | version = "0.1.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 806 | 807 | [[package]] 808 | name = "vcpkg" 809 | version = "0.2.15" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 812 | 813 | [[package]] 814 | name = "wasi" 815 | version = "0.9.0+wasi-snapshot-preview1" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 818 | 819 | [[package]] 820 | name = "winapi" 821 | version = "0.3.9" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 824 | dependencies = [ 825 | "winapi-i686-pc-windows-gnu", 826 | "winapi-x86_64-pc-windows-gnu", 827 | ] 828 | 829 | [[package]] 830 | name = "winapi-i686-pc-windows-gnu" 831 | version = "0.4.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 834 | 835 | [[package]] 836 | name = "winapi-x86_64-pc-windows-gnu" 837 | version = "0.4.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 840 | 841 | [[package]] 842 | name = "windows" 843 | version = "0.56.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" 846 | dependencies = [ 847 | "windows-core", 848 | "windows-targets", 849 | ] 850 | 851 | [[package]] 852 | name = "windows-core" 853 | version = "0.56.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" 856 | dependencies = [ 857 | "windows-implement", 858 | "windows-interface", 859 | "windows-result", 860 | "windows-targets", 861 | ] 862 | 863 | [[package]] 864 | name = "windows-implement" 865 | version = "0.56.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" 868 | dependencies = [ 869 | "proc-macro2 1.0.83", 870 | "quote 1.0.36", 871 | "syn 2.0.65", 872 | ] 873 | 874 | [[package]] 875 | name = "windows-interface" 876 | version = "0.56.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" 879 | dependencies = [ 880 | "proc-macro2 1.0.83", 881 | "quote 1.0.36", 882 | "syn 2.0.65", 883 | ] 884 | 885 | [[package]] 886 | name = "windows-result" 887 | version = "0.1.1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" 890 | dependencies = [ 891 | "windows-targets", 892 | ] 893 | 894 | [[package]] 895 | name = "windows-targets" 896 | version = "0.52.5" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 899 | dependencies = [ 900 | "windows_aarch64_gnullvm", 901 | "windows_aarch64_msvc", 902 | "windows_i686_gnu", 903 | "windows_i686_gnullvm", 904 | "windows_i686_msvc", 905 | "windows_x86_64_gnu", 906 | "windows_x86_64_gnullvm", 907 | "windows_x86_64_msvc", 908 | ] 909 | 910 | [[package]] 911 | name = "windows_aarch64_gnullvm" 912 | version = "0.52.5" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 915 | 916 | [[package]] 917 | name = "windows_aarch64_msvc" 918 | version = "0.52.5" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 921 | 922 | [[package]] 923 | name = "windows_i686_gnu" 924 | version = "0.52.5" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 927 | 928 | [[package]] 929 | name = "windows_i686_gnullvm" 930 | version = "0.52.5" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 933 | 934 | [[package]] 935 | name = "windows_i686_msvc" 936 | version = "0.52.5" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 939 | 940 | [[package]] 941 | name = "windows_x86_64_gnu" 942 | version = "0.52.5" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 945 | 946 | [[package]] 947 | name = "windows_x86_64_gnullvm" 948 | version = "0.52.5" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 951 | 952 | [[package]] 953 | name = "windows_x86_64_msvc" 954 | version = "0.52.5" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 957 | 958 | [[package]] 959 | name = "x11" 960 | version = "2.21.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" 963 | dependencies = [ 964 | "libc", 965 | "pkg-config", 966 | ] 967 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 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" 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in deskbot.gemspec 6 | gemspec 7 | 8 | gem "rake" 9 | gem "rake-compiler" 10 | gem "rb_sys" 11 | gem "rspec" 12 | gem "rubocop-inhouse", require: false 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | deskbot (0.1.0) 5 | dry-matcher (~> 1.0) 6 | dry-monads (~> 1.6) 7 | dry-struct (~> 1.6) 8 | dry-types (~> 1.7) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (7.1.3.3) 14 | base64 15 | bigdecimal 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | connection_pool (>= 2.2.5) 18 | drb 19 | i18n (>= 1.6, < 2) 20 | minitest (>= 5.1) 21 | mutex_m 22 | tzinfo (~> 2.0) 23 | ast (2.4.2) 24 | base64 (0.2.0) 25 | bigdecimal (3.1.8) 26 | concurrent-ruby (1.2.3) 27 | connection_pool (2.4.1) 28 | diff-lcs (1.5.1) 29 | drb (2.2.1) 30 | dry-core (1.0.1) 31 | concurrent-ruby (~> 1.0) 32 | zeitwerk (~> 2.6) 33 | dry-inflector (1.0.0) 34 | dry-logic (1.5.0) 35 | concurrent-ruby (~> 1.0) 36 | dry-core (~> 1.0, < 2) 37 | zeitwerk (~> 2.6) 38 | dry-matcher (1.0.0) 39 | dry-core (~> 1.0, < 2) 40 | dry-monads (1.6.0) 41 | concurrent-ruby (~> 1.0) 42 | dry-core (~> 1.0, < 2) 43 | zeitwerk (~> 2.6) 44 | dry-struct (1.6.0) 45 | dry-core (~> 1.0, < 2) 46 | dry-types (>= 1.7, < 2) 47 | ice_nine (~> 0.11) 48 | zeitwerk (~> 2.6) 49 | dry-types (1.7.2) 50 | bigdecimal (~> 3.0) 51 | concurrent-ruby (~> 1.0) 52 | dry-core (~> 1.0) 53 | dry-inflector (~> 1.0) 54 | dry-logic (~> 1.4) 55 | zeitwerk (~> 2.6) 56 | i18n (1.14.5) 57 | concurrent-ruby (~> 1.0) 58 | ice_nine (0.11.2) 59 | json (2.7.2) 60 | language_server-protocol (3.17.0.3) 61 | minitest (5.23.1) 62 | mutex_m (0.2.0) 63 | parallel (1.24.0) 64 | parser (3.3.1.0) 65 | ast (~> 2.4.1) 66 | racc 67 | racc (1.8.0) 68 | rack (3.0.11) 69 | rainbow (3.1.1) 70 | rake (13.2.1) 71 | rake-compiler (1.2.7) 72 | rake 73 | rb_sys (0.9.97) 74 | regexp_parser (2.9.2) 75 | rexml (3.2.8) 76 | strscan (>= 3.0.9) 77 | rspec (3.13.0) 78 | rspec-core (~> 3.13.0) 79 | rspec-expectations (~> 3.13.0) 80 | rspec-mocks (~> 3.13.0) 81 | rspec-core (3.13.0) 82 | rspec-support (~> 3.13.0) 83 | rspec-expectations (3.13.0) 84 | diff-lcs (>= 1.2.0, < 2.0) 85 | rspec-support (~> 3.13.0) 86 | rspec-mocks (3.13.1) 87 | diff-lcs (>= 1.2.0, < 2.0) 88 | rspec-support (~> 3.13.0) 89 | rspec-support (3.13.1) 90 | rubocop (1.63.5) 91 | json (~> 2.3) 92 | language_server-protocol (>= 3.17.0) 93 | parallel (~> 1.10) 94 | parser (>= 3.3.0.2) 95 | rainbow (>= 2.2.2, < 4.0) 96 | regexp_parser (>= 1.8, < 3.0) 97 | rexml (>= 3.2.5, < 4.0) 98 | rubocop-ast (>= 1.31.1, < 2.0) 99 | ruby-progressbar (~> 1.7) 100 | unicode-display_width (>= 2.4.0, < 3.0) 101 | rubocop-ast (1.31.3) 102 | parser (>= 3.3.1.0) 103 | rubocop-capybara (2.20.0) 104 | rubocop (~> 1.41) 105 | rubocop-factory_bot (2.25.1) 106 | rubocop (~> 1.41) 107 | rubocop-inhouse (0.1.5) 108 | rubocop (>= 1.5) 109 | rubocop-capybara (>= 2.1) 110 | rubocop-performance (>= 1.1) 111 | rubocop-rails (>= 2.2) 112 | rubocop-rake (>= 0.6) 113 | rubocop-rspec (>= 2.2) 114 | rubocop-performance (1.21.0) 115 | rubocop (>= 1.48.1, < 2.0) 116 | rubocop-ast (>= 1.31.1, < 2.0) 117 | rubocop-rails (2.25.0) 118 | activesupport (>= 4.2.0) 119 | rack (>= 1.1) 120 | rubocop (>= 1.33.0, < 2.0) 121 | rubocop-ast (>= 1.31.1, < 2.0) 122 | rubocop-rake (0.6.0) 123 | rubocop (~> 1.0) 124 | rubocop-rspec (2.29.2) 125 | rubocop (~> 1.40) 126 | rubocop-capybara (~> 2.17) 127 | rubocop-factory_bot (~> 2.22) 128 | rubocop-rspec_rails (~> 2.28) 129 | rubocop-rspec_rails (2.28.3) 130 | rubocop (~> 1.40) 131 | ruby-progressbar (1.13.0) 132 | strscan (3.1.0) 133 | tzinfo (2.0.6) 134 | concurrent-ruby (~> 1.0) 135 | unicode-display_width (2.5.0) 136 | zeitwerk (2.6.14) 137 | 138 | PLATFORMS 139 | ruby 140 | x86_64-linux 141 | 142 | DEPENDENCIES 143 | deskbot! 144 | rake 145 | rake-compiler 146 | rb_sys 147 | rspec 148 | rubocop-inhouse 149 | 150 | BUNDLED WITH 151 | 2.5.9 152 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | You will need to have installed [Rust](https://www.rust-lang.org/tools/install) and 20 | [opencv](https://github.com/twistedfall/opencv-rust/blob/master/INSTALL.md) 21 | 22 | Install the gem and add to the application's Gemfile by executing: 23 | 24 | $ bundle add deskbot 25 | 26 | If bundler is not being used to manage dependencies, install the gem by executing: 27 | 28 | $ gem install deskbot 29 | 30 | ## Usage 31 | 32 | To start you can require the gem in your script and initialize a screen: 33 | 34 | ```ruby 35 | require "deskbot" 36 | 37 | screen = Deskbot.screen 38 | ``` 39 | 40 | ### Typing 41 | 42 | Type something on the keyboard 43 | 44 | ```ruby 45 | # Type SOMETHING at 60 words per minute 46 | screen.type("something", flags: [:shift], wpm: 60.0, noise: 0.0) 47 | ``` 48 | 49 | You can also tap a key: 50 | 51 | ```ruby 52 | # Tap shift + a after a 1 second delay 53 | screen.tap_key("a", flags: [:shift], delay_ms: 1.0) 54 | ``` 55 | 56 | And even more primitively you can toggle a key: 57 | 58 | ```ruby 59 | # Press the "a" key down 60 | screen.toggle_key("a", down: true) 61 | ``` 62 | 63 | ### Alerts 64 | 65 | You can make alerts 66 | 67 | ```ruby 68 | screen.alert("Hello") 69 | ``` 70 | 71 | ### Mouse movement 72 | 73 | You can teleport your mouse somewhere on the screen: 74 | 75 | ```ruby 76 | # Teleport the mouse to coordinates x: 100, y: 100 77 | screen.move_mouse(100, 100) 78 | ``` 79 | 80 | You can also move the mouse smoothly somewhere: 81 | 82 | ```ruby 83 | # Move the mouse smoothly to x: 100, y: 100 over 2 seconds 84 | screen.smooth_move_mouse(100, 100, duration: 2) 85 | ``` 86 | 87 | You can click: 88 | 89 | ```ruby 90 | # Left click 91 | screen.click 92 | 93 | # Right click 94 | screen.click(:right) 95 | ``` 96 | 97 | Or even scroll: 98 | 99 | ```ruby 100 | # Scroll 1 click up 101 | screen.scroll 102 | 103 | # Scroll 5 clicks up 104 | screen.scroll(clicks: 5) 105 | 106 | # Scroll 5 clicks down 107 | screen.scroll(:down, clicks: 5) 108 | ``` 109 | 110 | ### Screen introspection 111 | 112 | You can query the color of a specific pixel: 113 | 114 | ```ruby 115 | # Get the color of a specific pixel at x: 100, y: 100 116 | color = screen.color_at(100, 100) 117 | ``` 118 | 119 | This returns a `Deskbot::Color` object with `red`, `green`, `blue` and `alpha` 120 | attributes. 121 | 122 | You can query the size of the screen: 123 | 124 | ```ruby 125 | size = screen.size 126 | scale = screen.scale 127 | ``` 128 | 129 | The size would be a `Deskbot::Size` with `width` and `height` attributes. The 130 | scale would simply be a float. 131 | 132 | You can query if a point is visible on the screen: 133 | 134 | ```ruby 135 | screen.point_visible?(100, 100) 136 | screen.area_visible?(x: 100, y: 100, width: 400, height: 400) 137 | ``` 138 | 139 | ### Bitmaps 140 | 141 | You can capture your screen: 142 | 143 | ```ruby 144 | # We can capture the whole screen 145 | bitmap = screen.capture 146 | 147 | # Or we can capture part of our screen 148 | bitmap = screen.capture_area(x: 100, y: 100, width: 400, height: 400) 149 | ``` 150 | 151 | This returns a `Deskbot::Bitmap` which you can use to find areas that match 152 | images you provide. 153 | 154 | You can query the bounds of the bitmap: 155 | 156 | ```ruby 157 | bitmap.bounds 158 | ``` 159 | 160 | Which would return a `Deskbot::Area` with `x`, `y`, `width` and `height` 161 | attributes. 162 | 163 | We can check for the colors of points on the bitmap: 164 | 165 | ```ruby 166 | bitmap.color_at(100, 100) 167 | ``` 168 | 169 | Which returns a `Deskbot::Color`. 170 | 171 | ### Comparing images 172 | 173 | You can compare images to your bitmap with a given tolerance (optional): 174 | 175 | ```ruby 176 | bitmap.find("images/test.jpg") 177 | bitmap.all("images/test.jpg", tolerance: 4.0) 178 | ``` 179 | 180 | `find` and `find_color` both return `Dry::Monad::Result` objects meaning you 181 | need to unwrap their values: 182 | 183 | ```ruby 184 | result = bitmap.find("images/test.jpg") 185 | point = result.success 186 | ``` 187 | 188 | They are also wrapped with an optional matcher so you can use a much nicer 189 | syntax to grab their values: 190 | 191 | ```ruby 192 | bitmap.find("images/test.jpg") do |on| 193 | on.success do |point| 194 | # Do something with the point, no need to call point.success 195 | end 196 | 197 | on.failure do 198 | # Handle you error here 199 | end 200 | end 201 | ``` 202 | 203 | These return `Deskbot::Point` objects with `x` and `y` attributes. 204 | 205 | You can ask higher level questions about the image such as: 206 | 207 | ```ruby 208 | bitmap.eql?("images/test.jpg", tolerance: 2.0) 209 | bitmap.count("images/test.jpg") 210 | ``` 211 | 212 | You can also query for positions of colors: 213 | 214 | ```ruby 215 | bitmap.find_color([255, 255, 255, 0], tolerance: 0.0) 216 | bitmap.all_color([255, 255, 255, 0]) 217 | ``` 218 | 219 | These will return `Deskbot::Point` objects with `x` and `y` attributes. 220 | 221 | ## Development 222 | 223 | After checking out the repo, run `bin/setup` to install dependencies. 224 | Then, run `rake spec` to run the tests. You can also run `bin/console` for 225 | an interactive prompt that will allow you to experiment. 226 | 227 | To install this gem onto your local machine, run `bundle exec rake install`. 228 | To release a new version, update the version number in `version.rb`, and then 229 | run `bundle exec rake release`, which will create a git tag for the version, 230 | push git commits and the created tag, and push the `.gem` file 231 | to [rubygems.org](https://rubygems.org). 232 | 233 | ## Contributing 234 | 235 | Bug reports and pull requests are welcome on GitHub at https://github.com/nolantait/deskbot. 236 | 237 | ## License 238 | 239 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 240 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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] 19 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || 66 | cli_arg_version || 67 | bundler_requirement_for(lockfile_version) 68 | end 69 | 70 | def bundler_requirement_for(version) 71 | return "#{Gem::Requirement.default}.a" unless version 72 | 73 | bundler_gem_version = Gem::Version.new(version) 74 | 75 | bundler_gem_version.approximate_recommendation 76 | end 77 | 78 | def load_bundler! 79 | ENV["BUNDLE_GEMFILE"] ||= gemfile 80 | 81 | activate_bundler 82 | end 83 | 84 | def activate_bundler 85 | gem_error = activation_error_handling do 86 | gem "bundler", bundler_requirement 87 | end 88 | return if gem_error.nil? 89 | require_error = activation_error_handling do 90 | require "bundler/version" 91 | end 92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 93 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | if m.invoked_as_script? 108 | load Gem.bin_path("bundler", "bundle") 109 | end 110 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "deskbot" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Running rubocop ==" 17 | system! "bundle exec rubocop --autocorrect-all --fail-level error" 18 | end 19 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rspec-core", "rspec") 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /deskbot.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/deskbot/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "deskbot" 7 | spec.version = Deskbot::VERSION 8 | spec.authors = ["Nolan J Tait"] 9 | spec.email = ["nolanjtait@gmail.com"] 10 | 11 | spec.summary = "Ruby dekstop automation" 12 | spec.description = "Ruby desktop automation" 13 | spec.homepage = "https://github.com/nolantait/desktbot" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.3" 16 | spec.required_rubygems_version = ">= 3.3.11" 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | spec.metadata["source_code_uri"] = spec.homepage 20 | spec.metadata["changelog_uri"] = spec.homepage 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | gemspec = File.basename(__FILE__) 25 | spec.files = IO.popen( 26 | %w[git ls-files -z], 27 | chdir: __dir__, 28 | err: IO::NULL 29 | ) do |ls| 30 | ls.readlines( 31 | "\x0", 32 | chomp: true 33 | ).reject do |f| 34 | (f == gemspec) || 35 | f.start_with?( 36 | *%w[ 37 | bin/ 38 | test/ 39 | spec/ 40 | features/ 41 | .git 42 | appveyor 43 | Gemfile 44 | ] 45 | ) 46 | end 47 | end 48 | spec.bindir = "exe" 49 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 50 | spec.require_paths = ["lib"] 51 | spec.extensions = ["ext/deskbot/Cargo.toml"] 52 | 53 | # Uncomment to register a new dependency of your gem 54 | spec.add_dependency "dry-matcher", "~> 1.0" 55 | spec.add_dependency "dry-monads", "~> 1.6" 56 | spec.add_dependency "dry-struct", "~> 1.6" 57 | spec.add_dependency "dry-types", "~> 1.7" 58 | spec.metadata["rubygems_mfa_required"] = "true" 59 | end 60 | -------------------------------------------------------------------------------- /ext/deskbot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deskbot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Nolan J Tait "] 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" } 17 | -------------------------------------------------------------------------------- /ext/deskbot/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mkmf" 4 | require "rb_sys/mkmf" 5 | 6 | create_rust_makefile("deskbot/deskbot") 7 | -------------------------------------------------------------------------------- /ext/deskbot/src/alert.rs: -------------------------------------------------------------------------------- 1 | extern crate autopilot; 2 | 3 | pub fn alert(message: String) -> () { 4 | autopilot::alert::alert(&message.as_str(), None, None, None); 5 | } 6 | -------------------------------------------------------------------------------- /ext/deskbot/src/bitmap.rs: -------------------------------------------------------------------------------- 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 { 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 { 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 { 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. 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, 54 | ) -> Option> { 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) -> Option> { 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) -> Vec> { 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) -> Vec> { 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 = 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) -> 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::::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 { 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 { 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 { 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 { 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 | } 269 | -------------------------------------------------------------------------------- /ext/deskbot/src/keys.rs: -------------------------------------------------------------------------------- 1 | use magnus::RArray; 2 | extern crate autopilot; 3 | 4 | fn key_flags(symbols: Vec) -> Vec { 5 | symbols 6 | .iter() 7 | .filter_map(|symbol| key_flag(symbol)) 8 | .collect::>() 9 | } 10 | 11 | fn key_flag(symbol: &String) -> Option { 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::().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::().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::().unwrap()); 36 | let key = autopilot::key::Character(_key); 37 | autopilot::key::tap(&key, &flags, delay_ms, modifier_delay_ms); 38 | } 39 | -------------------------------------------------------------------------------- /ext/deskbot/src/lib.rs: -------------------------------------------------------------------------------- 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 | } 50 | -------------------------------------------------------------------------------- /ext/deskbot/src/mouse.rs: -------------------------------------------------------------------------------- 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 { 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) -> 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) -> () { 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 | } 56 | -------------------------------------------------------------------------------- /ext/deskbot/src/screen.rs: -------------------------------------------------------------------------------- 1 | extern crate autopilot; 2 | use std::collections::HashMap; 3 | 4 | pub fn get_color(x: f64, y: f64) -> Option> { 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 { 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 | } 35 | -------------------------------------------------------------------------------- /lib/deskbot.rb: -------------------------------------------------------------------------------- 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 29 | -------------------------------------------------------------------------------- /lib/deskbot/area.rb: -------------------------------------------------------------------------------- 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 13 | -------------------------------------------------------------------------------- /lib/deskbot/bitmap.rb: -------------------------------------------------------------------------------- 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 90 | -------------------------------------------------------------------------------- /lib/deskbot/color.rb: -------------------------------------------------------------------------------- 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 37 | -------------------------------------------------------------------------------- /lib/deskbot/point.rb: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /lib/deskbot/providers/autopilot.rb: -------------------------------------------------------------------------------- 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 29 | -------------------------------------------------------------------------------- /lib/deskbot/providers/autopilot/bitmap.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/deskbot/screen.rb: -------------------------------------------------------------------------------- 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 132 | -------------------------------------------------------------------------------- /lib/deskbot/size.rb: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /lib/deskbot/types.rb: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /lib/deskbot/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Deskbot 4 | VERSION = "0.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /sig/deskbot.rbs: -------------------------------------------------------------------------------- 1 | module Deskbot 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /spec/deskbot/bitmap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Deskbot::Bitmap do 4 | let(:bitmap) { described_class.new(provider) } 5 | 6 | describe "#save" do 7 | let(:provider) { double("provider", save: nil) } 8 | 9 | it "saves the bitmap" do 10 | expect(provider).to receive(:save).with("images/test.png") 11 | bitmap.save("images/test.png") 12 | end 13 | end 14 | 15 | describe "#bounds" do 16 | let(:provider) do 17 | double( 18 | "provider", 19 | bounds: { 20 | "x" => 1.0, 21 | "y" => 2.0, 22 | "width" => 1920.0, 23 | "height" => 1080.0 24 | } 25 | ) 26 | end 27 | 28 | it "returns an area" do 29 | area = bitmap.bounds 30 | expect(area).to be_a(Deskbot::Area) 31 | end 32 | end 33 | 34 | describe "#color_at" do 35 | let(:provider) { double("provider", color_at: [255, 255, 255, 255]) } 36 | 37 | it "returns a color" do 38 | color = bitmap.color_at(1, 1) 39 | expect(color).to be_a(Deskbot::Color) 40 | end 41 | end 42 | 43 | describe "#eql?" do 44 | let(:provider) { double("provider", bitmap_eq: true) } 45 | 46 | it "returns a boolean" do 47 | result = bitmap.eql?("images/test.png") 48 | 49 | expect(result).to be true 50 | end 51 | end 52 | 53 | describe "#find" do 54 | context "when a point is found" do 55 | let(:provider) { double("provider", find: { "x" => 1.0, "y" => 2.0 }) } 56 | 57 | it "returns a point in a monad" do 58 | result = bitmap.find("images/test.png") 59 | expect(result).to be_success 60 | expect(result.success).to be_a(Deskbot::Point) 61 | end 62 | end 63 | 64 | context "when a point is not found" do 65 | let(:provider) { double("provider", find: nil) } 66 | 67 | it "returns a failure in a monad" do 68 | result = bitmap.find("images/test.png") 69 | expect(result).to be_failure 70 | end 71 | end 72 | end 73 | 74 | describe "#find_color" do 75 | context "when a point is found" do 76 | let(:provider) { double("provider", find_color: { "x" => 1.0, "y" => 2.0 }) } 77 | 78 | it "returns a point" do 79 | result = bitmap.find_color([255, 255, 255, 255]) 80 | expect(result).to be_success 81 | expect(result.success).to be_a(Deskbot::Point) 82 | end 83 | end 84 | 85 | context "when a point is not found" do 86 | let(:provider) { double("provider", find_color: nil) } 87 | 88 | it "returns nil" do 89 | result = bitmap.find_color([255, 255, 255, 255]) 90 | expect(result).to be_failure 91 | end 92 | end 93 | end 94 | 95 | describe "#all" do 96 | let(:provider) { double("provider", all: [{ "x" => 1.0, "y" => 2.0 }]) } 97 | 98 | it "returns a list of points" do 99 | points = bitmap.all("images/test.png") 100 | expect(points).to all(be_a(Deskbot::Point)) 101 | end 102 | end 103 | 104 | describe "#all_color" do 105 | let(:provider) { double("provider", all_color: [{ "x" => 1.0, "y" => 2.0 }]) } 106 | 107 | it "returns a list of points" do 108 | points = bitmap.all_color([255, 255, 255, 255]) 109 | expect(points).to all(be_a(Deskbot::Point)) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/deskbot/color_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Deskbot::Color do 4 | describe ".from_hex" do 5 | it "parses a hex color" do 6 | result = described_class.from_hex("#ff00ff00") 7 | 8 | expect(result.red).to eq(255) 9 | expect(result.green).to eq(0) 10 | expect(result.blue).to eq(255) 11 | expect(result.alpha).to eq(0) 12 | end 13 | 14 | it "parses a hex color without alpha" do 15 | result = described_class.from_hex("#ff00ff") 16 | 17 | expect(result.red).to eq(255) 18 | expect(result.green).to eq(0) 19 | expect(result.blue).to eq(255) 20 | expect(result.alpha).to eq(255) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/deskbot/screen_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Deskbot::Screen do 4 | let(:screen) { described_class.new(provider) } 5 | 6 | describe "#alert" do 7 | let(:provider) { double("provider", alert: nil) } 8 | 9 | it "calls alert on the provider" do 10 | expect(provider).to receive(:alert).with("hello") 11 | screen.alert("hello") 12 | end 13 | end 14 | 15 | describe "#type" do 16 | let(:provider) { double("provider", type: nil) } 17 | 18 | it "calls type on the provider" do 19 | expect(provider).to receive(:type).with("hello", [], 60.0, 0.0) 20 | screen.type("hello") 21 | end 22 | end 23 | 24 | describe "#toggle_key" do 25 | let(:provider) { double("provider", toggle_key: nil) } 26 | 27 | it "calls toggle_key on the provider" do 28 | expect(provider).to receive(:toggle_key).with("a", true, [], 0.0) 29 | screen.toggle_key("a") 30 | end 31 | end 32 | 33 | describe "#tap_key" do 34 | let(:provider) { double("provider", tap_key: nil) } 35 | 36 | it "calls tap_key on the provider" do 37 | expect(provider).to receive(:tap_key).with("a", [], 0.0, 0.0) 38 | screen.tap_key("a") 39 | end 40 | end 41 | 42 | describe "#mouse_location" do 43 | let(:provider) { double("provider", mouse_location: { "x" => 1.0, "y" => 1.0 }) } 44 | 45 | it "returns a point" do 46 | point = screen.mouse_location 47 | 48 | expect(point).to be_a(Deskbot::Point) 49 | end 50 | end 51 | 52 | describe "#move_mouse" do 53 | let(:provider) { double("provider", move_mouse: nil) } 54 | 55 | it "calls move_mouse on the provider" do 56 | expect(provider).to receive(:move_mouse).with(1.0, 1.0) 57 | screen.move_mouse(1, 1) 58 | end 59 | end 60 | 61 | describe "#smooth_move_mouse" do 62 | let(:provider) { double("provider", smooth_move_mouse: nil) } 63 | 64 | it "calls smooth_move_mouse on the provider" do 65 | expect(provider).to receive(:smooth_move_mouse).with(1.0, 1.0, 1.0) 66 | screen.smooth_move_mouse(1, 1) 67 | end 68 | end 69 | 70 | describe "#toggle_mouse" do 71 | let(:provider) { double("provider", toggle_mouse: nil) } 72 | 73 | it "calls toggle_mouse on the provider" do 74 | expect(provider).to receive(:toggle_mouse).with("left", true) 75 | screen.toggle_mouse 76 | end 77 | end 78 | 79 | describe "#click" do 80 | let(:provider) { double("provider", click: nil) } 81 | 82 | it "calls click on the provider" do 83 | expect(provider).to receive(:click).with("left", nil) 84 | screen.click 85 | end 86 | end 87 | 88 | describe "#scroll" do 89 | let(:provider) { double("provider", scroll: nil) } 90 | 91 | it "calls scroll on the provider" do 92 | expect(provider).to receive(:scroll).with("up", 1.0) 93 | screen.scroll 94 | end 95 | end 96 | 97 | describe "#color_at" do 98 | let(:provider) { double("provider", color_at: [255, 255, 255, 255]) } 99 | 100 | it "returns a color" do 101 | color = screen.color_at(1, 1) 102 | expect(color).to be_a(Deskbot::Color) 103 | end 104 | end 105 | 106 | describe "#size" do 107 | let(:provider) { double("provider", screen_size: { "width" => 1920.0, "height" => 1080.0 }) } 108 | 109 | it "returns a size" do 110 | size = screen.size 111 | expect(size).to be_a(Deskbot::Size) 112 | end 113 | end 114 | 115 | describe "#scale" do 116 | let(:provider) { double("provider", screen_scale: 1.0) } 117 | 118 | it "returns a float" do 119 | expect(screen.scale).to eq 1.0 120 | end 121 | end 122 | 123 | describe "#point_visible?" do 124 | let(:provider) { double("provider", is_point_visible: true) } 125 | 126 | it "returns a boolean" do 127 | expect(screen.point_visible?(1, 1)).to be true 128 | end 129 | end 130 | 131 | describe "#area_visible?" do 132 | let(:provider) { double("provider", is_area_visible: true) } 133 | 134 | it "returns a boolean" do 135 | expect(screen.area_visible?(x: 1, y: 1, width: 2, height: 2)).to be true 136 | end 137 | end 138 | 139 | describe "#capture" do 140 | let(:provider) { double("provider", capture_screen: double("Bitmap")) } 141 | 142 | it "returns a bitmap" do 143 | expect(screen.capture).to be_a(Deskbot::Bitmap) 144 | end 145 | end 146 | 147 | describe "#capture_area" do 148 | let(:provider) { double("provider", capture_screen_area: double("Bitmap")) } 149 | 150 | it "returns a bitmap" do 151 | expect(screen.capture_area(x: 1, y: 1, width: 2, height: 2)).to be_a(Deskbot::Bitmap) 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/deskbot_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Deskbot do 4 | it "has a version number" do 5 | expect(Deskbot::VERSION).not_to be_nil 6 | end 7 | 8 | describe ".screen" do 9 | it "returns a screen" do 10 | expect(described_class.screen).to be_a(Deskbot::Screen) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "deskbot" 4 | 5 | RSpec.configure do |config| 6 | # Enable flags like --only-failures and --next-failure 7 | config.example_status_persistence_file_path = ".rspec_status" 8 | 9 | # Disable RSpec exposing methods globally on `Module` and `main` 10 | config.disable_monkey_patching! 11 | 12 | config.expect_with :rspec do |c| 13 | c.syntax = :expect 14 | end 15 | end 16 | --------------------------------------------------------------------------------