├── .gitignore ├── .node-version ├── .npm-version ├── .npmignore ├── LICENSE ├── README.md ├── TODO.md ├── examples ├── README.md ├── anti-afk.js ├── record-and-replay.ts ├── toggle-holding-left-click.js └── toggle-holding-w.js ├── index.d.ts ├── index.js ├── index.test.ts ├── package-lock.json ├── package.json ├── script.ts ├── src ├── cli.ts ├── index.ts ├── input │ ├── held-keys.ts │ ├── held-mouse-buttons.ts │ ├── index.ts │ ├── keyboard.ts │ └── mouse.ts ├── libnut.d.ts ├── libnut.js ├── mac-permissions.ts ├── output │ ├── index.ts │ ├── keyboard.ts │ └── mouse.ts ├── patch-cjs.ts ├── recording │ ├── event-filters.ts │ ├── index.ts │ ├── tape-data.ts │ ├── tape-player.ts │ ├── tape-recorder.ts │ └── tape.ts ├── screen.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.9.0 2 | -------------------------------------------------------------------------------- /.npm-version: -------------------------------------------------------------------------------- 1 | 10.8.3 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | examples 4 | script.ts 5 | src 6 | **/*.test.* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2023-2024 Lily Skye 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # suchibot 2 | 3 | A cross-platform AutoHotKey-like thing with JavaScript/TypeScript as its scripting language. Built on top of [`uiohook-napi`](https://npm.im/uiohook-napi) and [`nut.js`](https://github.com/nut-tree/nut.js). 4 | 5 | ## Installation 6 | 7 | First, you'll need [node.js](https://nodejs.org/en/download/) installed. Suchibot should work with most versions of Node, but for best compatibility, you can install [version 18](https://nodejs.org/dist/latest-hydrogen/), which is what it was tested against. 8 | 9 | On Windows, you'll also need to install the [Microsoft Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads). 10 | 11 | If you're running Windows 10 N, you'll also need to install the [Media Feature Pack](https://support.microsoft.com/en-us/topic/media-feature-pack-for-windows-10-n-may-2020-ebbdf559-b84c-0fc2-bd51-e23c9f6a4439). 12 | 13 | On Linux, you also need libXtst. You can get that on Ubuntu-based distros by running this in a terminal: 14 | 15 | ``` 16 | sudo apt install build-essential libxtst-dev 17 | ``` 18 | 19 | > NOTE: I haven't tested suchibot in wayland, only Xorg. It probably doesn't work in wayland. 20 | 21 | On macOS, you also need XCode command-line tools. You can get that by running this in a terminal: 22 | 23 | ``` 24 | xcode-select --install 25 | ``` 26 | 27 | > NOTE: Nowadays in macOS, apps need permissions in order to monitor and simulate input. suchibot will automatically request them the first time it's run, but be aware that permission requesting is broken on macOS if you have System Integrity Protection disabled (not a bug in suchibot; a "feature" of macOS). 28 | > 29 | > You can run with env var "DEBUG" set to `suchibot:mac-permissions` to see log output relating to the permission requests. 30 | > 31 | > That said, it seems things aren't working reliably on macOS... I need to investigate this later... 32 | 33 | Then, to install suchibot itself: 34 | 35 | ```sh 36 | # cd to a folder where you want to keep your scripts, then: 37 | npm install suchibot 38 | ``` 39 | 40 | ## Usage (CLI) 41 | 42 | Write your macro script in either JavaScript or TypeScript; for example: 43 | 44 | ```ts 45 | import { Keyboard, Mouse, Key, MouseButton, sleep } from "suchibot"; 46 | 47 | Keyboard.onDown(Key.NUMPAD_0, async () => { 48 | Mouse.click(MouseButton.LEFT); 49 | await sleep.async(100); 50 | Keyboard.tap(Key.NINE); 51 | }); 52 | ``` 53 | 54 | Run `suchibot` with the path to your script: 55 | 56 | ``` 57 | $ npx suchibot ./myscript.js 58 | ``` 59 | 60 | It's recommended to use Visual Studio Code to write your script, as it will give you autocomplete for the functions and key names (since they have TypeScript types). 61 | 62 | See the [examples folder](https://github.com/suchipi/suchibot/tree/main/examples) for some example scripts. 63 | 64 | ## Usage (Node API) 65 | 66 | Import stuff from the suchibot library and set up macros however, then use `suchibot.startListening()` and `suchibot.stopListening()`: 67 | 68 | ```js 69 | const suchibot = require("suchibot"); 70 | 71 | function pressNine() { 72 | suchibot.Keyboard.tap(suchibot.Key.NINE); 73 | } 74 | 75 | suchibot.Keyboard.onUp(suchibot.Key.ANY, (event) => { 76 | console.log("someone pressed:", event.key); 77 | }); 78 | 79 | suchibot.startListening(); 80 | 81 | // and then, some time later: 82 | suchibot.stopListening(); 83 | ``` 84 | 85 | ## Quick API Overview 86 | 87 | ```ts 88 | import { 89 | Key, 90 | Keyboard, 91 | Mouse, 92 | MouseButton, 93 | Screen, 94 | sleep, 95 | Tape, 96 | } from "suchibot"; 97 | 98 | // `Mouse` contains functions for capturing and simulating mouse events, eg: 99 | Mouse.click(MouseButton.RIGHT); 100 | Mouse.onClick(MouseButton.ANY, (event) => { 101 | console.log(event.button, "was clicked at", event.x, event.y); 102 | }); 103 | Mouse.onMove((event) => { 104 | console.log("mouse moved to", event.x, event.y); 105 | }); 106 | 107 | console.log(Mouse); 108 | // { 109 | // moveTo: [Function: moveTo], 110 | // click: [Function: click], 111 | // doubleClick: [Function: doubleClick], 112 | // hold: [Function: hold], 113 | // release: [Function: release], 114 | // getPosition: [Function: getPosition], 115 | // scroll: [Function: scroll], 116 | // onDown: [Function: onDown], 117 | // onUp: [Function: onUp], 118 | // onClick: [Function: onClick], 119 | // onMove: [Function: onMove] 120 | // } 121 | 122 | // `Keyboard` contains functions for capturing and simulating keyboard events, eg: 123 | Keyboard.tap(Key.A); 124 | Keyboard.onUp(Key.ZERO, () => { 125 | console.log("someone pressed zero!"); 126 | }); 127 | 128 | console.log(Keyboard); 129 | // { 130 | // tap: [Function: tap], 131 | // hold: [Function: hold], 132 | // release: [Function: release], 133 | // type: [Function: type], 134 | // onDown: [Function: onDown], 135 | // onUp: [Function: onUp] 136 | // } 137 | 138 | // `Key` contains constants that you pass into functions. 139 | console.log(Key); 140 | // { 141 | // BACKSPACE: "BACKSPACE", 142 | // DELETE: "DELETE", 143 | // ENTER: "ENTER", 144 | // ...and more 145 | // } 146 | 147 | // `MouseButton` contains constants that you pass into functions. 148 | console.log(Key); 149 | // { 150 | // LEFT: "LEFT", 151 | // RIGHT: "RIGHT", 152 | // MIDDLE: "MIDDLE", 153 | // ANY: "ANY" 154 | // } 155 | 156 | // `Screen` can give you info about the screen size, eg: 157 | const { width, height } = await Screen.getSize(); 158 | 159 | console.log(Screen); 160 | // { getSize: [Function: getSize] } 161 | 162 | // `sleep.async` returns a Promise that resolves in the specified number of milliseconds. eg: 163 | await sleep.async(100); 164 | 165 | // `sleep.sync` blocks the main thread for the specified number of milliseconds. eg: 166 | sleep.sync(100); 167 | 168 | // `Tape`s records all the mouse/keyboard events that happen until you 169 | // call `tape.stopRecording()`, and then you can replay the tape to simulate 170 | // the same mouse/keyboard events. 171 | const tape = new Tape(); 172 | 173 | // Start the recording... 174 | tape.record(); 175 | 176 | // We'll take a 4-second recording by waiting 4000ms before calling `stopRecording`. 177 | await sleep.async(4000); 178 | 179 | // Move the mouse around, press keys, etc. 180 | 181 | // Now, stop recording. 182 | tape.stopRecording(); 183 | 184 | // Now, replay the tape, and the mouse and keyboard will do the same things you did during the 4000ms wait. 185 | tape.play(); 186 | ``` 187 | 188 | See the [examples folder](https://github.com/suchipi/suchibot/tree/main/examples) for some example scripts. 189 | 190 | ## Full API Documentation 191 | 192 | The "suchibot" module has 17 named exports. Each is documented here. 193 | 194 | ### Mouse 195 | 196 | Functions that let you control the mouse and/or register code to be run when a mouse event occurs. 197 | 198 | #### Mouse.moveTo 199 | 200 | Moves the mouse cursor to the specified position. 201 | 202 | Definition: 203 | 204 | ```ts 205 | function moveTo(x: number, y: number, smooth: boolean = false): void; 206 | ``` 207 | 208 | Example: 209 | 210 | ```js 211 | Mouse.moveTo(100, 100); 212 | 213 | // Moves smoothly over time rather than jumping to the spot immediately 214 | Mouse.moveTo(100, 100, true); 215 | ``` 216 | 217 | #### Mouse.click 218 | 219 | Clicks the specified mouse button. 220 | 221 | If no mouse button is specified, clicks the left mouse button. 222 | 223 | Definition: 224 | 225 | ```ts 226 | function click(button: MouseButton = MouseButton.LEFT): void; 227 | ``` 228 | 229 | Example: 230 | 231 | ```js 232 | Mouse.click(); 233 | 234 | // To specify a button other than the left button: 235 | Mouse.click(MouseButton.RIGHT); 236 | ``` 237 | 238 | #### Mouse.doubleClick 239 | 240 | Double-clicks the specified mouse button. 241 | 242 | If no mouse button is specified, double-clicks the left mouse button. 243 | 244 | Definition: 245 | 246 | ```ts 247 | function doubleClick(button: MouseButton = MouseButton.LEFT): void; 248 | ``` 249 | 250 | Example: 251 | 252 | ```js 253 | Mouse.doubleClick(); 254 | 255 | // To specify a button other than the left button: 256 | Mouse.doubleClick(MouseButton.RIGHT); 257 | ``` 258 | 259 | #### Mouse.hold 260 | 261 | Holds down the specified mouse button until you call `Mouse.release`. 262 | 263 | If no mouse button is specified, holds the left mouse button. 264 | 265 | Definition: 266 | 267 | ```ts 268 | function hold(button: MouseButton = MouseButton.LEFT): void; 269 | ``` 270 | 271 | Example: 272 | 273 | ```js 274 | Mouse.hold(); 275 | // and then, later... 276 | Mouse.release(); 277 | 278 | // To specify a button other than the left button: 279 | Mouse.hold(MouseButton.RIGHT); 280 | ``` 281 | 282 | #### Mouse.release 283 | 284 | Stops holding down the specified mouse button, that was being held down because `Mouse.hold` was called, or the user was holding the button. 285 | 286 | Definition: 287 | 288 | ```ts 289 | function release(button: MouseButton = MouseButton.LEFT): void; 290 | ``` 291 | 292 | Example: 293 | 294 | ```js 295 | Mouse.hold(); 296 | // and then, later... 297 | Mouse.release(); 298 | 299 | // To specify a button other than the left button: 300 | Mouse.release(MouseButton.RIGHT); 301 | ``` 302 | 303 | #### Mouse.getPosition 304 | 305 | Returns an object with `x` and `y` properties referring to the current mouse cursor position on the screen. 306 | 307 | Definition: 308 | 309 | ```ts 310 | function getPosition(): { x: number; y: number }; 311 | ``` 312 | 313 | Example: 314 | 315 | ```js 316 | const position = Mouse.getPosition(); 317 | console.log(position); // { x: 100, y: 400 } 318 | ``` 319 | 320 | #### Mouse.scroll 321 | 322 | Uses the mouse wheel or trackpad to scroll in the specified direction by the specified amount. 323 | 324 | Definition: 325 | 326 | ```ts 327 | function scroll({ x = 0, y = 0 } = {}): void; 328 | ``` 329 | 330 | Example: 331 | 332 | ```js 333 | // Scroll down some: 334 | Mouse.scroll({ y: 100 }); 335 | 336 | // Scroll down a lot: 337 | Mouse.scroll({ y: 1000 }); 338 | 339 | // Scroll up: 340 | Mouse.scroll({ y: -100 }); 341 | 342 | // Scroll right: 343 | Mouse.scroll({ x: 100 }); 344 | 345 | // Scroll left: 346 | Mouse.scroll({ x: -100 }); 347 | 348 | // Scroll diagonally: 349 | Mouse.scroll({ x: 100, y: 100 }); 350 | ``` 351 | 352 | #### Mouse.onDown 353 | 354 | Registers a function to be called when a mouse button is pressed down. 355 | 356 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the mouse button is pressed down. 357 | 358 | Definition: 359 | 360 | ```ts 361 | function onDown( 362 | button: MouseButton, 363 | eventHandler: (event: MouseEvent) => void 364 | ): Listener; 365 | ``` 366 | 367 | Example: 368 | 369 | ```js 370 | // log whenever mouse right button is pressed: 371 | Mouse.onDown(MouseButton.RIGHT, (event) => { 372 | console.log("right mouse was pressed at", event.x, event.y); 373 | }); 374 | 375 | // log whenever any mouse button is pressed: 376 | Mouse.onDown(MouseButton.ANY, (event) => { 377 | console.log(event.button, "was pressed at", event.x, event.y); 378 | }); 379 | ``` 380 | 381 | #### Mouse.onUp 382 | 383 | Registers a function to be called when a mouse button is released (stops being held down). 384 | 385 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the mouse button is released. 386 | 387 | Definition: 388 | 389 | ```ts 390 | function onUp( 391 | button: MouseButton, 392 | listener: (event: MouseEvent) => void 393 | ): Listener; 394 | ``` 395 | 396 | Example: 397 | 398 | ```js 399 | // log whenever mouse right button is relseased: 400 | Mouse.onUp(MouseButton.RIGHT, (event) => { 401 | console.log("right mouse was released at", event.x, event.y); 402 | }); 403 | 404 | // log whenever any mouse button is released: 405 | Mouse.onUp(MouseButton.ANY, (event) => { 406 | console.log(event.button, "was released at", event.x, event.y); 407 | }); 408 | ``` 409 | 410 | #### Mouse.onClick 411 | 412 | Registers a function to be called when a mouse button is clicked (pressed and then released without moving around between the press and release). 413 | 414 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the mouse button is clicked. 415 | 416 | Definition: 417 | 418 | ```ts 419 | function onClick( 420 | button: MouseButton, 421 | eventHandler: (event: MouseEvent) => void 422 | ): void; 423 | ``` 424 | 425 | Example: 426 | 427 | ```js 428 | // log whenever mouse right button is clicked: 429 | Mouse.onClick(MouseButton.RIGHT, (event) => { 430 | console.log("right mouse was clicked at", event.x, event.y); 431 | }); 432 | 433 | // log whenever any mouse button is clicked: 434 | Mouse.onClick(MouseButton.ANY, (event) => { 435 | console.log(event.button, "was clicked at", event.x, event.y); 436 | }); 437 | ``` 438 | 439 | #### Mouse.onMove 440 | 441 | Registers a function to be called whenever the mouse cursor is moved. 442 | 443 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the mouse is moved. 444 | 445 | Definition: 446 | 447 | ```ts 448 | function onMove(eventHandler: (event: MouseEvent) => void): Listener; 449 | ``` 450 | 451 | Example: 452 | 453 | ```js 454 | // log whenever mouse is moved 455 | Mouse.onMove((event) => { 456 | console.log("mouse moved to:", event.x, event.y); 457 | }); 458 | ``` 459 | 460 | #### Mouse.isDown 461 | 462 | Returns whether the specified mouse button is currently being held down, either by user input or suchibot. 463 | 464 | Definition: 465 | 466 | ```ts 467 | function isDown(button: MouseButton): boolean; 468 | ``` 469 | 470 | Example: 471 | 472 | ```js 473 | const isRightClickHeld = Mouse.isDown(MouseButton.RIGHT); 474 | console.log(isRightClickHeld); // true or false 475 | ``` 476 | 477 | #### Mouse.isUp 478 | 479 | Returns whether the specified mouse button is currently NOT being held down. Mouse buttons can be released either by user input or by suchibot. 480 | 481 | Definition: 482 | 483 | ```ts 484 | function isUp(button: MouseButton): boolean; 485 | ``` 486 | 487 | Example: 488 | 489 | ```js 490 | const isLeftClickReleased = Mouse.isUp(MouseButton.LEFT); 491 | console.log(isLeftClickReleased); // true or false 492 | ``` 493 | 494 | ### Keyboard 495 | 496 | Functions that let you control the keyboard and/or register code to be run when a keyboard event occurs. 497 | 498 | #### Keyboard.tap 499 | 500 | Presses and then releases the specified key. 501 | 502 | Definition: 503 | 504 | ```ts 505 | tap(key: Key): void; 506 | ``` 507 | 508 | Example: 509 | 510 | ```js 511 | // presses and then releases zero 512 | Keyboard.tap(Key.ZERO); 513 | ``` 514 | 515 | #### Keyboard.hold 516 | 517 | Presses down the specified key (and keeps holding it down until you release it, via `Keyboard.release`). 518 | 519 | Definition: 520 | 521 | ```ts 522 | hold(key: Key): void; 523 | ``` 524 | 525 | Example: 526 | 527 | ```js 528 | // presses down zero 529 | Keyboard.hold(Key.ZERO); 530 | ``` 531 | 532 | #### Keyboard.release 533 | 534 | Releases (stops holding down) the specified key (which maybe is being held down because you used `Keyboard.hold`). 535 | 536 | Definition: 537 | 538 | ```ts 539 | release(key: Key): void; 540 | ``` 541 | 542 | Example: 543 | 544 | ```js 545 | // stops holding down zero 546 | Keyboard.release(Key.ZERO); 547 | ``` 548 | 549 | #### Keyboard.type 550 | 551 | Types the specified string in, by tapping one key at a time, with a delay in-between each key. 552 | 553 | Optionally, specify how fast to type by passing the delay in milliseconds between key as the second argument. 554 | 555 | Definition: 556 | 557 | ```ts 558 | function type(textToType: string, delayBetweenKeyPresses: number = 10): void; 559 | ``` 560 | 561 | Example: 562 | 563 | ```js 564 | // type in a username and password: 565 | Keyboard.type("my-username"); 566 | Keyboard.tap(Keys.TAB); 567 | Keyboard.type("myP@ssw0rd!"); 568 | 569 | // type a message really slow: 570 | Keyboard.type("hihihihihihihi", 200); 571 | ``` 572 | 573 | #### Keyboard.onDown 574 | 575 | Registers a function to be called when a key on the keyboard starts being held down. 576 | 577 | Each key on the keyboard can either be "down" or "up". When you hold a key down, it transitions from "up" to "down". When you stop holding a key down, it transitions from "down" to "up". When you tap a key, it transitions from "up" to "down", then a moment later, it transitions from "down" to "up". 578 | 579 | `Keyboard.onDown` registers a function to be called whenever a key on the keyboard transitions from "up" to "down". 580 | 581 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the key is pressed down. 582 | 583 | Definition: 584 | 585 | ```ts 586 | function onDown( 587 | button: Key, 588 | eventHandler: (event: KeyboardEvent) => void 589 | ): Listener; 590 | ``` 591 | 592 | Example: 593 | 594 | ```js 595 | // log whenever "B" is pressed down: 596 | Keyboard.onDown(Key.B, () => { 597 | console.log("someone pressed B down!"); 598 | }); 599 | 600 | // log whenever any key is pressed down: 601 | Keyboard.onDown(Key.ANY, (event) => { 602 | console.log("someone pressed down:", event.key); 603 | }); 604 | ``` 605 | 606 | #### Keyboard.onUp 607 | 608 | Registers a function to be called when a key on the keyboard stops being held down. 609 | 610 | Each key on the keyboard can either be "down" or "up". When you hold a key down, it transitions from "up" to "down". When you stop holding a key down, it transitions from "down" to "up". When you tap a key, it transitions from "up" to "down", then a moment later, it transitions from "down" to "up". 611 | 612 | `Keyboard.onUp` registers a function to be called whenever a key on the keyboard transitions from "down" to "up". 613 | 614 | Returns a `Listener` object; call `.stop()` on the listener to unregister the function, so it's no longer called when the key is released. 615 | 616 | Definition: 617 | 618 | 619 | ```ts 620 | function onUp( 621 | button: Key, 622 | eventHandler: (event: KeyboardEvent) => void 623 | ): Listener; 624 | ``` 625 | 626 | Example: 627 | 628 | ```js 629 | // log whenever "B" is released: 630 | Keyboard.onUp(Key.B, () => { 631 | console.log("someone released B!"); 632 | }); 633 | 634 | // log whenever any key is released: 635 | Keyboard.onUp(Key.ANY, (event) => { 636 | console.log("someone released:", event.key); 637 | }); 638 | ``` 639 | 640 | #### Keyboard.isDown 641 | 642 | Returns whether the specified key is currently being pressed, either by user input or suchibot. 643 | 644 | Definition: 645 | 646 | ```ts 647 | function isDown(key: Key): boolean; 648 | ``` 649 | 650 | Example: 651 | 652 | ```js 653 | const isSpaceHeld = Keyboard.isDown(Key.SPACE); 654 | console.log(isSpaceHeld); // true or false 655 | ``` 656 | 657 | #### Keyboard.isUp 658 | 659 | Returns whether the specified key is currently NOT being pressed. Keys can be released either by user input or by suchibot. 660 | 661 | Definition: 662 | 663 | ```ts 664 | function isUp(key: Key): boolean; 665 | ``` 666 | 667 | Example: 668 | 669 | ```js 670 | const isEscapeReleased = Keyboard.isDown(Key.ESCAPE); 671 | console.log(isEscapeReleased); // true or false 672 | ``` 673 | 674 | ### Screen 675 | 676 | Functions that give you information about the screen. 677 | 678 | #### Screen.getSize 679 | 680 | Returns an object with `width` and `height` properties describing the screen resolution in pixels. 681 | 682 | Definition: 683 | 684 | ```ts 685 | function getSize(): { width: number; height: number }; 686 | ``` 687 | 688 | Example: 689 | 690 | ```js 691 | // get the screen resolution 692 | const { width, height } = Screen.getSize(); 693 | console.log(width, height); // 1920, 1080 694 | ``` 695 | 696 | ### Key 697 | 698 | This object has strings on it with keyboard key names. These strings get passed into keyboard-related functions, like `Keyboard.tap`. 699 | 700 | List of keys: 701 | 702 | - `BACKSPACE` 703 | - `DELETE` 704 | - `ENTER` 705 | - `TAB` 706 | - `ESCAPE` 707 | - `UP` 708 | - `DOWN` 709 | - `RIGHT` 710 | - `LEFT` 711 | - `HOME` 712 | - `INSERT` 713 | - `END` 714 | - `PAGE_UP` 715 | - `PAGE_DOWN` 716 | - `SPACE` 717 | - `F1` 718 | - `F2` 719 | - `F3` 720 | - `F4` 721 | - `F5` 722 | - `F6` 723 | - `F7` 724 | - `F8` 725 | - `F9` 726 | - `F10` 727 | - `F11` 728 | - `F12` 729 | - `F13` 730 | - `F14` 731 | - `F15` 732 | - `F16` 733 | - `F17` 734 | - `F18` 735 | - `F19` 736 | - `F20` 737 | - `F21` 738 | - `F22` 739 | - `F23` 740 | - `F24` 741 | - `LEFT_ALT` 742 | - `LEFT_CONTROL` 743 | - `LEFT_SHIFT` 744 | - `RIGHT_ALT` 745 | - `RIGHT_CONTROL` 746 | - `RIGHT_SHIFT` 747 | - `LEFT_SUPER` 748 | - `LEFT_WINDOWS` (alias of LEFT_SUPER) 749 | - `LEFT_COMMAND` (alias of LEFT_SUPER) 750 | - `LEFT_META` (alias of LEFT_SUPER) 751 | - `RIGHT_SUPER` 752 | - `RIGHT_WINDOWS` (alias of RIGHT_SUPER) 753 | - `RIGHT_COMMAND` (alias of RIGHT_SUPER) 754 | - `RIGHT_META` (alias of RIGHT_SUPER) 755 | - `PRINT_SCREEN` 756 | - `VOLUME_DOWN` 757 | - `VOLUME_UP` 758 | - `MUTE` 759 | - `PAUSE_BREAK` 760 | - `NUMPAD_0` 761 | - `NUMPAD_1` 762 | - `NUMPAD_2` 763 | - `NUMPAD_3` 764 | - `NUMPAD_4` 765 | - `NUMPAD_5` 766 | - `NUMPAD_6` 767 | - `NUMPAD_7` 768 | - `NUMPAD_8` 769 | - `NUMPAD_9` 770 | - `NUMPAD_MULTIPLY` 771 | - `NUMPAD_ADD` 772 | - `NUMPAD_SUBTRACT` 773 | - `NUMPAD_DECIMAL` 774 | - `NUMPAD_DIVIDE` 775 | - `NUMPAD_ENTER` 776 | - `CAPS_LOCK` 777 | - `NUM_LOCK` 778 | - `SCROLL_LOCK` 779 | - `SEMICOLON` 780 | - `EQUAL` 781 | - `COMMA` 782 | - `MINUS` 783 | - `PERIOD` 784 | - `SLASH` 785 | - `BACKTICK` (aka grave accent) 786 | - `LEFT_BRACKET` 787 | - `BACKSLASH` 788 | - `RIGHT_BRACKET` 789 | - `QUOTE` 790 | - `A` 791 | - `B` 792 | - `C` 793 | - `D` 794 | - `E` 795 | - `F` 796 | - `G` 797 | - `H` 798 | - `I` 799 | - `J` 800 | - `K` 801 | - `L` 802 | - `M` 803 | - `N` 804 | - `O` 805 | - `P` 806 | - `Q` 807 | - `R` 808 | - `S` 809 | - `T` 810 | - `U` 811 | - `V` 812 | - `W` 813 | - `X` 814 | - `Y` 815 | - `Z` 816 | - `ZERO` 817 | - `ONE` 818 | - `TWO` 819 | - `THREE` 820 | - `FOUR` 821 | - `FIVE` 822 | - `SIX` 823 | - `SEVEN` 824 | - `EIGHT` 825 | - `NINE` 826 | - `ANY` (this one can't be pressed; it's only used when registering event listeners) 827 | 828 | ### MouseButton 829 | 830 | This object has strings on it with the names of buttons on the mouse. These strings get passed into mouse-related functions, `Mouse.click`. 831 | 832 | List of mouse buttons: 833 | 834 | - `LEFT` 835 | - `RIGHT` 836 | - `MIDDLE` 837 | - `ANY` (this one can't be pressed; it's only used when registering event listeners) 838 | 839 | ### MouseEvent 840 | 841 | This object contains information about a mouse event that just occured. It gets passed into a function you pass in to `Mouse.onDown`, `Mouse.onUp`, `Mouse.onClick` or `Mouse.onMove`. 842 | 843 | Each `MouseEvent` has these properties on it: 844 | 845 | - `type`: A string indicating which kind of event this MouseEvent represents; could be either "click", "down", "up", or "move". 846 | - `button`: A string from `MouseButton` indicating which button this event pertains to. 847 | - `x`: A number indicating which horizontal location on the screen the event happened at. Small values represent the left side of the screen, and larger values represent the right side of the screen. 848 | - `y`: A number indicating which vertical location on the screen the event happened at. Small values represent the top of the screen, and larger values represent the bottom of the screen. 849 | 850 | ### KeyboardEvent 851 | 852 | This object contains information about a keyboard event that just occured. It gets passed into a function you pass in to `Keyboard.onDown` or `Keyboard.onUp`. 853 | 854 | Each `KeyboardEvent` has these properties on it: 855 | 856 | - `type`: A string indicating which kind of event this KeyboardEvent represents; could be either "down" or "up". 857 | - `key`: A string from `Key` indicating which keyboard key this event pertains to. 858 | 859 | ### isKeyboardEvent 860 | 861 | A function which returns whether its argument is a `KeyboardEvent` object. 862 | 863 | ### isMouseEvent 864 | 865 | A function which returns whether its argument is a `MouseEvent` object. 866 | 867 | ### startListening 868 | 869 | Call this function to start listening for input events, which makes functions starting with `on` work, like `Mouse.onClick`, `Keyboard.onUp`, etc. 870 | 871 | You only need to call this when using sucibot as a package in a node script. When you run your script with the suchibot CLI, `startListening` will be called automatically before loading your script. 872 | 873 | ### stopListening 874 | 875 | Call this function to stop listening for input events. This will make functions starting with `on` stop working, like `Mouse.onClick`, `Keyboard.onUp`, etc. If the node process isn't doing any other work (http server or something), this will also make the process exit. 876 | 877 | Generally, you don't need to call this, as you can stop your script at any time by pressing Ctrl+C in the terminal window where it's running. 878 | 879 | ### sleep.async 880 | 881 | This function returns a Promise that resolves in the specified number of milliseconds. You can use it to wait for an amount of time before running the next part of your code. 882 | 883 | This function works best when used with the `await` keyword. The `await` keyword can only be used inside a function that has been marked as asynchronous using the `async` keyword. 884 | 885 | #### Definition 886 | 887 | ```ts 888 | function async(milliseconds: number): Promise; 889 | ``` 890 | 891 | #### Example 892 | 893 | ```js 894 | // Note that you have to write `async` here 895 | Mouse.onClick(MouseButton.LEFT, async () => { 896 | Keyboard.tap(Keys.H); 897 | // Wait 100ms. Note that you have to write `await` here. 898 | await sleep.async(100); 899 | 900 | // After waiting 100ms, the code underneath the `await sleep.async` line will run. 901 | Keyboard.tap(Keys.I); 902 | }); 903 | 904 | // If you need to use sleep.async in a non-async function, you can use the `then` method on the Promise it returns: 905 | 906 | console.log("This prints now"); 907 | sleep.async(1000).then(() => { 908 | console.log("This prints 1 second later"); 909 | }); 910 | ``` 911 | 912 | ### sleep.sync 913 | 914 | This function pauses execution (blocking the main thread) for the specified number of milliseconds. 915 | 916 | Note that, if this function is used, no processing will occur until the specified amount of time has passed; notably, other mouse/keyboard events will not be processed. That means that those events will not be written to any `Tape`s that are recording. To avoid this issue, use `sleep.async` (instead of `sleep.sync`) inside of an `async` function. 917 | 918 | > tl;dr use `sleep.async` instead unless you understand what you are doing 919 | 920 | #### Definition 921 | 922 | ```ts 923 | function sync(milliseconds: number): void; 924 | ``` 925 | 926 | #### Example 927 | 928 | ```js 929 | // pauses everything for 100ms 930 | sleep.sync(100); 931 | 932 | // pause everything for 1 second 933 | sleep.sync(1000); 934 | 935 | // pause everything for 1 minute 936 | sleep.sync(60000); 937 | ``` 938 | 939 | ### Tape 940 | 941 | `Tape`s can record all mouse/keyboard inputs so that they can be played back later. Call `tape.record()` to start keeping track of inputs. After some time has passed, call the `stopRecording` function to stop recording inputs. Then, to replay those inputs, call the `play` function on the `Tape`. 942 | 943 | You can optionally pass an array of event filters into the `Tape` constructor; any events that match those filters will not be saved onto the recording. This is useful when you are using keyboard keys to start/stop/play recordings, and you don't want events for those keys to end up in the recording. 944 | 945 | #### Definition 946 | 947 | ```ts 948 | class Tape { 949 | static enum State { 950 | RECORDING, 951 | PLAYING, 952 | IDLE, 953 | }; 954 | 955 | readonly state: State; 956 | 957 | constructor( 958 | eventsToIgnore?: Array 959 | ): Tape; 960 | 961 | record(): void; 962 | stopRecording(): void; 963 | 964 | play(): Promise; // Promise resolves when playback completes 965 | stopPlaying(): void; // Used to stop playback before completion 966 | 967 | serialize(): Object; // Converts the Tape into a JSON-compatible format 968 | 969 | static deserialize(serialized: Object): Tape; // Loads a tape from the serialized format 970 | } 971 | ``` 972 | 973 | #### Example 974 | 975 | ```js 976 | // Make a tape: 977 | const tape = new Tape(); 978 | 979 | // Start the recording... 980 | tape.record(); 981 | 982 | // We'll take a 4-second recording by waiting 4000ms before calling `stop`. 983 | await sleep.async(4000); 984 | 985 | // Move the mouse around, press keys, etc. 986 | 987 | // Now, stop recording. 988 | tape.stopRecording(); 989 | 990 | // Now, replay the tape, and the mouse and keyboard will do the same things you did during the 4000ms wait. 991 | tape.play(); 992 | ``` 993 | 994 | And, another example demonstrating using event filters to ignore certain events: 995 | 996 | ```js 997 | import { keyboardEventFilter, Key, Tape } from "suchibot"; 998 | 999 | const eventsToIgnore = [ 1000 | keyboardEventFilter({ 1001 | key: Key.SCROLL_LOCK, 1002 | }), 1003 | ]; 1004 | 1005 | const tape = new Tape(eventsToIgnore); 1006 | 1007 | // Now, when you record with this tape, any keyboard events with the same key as the one you passed to your filter will not be added to the recording (in this case, Scroll Lock). 1008 | 1009 | // You can make event filters more specific by passing more information into them. To explain, consider the differences between the filters below: 1010 | 1011 | // Matches all keyboard events: 1012 | const allKeyboardEvents = keyboardEventFilter(); 1013 | 1014 | // Matches all "down" keyboard events: 1015 | const allKeyDownEvents = keyboardEventFilter({ type: "down" }); 1016 | 1017 | // Matches all "up" keyboard events for the Pause/Break key: 1018 | const allPauseBreakUpEvents = keyboardEventFilter({ 1019 | type: "up", 1020 | key: Key.PAUSE_BREAK, 1021 | }); 1022 | 1023 | // Mouse event filters work the same way, but for the properties on `MouseEvent`s: 1024 | 1025 | import { mouseEventFilter, MouseButton } from "suchibot"; 1026 | 1027 | const allMouseEvents = mouseEventFilter(); 1028 | const allMouseMoves = mouseEventFilter({ type: "move" }); 1029 | const allRightClicks = mouseEventFilter({ button: MouseButton.RIGHT }); 1030 | ``` 1031 | 1032 | ### Listener 1033 | 1034 | This object is returned from `Mouse.onDown`, `Mouse.onUp`, `Mouse.onClick`, `Mouse.onMove`, `Keyboard.onDown`, and `Keyboard.onUp`. It has a `stop` function on it that will make the event listener/handler function you passed in stop being called when the corresponding input event happens. See the example below for more information. 1035 | 1036 | #### Definition 1037 | 1038 | ```ts 1039 | type Listener = { 1040 | stop: () => void; 1041 | }; 1042 | ``` 1043 | 1044 | #### Example 1045 | 1046 | ```js 1047 | // Save the listener when calling an on* function: 1048 | const listener = Mouse.onMove((event) => { 1049 | console.log(`Mouse moved to: ${event.x}, ${event.y}`); 1050 | }); 1051 | 1052 | // Now, if you move the mouse around, you'll see the console log messages. 1053 | // To stop seeing them, you call `listener.stop`: 1054 | listener.stop(); 1055 | 1056 | // Now, you'll no longer see the console.log messages, because the function you 1057 | // passed in to Mouse.onMove isn't being called anymore. 1058 | ``` 1059 | 1060 | ### KeyboardEventFilter, MouseEventFilter, eventMatchesFilter, keyboardEventFilter, and mouseEventFilter 1061 | 1062 | These types and functions are used to work with event filters, which can be passed into the `Tape` constructor to specify events to omit from the tape's recording. 1063 | 1064 | `KeyboardEventFilter` and `MouseEventFilter` (with capitalized first letters) are the object types for filters. `keyboardEventFilter` and `mouseEventFilter` (with lowercase first letters) are builder functions for making filter objects corresponding to those types. `eventMatchesFilter` can be used to check if a `MouseEvent` or `KeyboardEvent` matches a filter. 1065 | 1066 | The properties on a `MouseEventFilter` match those on a `MouseEvent`, and, likewise, the properties on a `KeyboardEventFilter` match those on a `KeyboardEvent`. As such, to identify what properties to create your filter with, you can use console.log to look at the `MouseEvent` or `KeyboardEvent` that you want to filter out (by performing the action and setting up a listener for it, with `Mouse.onDown`, `Keyboard.onUp`, etc). 1067 | 1068 | #### Definition 1069 | 1070 | ```ts 1071 | type MouseEventFilter = { 1072 | filterType: "Mouse"; 1073 | type?: "click" | "down" | "up" | "move"; 1074 | button?: MouseButton; 1075 | x?: number; 1076 | y?: number; 1077 | }; 1078 | 1079 | type KeyboardEventFilter = { 1080 | filterType: "Keyboard"; 1081 | type?: "down" | "up"; 1082 | key?: Key; 1083 | }; 1084 | 1085 | function keyboardEventFilter(properties?: { 1086 | type?: "down" | "up"; 1087 | key?: Key; 1088 | }): KeyboardEventFilter; 1089 | 1090 | function mouseEventFilter(properties?: { 1091 | type?: "click" | "down" | "up" | "move"; 1092 | button?: MouseButton; 1093 | x?: number; 1094 | y?: number; 1095 | }): MouseEventFilter; 1096 | 1097 | function eventMatchesFilter( 1098 | event: MouseEvent | KeyboardEvent, 1099 | filter: MouseEventFilter | KeyboardEventFilter 1100 | ): boolean; 1101 | ``` 1102 | 1103 | #### Example 1104 | 1105 | ```js 1106 | import { keyboardEventFilter, Key, Tape } from "suchibot"; 1107 | 1108 | const eventsToIgnore = [ 1109 | keyboardEventFilter({ 1110 | key: Key.SCROLL_LOCK, 1111 | }), 1112 | ]; 1113 | 1114 | const tape = new Tape(eventsToIgnore); 1115 | 1116 | // Now, when you record with this tape, any keyboard events with the same key as the one you passed to your filter will not be added to the recording (in this case, Scroll Lock). 1117 | 1118 | // You can make event filters more specific by passing more information into them. To explain, consider the differences between the filters below: 1119 | 1120 | // Matches all keyboard events: 1121 | const allKeyboardEvents = keyboardEventFilter(); 1122 | 1123 | // Matches all "down" keyboard events: 1124 | const allKeyDownEvents = keyboardEventFilter({ type: "down" }); 1125 | 1126 | // Matches all "up" keyboard events for the Pause/Break key: 1127 | const allPauseBreakUpEvents = keyboardEventFilter({ 1128 | type: "up", 1129 | key: Key.PAUSE_BREAK, 1130 | }); 1131 | 1132 | // Mouse event filters work the same way, but for the properties on `MouseEvent`s: 1133 | 1134 | import { mouseEventFilter, MouseButton } from "suchibot"; 1135 | 1136 | const allMouseEvents = mouseEventFilter(); 1137 | const allMouseMoves = mouseEventFilter({ type: "move" }); 1138 | const allRightClicks = mouseEventFilter({ button: MouseButton.RIGHT }); 1139 | ``` 1140 | 1141 | ## License 1142 | 1143 | MIT 1144 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] add numpad keys (windows reports keycodes up near 60000) 2 | - [ ] get working in node 20 and 22 on windows 3 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # suchibot examples 2 | 3 | In this folder, you'll find some example scripts that give you an idea of the sorts of things you can do with suchibot. 4 | 5 | There's also some more scripts in my personal suchibot scripts repo: 6 | -------------------------------------------------------------------------------- /examples/anti-afk.js: -------------------------------------------------------------------------------- 1 | import { Keyboard, Key, sleep } from ".."; 2 | 3 | // This script presses "A" or "D" every 5 minutes, 4 | // To prevent your character from getting kicked from a game for being AFK. 5 | 6 | // DELAY is in milliseconds, and there's 60 seconds in a minute, and we want a delay of 5 minutes 7 | const DELAY = 5 * 60 * 1000; 8 | 9 | while (true) { 10 | Keyboard.tap(Key.A); 11 | sleep.sync(DELAY); 12 | Keyboard.tap(Key.D); 13 | sleep.sync(DELAY); 14 | } 15 | -------------------------------------------------------------------------------- /examples/record-and-replay.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard, Key, Tape } from ".."; 2 | 3 | let tape: Tape | null = null; 4 | 5 | Keyboard.onUp(Key.SCROLL_LOCK, () => { 6 | if (tape && tape.state === Tape.State.RECORDING) { 7 | tape.stopRecording(); 8 | console.log("Stopped recording"); 9 | } else { 10 | if (tape) tape.stopPlaying(); 11 | tape = new Tape([ 12 | { 13 | filterType: "Keyboard", 14 | key: Key.SCROLL_LOCK, 15 | }, 16 | { 17 | filterType: "Keyboard", 18 | key: Key.PAUSE_BREAK, 19 | }, 20 | ]); 21 | console.log("Recording to tape..."); 22 | } 23 | }); 24 | 25 | let shouldLoop = false; 26 | Keyboard.onUp(Key.PAUSE_BREAK, async () => { 27 | shouldLoop = !shouldLoop; 28 | console.log("shouldLoop:", shouldLoop); 29 | }); 30 | 31 | Keyboard.onUp(Key.PAUSE_BREAK, async () => { 32 | if (!tape) return; 33 | 34 | if (tape.state === Tape.State.PLAYING) { 35 | await tape.stopPlaying(); 36 | } 37 | if (tape.state === Tape.State.RECORDING) { 38 | await tape.stopRecording(); 39 | } 40 | 41 | console.log("Playing tape..."); 42 | await tape.play(); 43 | console.log("Tape playback finished"); 44 | 45 | while (shouldLoop) { 46 | console.log("Replaying tape..."); 47 | await tape.play(); 48 | console.log("Tape playback finished"); 49 | } 50 | }); 51 | 52 | console.log("Recording system ready. Controls:"); 53 | console.log(" Scroll Lock: Start/stop recording"); 54 | console.log(" Pause/break: Toggle replaying recording"); 55 | -------------------------------------------------------------------------------- /examples/toggle-holding-left-click.js: -------------------------------------------------------------------------------- 1 | import { Keyboard, Key, Mouse, MouseButton } from "suchibot"; 2 | 3 | // This script toggles whether the left mouse button is being held whenever you press scroll lock. 4 | // I use it to hold down left-click in Minecraft so I can keep mining cobblestone from the cobblestone generator :3 5 | 6 | let shouldHold = false; 7 | 8 | Keyboard.onUp(Key.SCROLL_LOCK, () => { 9 | shouldHold = !shouldHold; 10 | 11 | if (shouldHold) { 12 | Mouse.hold(MouseButton.LEFT); 13 | } else { 14 | Mouse.release(MouseButton.LEFT); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /examples/toggle-holding-w.js: -------------------------------------------------------------------------------- 1 | import { Keyboard, Key } from "suchibot"; 2 | 3 | // This script toggles whether the W key is being held whenever you press pause/break. 4 | // I use it to toggle moving forward while in a boat in minecraft. 5 | 6 | let shouldHold = false; 7 | 8 | Keyboard.onUp(Key.PAUSE_BREAK, () => { 9 | shouldHold = !shouldHold; 10 | 11 | if (shouldHold) { 12 | Keyboard.hold(Key.W); 13 | } else { 14 | Keyboard.release(Key.W); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist"; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const suchibot = require("./dist/index"); 3 | module.exports = suchibot; 4 | 5 | if (module === require.main) { 6 | // sets up esbuild-register, and forces require.resolve to always resolve suchibot to the current file 7 | const { patchCjs } = require("./dist/patch-cjs"); 8 | patchCjs(); 9 | 10 | const { main } = require("./dist/cli"); 11 | Object.assign(globalThis, suchibot); 12 | main(suchibot); 13 | } 14 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import * as fs from "node:fs"; 3 | import * as path from "node:path"; 4 | 5 | // intentionally a require instead of an import so that vitest doesn't do ESM 6 | // interop transformations on it (which adds otherwise-nonexistent 'default' 7 | // export) 8 | const suchibot = require("."); 9 | 10 | test("module exports", () => { 11 | const exportNames = Object.keys(suchibot).filter((key) => suchibot[key]); 12 | 13 | expect(exportNames).toMatchInlineSnapshot(` 14 | [ 15 | "Mouse", 16 | "Keyboard", 17 | "MouseButton", 18 | "Key", 19 | "MouseEvent", 20 | "KeyboardEvent", 21 | "isKeyboardEvent", 22 | "isMouseEvent", 23 | "sleep", 24 | "sleepSync", 25 | "Screen", 26 | "startListening", 27 | "stopListening", 28 | "eventMatchesFilter", 29 | "keyboardEventFilter", 30 | "mouseEventFilter", 31 | "Tape", 32 | ] 33 | `); 34 | }); 35 | 36 | const readmeText = fs.readFileSync(path.join(__dirname, "README.md"), "utf-8"); 37 | 38 | test("readme specifies the correct number of exports", () => { 39 | const exportNames = Object.keys(suchibot).filter((key) => suchibot[key]); 40 | 41 | const phraseRegExp = /The "suchibot" module has (\d+) named exports/; 42 | const phraseMatches = readmeText.match(phraseRegExp); 43 | if (phraseMatches == null) { 44 | throw new Error( 45 | `Expected readme content to match RegExp ${phraseRegExp}, but it did not` 46 | ); 47 | } 48 | 49 | const numExports = exportNames.length; 50 | const numExportsInReadme = phraseMatches[1]; 51 | 52 | expect(numExports).toEqual(parseInt(numExportsInReadme)); 53 | }); 54 | 55 | test("readme mentions all the public methods of all the classes", () => { 56 | const classes = ["Mouse", "Keyboard", "Screen"]; 57 | for (const className of classes) { 58 | const klass = suchibot[className]; 59 | 60 | const staticMethodNames: Array = []; 61 | for (const name in klass) { 62 | const value = klass[name]; 63 | 64 | if (value === Object.prototype[name]) continue; 65 | 66 | if (typeof value === "function" && !name.startsWith("_")) { 67 | staticMethodNames.push(name); 68 | } 69 | } 70 | 71 | const instanceMethodNames: Array = []; 72 | for (const name in klass.prototype) { 73 | const value = klass.prototype[name]; 74 | 75 | if (value === Object.prototype[name]) continue; 76 | 77 | if (typeof value === "function" && !name.startsWith("_")) { 78 | instanceMethodNames.push(name); 79 | } 80 | } 81 | 82 | for (const name of [...staticMethodNames, ...instanceMethodNames]) { 83 | expect(readmeText).toMatch(new RegExp(`${className}.*${name}`, "m")); 84 | } 85 | } 86 | }); 87 | 88 | test("readme mentions all the module exports", () => { 89 | const exportNames = Object.keys(suchibot) 90 | .filter((key) => suchibot[key]) 91 | .filter((name) => { 92 | if (name === "sleepSync") { 93 | // ignore, because it's deprecated in favor of sleep.sync 94 | return false; 95 | } else { 96 | return true; 97 | } 98 | }); 99 | 100 | for (const name of exportNames) { 101 | expect(readmeText).toContain(name); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suchibot", 3 | "version": "1.9.2", 4 | "description": "A cross-platform AutoHotKey-like thing with TypeScript as its scripting language", 5 | "main": "index.js", 6 | "bin": "index.js", 7 | "scripts": { 8 | "build": "rimraf dist && tsc && cp src/libnut.js dist/", 9 | "start": "node ./index.js", 10 | "test": "vitest" 11 | }, 12 | "keywords": [ 13 | "macro", 14 | "automatic", 15 | "autohotkey", 16 | "keyboard", 17 | "mouse", 18 | "hotkey", 19 | "hot", 20 | "auto", 21 | "automated" 22 | ], 23 | "author": "Lily Skye ", 24 | "repository": "suchipi/suchibot", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@suchipi/defer": "^1.0.0", 28 | "a-mimir": "^1.0.1", 29 | "debug": "^4.3.5", 30 | "esbuild-register": "^3.5.0", 31 | "kleur": "^4.1.5", 32 | "mitt": "^3.0.1", 33 | "pretty-print-error": "^1.1.2", 34 | "uiohook-napi": "^1.5.4" 35 | }, 36 | "optionalDependencies": { 37 | "@suchipi/libnut-darwin": "^2.7.1", 38 | "@suchipi/libnut-linux": "^2.7.1", 39 | "@suchipi/libnut-win32": "^2.7.1", 40 | "@suchipi/node-mac-permissions": "2.2.1" 41 | }, 42 | "devDependencies": { 43 | "@types/debug": "^4.1.12", 44 | "@types/node": "^20.14.2", 45 | "rimraf": "^5.0.7", 46 | "typescript": "^5.4.5", 47 | "vitest": "^1.6.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /script.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env suchibot 2 | import { Keyboard, Key, Mouse, MouseButton, stopListening } from "."; 3 | 4 | function something(): string { 5 | return "hi"; 6 | } 7 | 8 | console.log("hi mom"); 9 | 10 | setTimeout(() => { 11 | Keyboard.type("hi there"); 12 | }, 100); 13 | 14 | // setTimeout(() => { 15 | // console.log("stopListening"); 16 | // stopListening(); 17 | // }, 2000); 18 | 19 | Keyboard.onDown(Key.A, (event) => { 20 | console.log("got keydown a 1"); 21 | 22 | // if (Keyboard.isDown(Key.LEFT_SHIFT)) { 23 | // console.log("left shift a"); 24 | // } 25 | // if (Keyboard.isDown(Key.RIGHT_SHIFT)) { 26 | // console.log("right shift a"); 27 | // } 28 | // if (Keyboard.isDown(Key.LEFT_SHIFT) || Keyboard.isDown(Key.RIGHT_SHIFT)) { 29 | // console.log("(either left or right) shift a"); 30 | // } 31 | 32 | // console.log("left mb down:", Mouse.isDown(MouseButton.LEFT)); 33 | }); 34 | 35 | // easier way to check modifier keys since they're commonly used: 36 | Keyboard.onDown(Key.A, (event) => { 37 | console.log("got keydown a 2"); 38 | 39 | // if (event.modifierKeys.leftShift) { 40 | // console.log("left shift a"); 41 | // } 42 | // if (event.modifierKeys.rightShift) { 43 | // console.log("right shift a"); 44 | // } 45 | // if (event.modifierKeys.shift) { 46 | // console.log("(either left or right) shift a"); 47 | // } 48 | 49 | // console.log("left mb down:", event.mouseButtons.left); 50 | }); 51 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import kleur from "kleur"; 4 | import { formatError } from "pretty-print-error"; 5 | 6 | // @ts-ignore 7 | import pkgJson from "../package.json"; 8 | 9 | function printUsage() { 10 | console.log(`suchibot ${pkgJson.version}`); 11 | console.log(""); 12 | console.log("Usage:"); 13 | console.log(" suchibot [path to script file]"); 14 | console.log(""); 15 | console.log( 16 | "If no script file path is specified, the program will attempt to load script.ts or script.js (whichever exists)." 17 | ); 18 | console.log(""); 19 | console.log("Examples:"); 20 | console.log(" suchibot ./my-script.js"); 21 | } 22 | 23 | function main(suchibot: typeof import("./index")) { 24 | if (process.argv.includes("-h") || process.argv.includes("--help")) { 25 | printUsage(); 26 | process.exit(0); 27 | } 28 | 29 | if (process.argv.includes("-v") || process.argv.includes("--version")) { 30 | console.log(`${pkgJson.version}`); 31 | process.exit(0); 32 | } 33 | 34 | let modulePath = process.argv[2]; 35 | 36 | const defaultTsPath = path.resolve(process.cwd(), "./script.ts"); 37 | const defaultJsPath = path.resolve(process.cwd(), "./script.js"); 38 | 39 | if (modulePath == null) { 40 | if (fs.existsSync(defaultTsPath)) { 41 | modulePath = defaultTsPath; 42 | } else if (fs.existsSync(defaultJsPath)) { 43 | modulePath = defaultJsPath; 44 | } else { 45 | console.log( 46 | kleur.red( 47 | `Not sure which file to run. Please either specify a script as the first argument to suchibot, or create a file named ${kleur.yellow( 48 | "script.ts" 49 | )} or ${kleur.yellow("script.js")} in the current folder.` 50 | ) 51 | ); 52 | console.log(""); 53 | printUsage(); 54 | process.exit(1); 55 | } 56 | } 57 | 58 | if (!path.isAbsolute(modulePath)) { 59 | modulePath = path.resolve(process.cwd(), modulePath); 60 | } 61 | 62 | if (!fs.existsSync(modulePath)) { 63 | console.log("Trying to load non-existent file:", modulePath); 64 | console.log(""); 65 | printUsage(); 66 | process.exit(1); 67 | } 68 | 69 | suchibot.startListening(); 70 | console.log( 71 | kleur.green( 72 | "Now listening for mouse/keyboard events. Press Ctrl+C to exit at any time." 73 | ) 74 | ); 75 | 76 | process.on("unhandledRejection", (error: any) => { 77 | console.log(kleur.red("An unhandled Promise rejection occurred:")); 78 | console.log(formatError(error)); 79 | }); 80 | 81 | try { 82 | require(modulePath); 83 | } catch (err: any) { 84 | console.log(kleur.red("An error occurred in your script:")); 85 | console.log(formatError(err)); 86 | 87 | suchibot.stopListening(); 88 | process.exit(1); 89 | } 90 | } 91 | 92 | export { main }; 93 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as input from "./input"; 2 | import * as output from "./output"; 3 | 4 | import { Key, MouseButton } from "./types"; 5 | import { 6 | MouseEvent, 7 | KeyboardEvent, 8 | KeyboardModifierKeysState, 9 | MouseButtonsState, 10 | isKeyboardEvent, 11 | isMouseEvent, 12 | startListening, 13 | stopListening, 14 | } from "./input"; 15 | import { 16 | Tape, 17 | SerializedTape, 18 | KeyboardEventFilter, 19 | MouseEventFilter, 20 | eventMatchesFilter, 21 | keyboardEventFilter, 22 | mouseEventFilter, 23 | } from "./recording"; 24 | import { Screen } from "./screen"; 25 | 26 | const Mouse = { 27 | ...output.Mouse, 28 | ...input.Mouse, 29 | }; 30 | 31 | const Keyboard = { 32 | ...output.Keyboard, 33 | ...input.Keyboard, 34 | }; 35 | 36 | import { sleep as mimir } from "a-mimir"; 37 | 38 | // This is callable for backwards-compatibility. sleep.async or sleep.sync is preferred over calling this directly 39 | const sleep = Object.assign( 40 | (milliseconds: number) => mimir.async(milliseconds), 41 | mimir 42 | ); 43 | 44 | // sleep.sync is preferred, but this is here for backwards-compatibility 45 | const sleepSync = mimir.sync; 46 | 47 | // prettier-ignore 48 | export { 49 | Mouse, 50 | Keyboard, 51 | 52 | MouseButton, 53 | Key, 54 | 55 | MouseEvent, 56 | KeyboardEvent, 57 | KeyboardModifierKeysState, 58 | MouseButtonsState, 59 | isKeyboardEvent, 60 | isMouseEvent, 61 | 62 | sleep, 63 | sleepSync, 64 | 65 | Screen, 66 | 67 | startListening, 68 | stopListening, 69 | 70 | KeyboardEventFilter, 71 | MouseEventFilter, 72 | eventMatchesFilter, 73 | keyboardEventFilter, 74 | mouseEventFilter, 75 | 76 | Tape, 77 | SerializedTape, 78 | }; 79 | -------------------------------------------------------------------------------- /src/input/held-keys.ts: -------------------------------------------------------------------------------- 1 | import { Key } from "../types"; 2 | import makeDebug from "debug"; 3 | 4 | const debug = makeDebug("suchibot:input/held-keys"); 5 | 6 | const heldKeys = new Map(); 7 | 8 | export function setKeyState(key: Key, upOrDown: "up" | "down") { 9 | if (key === Key.ANY) { 10 | return; 11 | } 12 | debug("setting held key state: %s -> %s", key, upOrDown); 13 | heldKeys.set(key, upOrDown === "down"); 14 | } 15 | 16 | export function isKeyDown(key: Key): boolean { 17 | if (key === Key.ANY) { 18 | for (const [_key, isDown] of heldKeys) { 19 | if (isDown) { 20 | debug("isKeyDown(ANY) -> true"); 21 | return true; 22 | } 23 | } 24 | debug("isKeyDown(ANY) -> false"); 25 | return false; 26 | } else { 27 | const result = Boolean(heldKeys.get(key)); 28 | debug("isKeyDown(%s) -> %o", key, result); 29 | return result; 30 | } 31 | } 32 | 33 | export function isKeyUp(key: Key): boolean { 34 | if (key === Key.ANY) { 35 | for (const [_key, isDown] of heldKeys) { 36 | if (!isDown) { 37 | debug("isKeyUp(ANY) -> true"); 38 | return true; 39 | } 40 | } 41 | debug("isKeyUp(ANY) -> false"); 42 | return false; 43 | } else { 44 | const result = !heldKeys.get(key); 45 | debug("isKeyUp(%s) -> %o", key, result); 46 | return result; 47 | } 48 | } 49 | 50 | export type KeyboardModifierKeysState = { 51 | alt: boolean; 52 | control: boolean; 53 | shift: boolean; 54 | super: boolean; 55 | windows: boolean; 56 | command: boolean; 57 | meta: boolean; 58 | 59 | leftAlt: boolean; 60 | leftControl: boolean; 61 | leftShift: boolean; 62 | leftSuper: boolean; 63 | leftWindows: boolean; 64 | leftCommand: boolean; 65 | leftMeta: boolean; 66 | 67 | rightAlt: boolean; 68 | rightControl: boolean; 69 | rightShift: boolean; 70 | rightSuper: boolean; 71 | rightWindows: boolean; 72 | rightCommand: boolean; 73 | rightMeta: boolean; 74 | }; 75 | 76 | export function getModifierKeysState(): KeyboardModifierKeysState { 77 | const leftAlt = Boolean(heldKeys.get(Key.LEFT_ALT)); 78 | const leftControl = Boolean(heldKeys.get(Key.LEFT_CONTROL)); 79 | const leftShift = Boolean(heldKeys.get(Key.LEFT_SHIFT)); 80 | const leftSuper = Boolean(heldKeys.get(Key.LEFT_SUPER)); 81 | 82 | const rightAlt = Boolean(heldKeys.get(Key.RIGHT_ALT)); 83 | const rightControl = Boolean(heldKeys.get(Key.RIGHT_CONTROL)); 84 | const rightShift = Boolean(heldKeys.get(Key.RIGHT_SHIFT)); 85 | const rightSuper = Boolean(heldKeys.get(Key.RIGHT_SUPER)); 86 | 87 | const alt = leftAlt || rightAlt; 88 | const control = leftControl || rightControl; 89 | const shift = leftShift || rightShift; 90 | const super_ = leftSuper || rightSuper; 91 | 92 | const result = { 93 | alt, 94 | control, 95 | shift, 96 | super: super_, 97 | windows: super_, 98 | command: super_, 99 | meta: super_, 100 | 101 | leftAlt, 102 | leftControl, 103 | leftShift, 104 | leftSuper, 105 | leftWindows: leftSuper, 106 | leftCommand: leftSuper, 107 | leftMeta: leftSuper, 108 | 109 | rightAlt, 110 | rightControl, 111 | rightShift, 112 | rightSuper, 113 | rightWindows: rightSuper, 114 | rightCommand: rightSuper, 115 | rightMeta: rightSuper, 116 | }; 117 | 118 | debug("getModifierKeysState() -> %o", result); 119 | return result; 120 | } 121 | -------------------------------------------------------------------------------- /src/input/held-mouse-buttons.ts: -------------------------------------------------------------------------------- 1 | import { MouseButton } from "../types"; 2 | import makeDebug from "debug"; 3 | 4 | const debug = makeDebug("suchibot:input/held-mouse-buttons"); 5 | 6 | const heldButtons = new Map(); 7 | 8 | export function setButtonState(button: MouseButton, upOrDown: "up" | "down") { 9 | if (button === MouseButton.ANY) { 10 | return; 11 | } 12 | debug("setting held button state: %s -> %s", button, upOrDown); 13 | heldButtons.set(button, upOrDown === "down"); 14 | } 15 | 16 | export function isButtonDown(button: MouseButton): boolean { 17 | if (button === MouseButton.ANY) { 18 | for (const [_key, isDown] of heldButtons) { 19 | if (isDown) { 20 | debug("isButtonDown(ANY) -> true"); 21 | return true; 22 | } 23 | } 24 | debug("isButtonDown(ANY) -> false"); 25 | return false; 26 | } else { 27 | const result = Boolean(heldButtons.get(button)); 28 | debug("isButtonDown(%s) -> %o", button, result); 29 | return result; 30 | } 31 | } 32 | 33 | export function isButtonUp(button: MouseButton): boolean { 34 | if (button === MouseButton.ANY) { 35 | for (const [_key, isDown] of heldButtons) { 36 | if (!isDown) { 37 | debug("isButtonUp(ANY) -> true"); 38 | return true; 39 | } 40 | } 41 | debug("isButtonUp(ANY) -> false"); 42 | return false; 43 | } else { 44 | const result = !heldButtons.get(button); 45 | debug("isButtonUp(%s) -> %o", button, result); 46 | return result; 47 | } 48 | } 49 | 50 | export type MouseButtonsState = { 51 | left: boolean; 52 | right: boolean; 53 | middle: boolean; 54 | mouse4: boolean; 55 | mouse5: boolean; 56 | }; 57 | 58 | export function getMouseButtonsState(): MouseButtonsState { 59 | const left = Boolean(heldButtons.get(MouseButton.LEFT)); 60 | const right = Boolean(heldButtons.get(MouseButton.RIGHT)); 61 | const middle = Boolean(heldButtons.get(MouseButton.MIDDLE)); 62 | const mouse4 = Boolean(heldButtons.get(MouseButton.MOUSE4)); 63 | const mouse5 = Boolean(heldButtons.get(MouseButton.MOUSE5)); 64 | 65 | const result = { 66 | left, 67 | right, 68 | middle, 69 | mouse4, 70 | mouse5, 71 | }; 72 | debug("getMouseButtonsState() -> %o", result); 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /src/input/index.ts: -------------------------------------------------------------------------------- 1 | import { uIOhook } from "uiohook-napi"; 2 | import { Mouse, MouseEvent, isMouseEvent } from "./mouse"; 3 | import { MouseButtonsState } from "./held-mouse-buttons"; 4 | import { Keyboard, KeyboardEvent, isKeyboardEvent } from "./keyboard"; 5 | import { KeyboardModifierKeysState } from "./held-keys"; 6 | import makeDebug from "debug"; 7 | 8 | const debug = makeDebug("suchibot:input/index"); 9 | 10 | export { 11 | Mouse, 12 | MouseEvent, 13 | MouseButtonsState, 14 | isMouseEvent, 15 | Keyboard, 16 | KeyboardEvent, 17 | KeyboardModifierKeysState, 18 | isKeyboardEvent, 19 | }; 20 | 21 | export function startListening() { 22 | debug("startListening()"); 23 | uIOhook.start(); 24 | } 25 | 26 | export function stopListening() { 27 | debug("stopListening()"); 28 | uIOhook.stop(); 29 | } 30 | -------------------------------------------------------------------------------- /src/input/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { Key, Listener } from "../types"; 2 | import { uIOhook, UiohookKey } from "uiohook-napi"; 3 | import mitt, { Emitter } from "mitt"; 4 | import { 5 | setKeyState, 6 | isKeyDown, 7 | isKeyUp, 8 | KeyboardModifierKeysState, 9 | getModifierKeysState, 10 | } from "./held-keys"; 11 | import { MouseButtonsState, getMouseButtonsState } from "./held-mouse-buttons"; 12 | import makeDebug from "debug"; 13 | 14 | const debug = makeDebug("suchibot:input/keyboard"); 15 | const eventsDebug = makeDebug("suchibot:input/keyboard/events"); 16 | const uIOhookEventsDebug = makeDebug("suchibot:input/keyboard/events/uiohook"); 17 | 18 | // ------------ keyboard stuff ------------- 19 | const IS_KEYBOARD_EVENT = Symbol("IS_KEYBOARD_EVENT"); 20 | 21 | export function isKeyboardEvent(event: any): event is KeyboardEvent { 22 | return typeof event === "object" && event != null && event[IS_KEYBOARD_EVENT]; 23 | } 24 | 25 | export class KeyboardEvent { 26 | type: "down" | "up"; 27 | key: Key; 28 | modifierKeys: KeyboardModifierKeysState; 29 | mouseButtons: MouseButtonsState; 30 | [IS_KEYBOARD_EVENT]: true; 31 | 32 | constructor( 33 | type: "down" | "up", 34 | key: Key, 35 | modifierKeys: KeyboardModifierKeysState = getModifierKeysState(), 36 | mouseButtons: MouseButtonsState = getMouseButtonsState() 37 | ) { 38 | this.type = type; 39 | this.key = key; 40 | this.modifierKeys = modifierKeys; 41 | this.mouseButtons = mouseButtons; 42 | this[IS_KEYBOARD_EVENT] = true; 43 | } 44 | } 45 | 46 | const uioToKeyMap = { 47 | [UiohookKey.Tab]: Key.TAB, 48 | [UiohookKey.Enter]: Key.ENTER, 49 | [UiohookKey.CapsLock]: Key.CAPS_LOCK, 50 | [UiohookKey.Escape]: Key.ESCAPE, 51 | [UiohookKey.Space]: Key.SPACE, 52 | [UiohookKey.PageUp]: Key.PAGE_UP, 53 | [UiohookKey.PageDown]: Key.PAGE_DOWN, 54 | [UiohookKey.End]: Key.END, 55 | [UiohookKey.Home]: Key.HOME, 56 | [UiohookKey.ArrowLeft]: Key.LEFT, 57 | [UiohookKey.ArrowUp]: Key.UP, 58 | [UiohookKey.ArrowRight]: Key.RIGHT, 59 | [UiohookKey.ArrowDown]: Key.DOWN, 60 | [UiohookKey.Insert]: Key.INSERT, 61 | [UiohookKey.Delete]: Key.DELETE, 62 | [UiohookKey[0]]: Key.ZERO, 63 | [UiohookKey[1]]: Key.ONE, 64 | [UiohookKey[2]]: Key.TWO, 65 | [UiohookKey[3]]: Key.THREE, 66 | [UiohookKey[4]]: Key.FOUR, 67 | [UiohookKey[5]]: Key.FIVE, 68 | [UiohookKey[6]]: Key.SIX, 69 | [UiohookKey[7]]: Key.SEVEN, 70 | [UiohookKey[8]]: Key.EIGHT, 71 | [UiohookKey[9]]: Key.NINE, 72 | [UiohookKey.A]: Key.A, 73 | [UiohookKey.B]: Key.B, 74 | [UiohookKey.C]: Key.C, 75 | [UiohookKey.D]: Key.D, 76 | [UiohookKey.E]: Key.E, 77 | [UiohookKey.F]: Key.F, 78 | [UiohookKey.G]: Key.G, 79 | [UiohookKey.H]: Key.H, 80 | [UiohookKey.I]: Key.I, 81 | [UiohookKey.J]: Key.J, 82 | [UiohookKey.K]: Key.K, 83 | [UiohookKey.L]: Key.L, 84 | [UiohookKey.M]: Key.M, 85 | [UiohookKey.N]: Key.N, 86 | [UiohookKey.O]: Key.O, 87 | [UiohookKey.P]: Key.P, 88 | [UiohookKey.Q]: Key.Q, 89 | [UiohookKey.R]: Key.R, 90 | [UiohookKey.S]: Key.S, 91 | [UiohookKey.T]: Key.T, 92 | [UiohookKey.U]: Key.U, 93 | [UiohookKey.V]: Key.V, 94 | [UiohookKey.W]: Key.W, 95 | [UiohookKey.X]: Key.X, 96 | [UiohookKey.Y]: Key.Y, 97 | [UiohookKey.Z]: Key.Z, 98 | [UiohookKey.Numpad0]: Key.NUMPAD_0, 99 | [UiohookKey.Numpad1]: Key.NUMPAD_1, 100 | [UiohookKey.Numpad2]: Key.NUMPAD_2, 101 | [UiohookKey.Numpad3]: Key.NUMPAD_3, 102 | [UiohookKey.Numpad4]: Key.NUMPAD_4, 103 | [UiohookKey.Numpad5]: Key.NUMPAD_5, 104 | [UiohookKey.Numpad6]: Key.NUMPAD_6, 105 | [UiohookKey.Numpad7]: Key.NUMPAD_7, 106 | [UiohookKey.Numpad8]: Key.NUMPAD_8, 107 | [UiohookKey.Numpad9]: Key.NUMPAD_9, 108 | [UiohookKey.NumpadMultiply]: Key.NUMPAD_MULTIPLY, 109 | [UiohookKey.NumpadAdd]: Key.NUMPAD_ADD, 110 | [UiohookKey.NumpadSubtract]: Key.NUMPAD_SUBTRACT, 111 | [UiohookKey.NumpadDecimal]: Key.NUMPAD_DECIMAL, 112 | 3612: Key.NUMPAD_ENTER, 113 | [UiohookKey.NumpadDivide]: Key.NUMPAD_DIVIDE, 114 | [UiohookKey.F1]: Key.F1, 115 | [UiohookKey.F2]: Key.F2, 116 | [UiohookKey.F3]: Key.F3, 117 | [UiohookKey.F4]: Key.F4, 118 | [UiohookKey.F5]: Key.F5, 119 | [UiohookKey.F6]: Key.F6, 120 | [UiohookKey.F7]: Key.F7, 121 | [UiohookKey.F8]: Key.F8, 122 | [UiohookKey.F9]: Key.F9, 123 | [UiohookKey.F10]: Key.F10, 124 | [UiohookKey.F11]: Key.F11, 125 | [UiohookKey.F12]: Key.F12, 126 | [UiohookKey.F13]: Key.F13, 127 | [UiohookKey.F14]: Key.F14, 128 | [UiohookKey.F15]: Key.F15, 129 | [UiohookKey.F16]: Key.F16, 130 | [UiohookKey.F17]: Key.F17, 131 | [UiohookKey.F18]: Key.F18, 132 | [UiohookKey.F19]: Key.F19, 133 | [UiohookKey.F20]: Key.F20, 134 | [UiohookKey.F21]: Key.F21, 135 | [UiohookKey.F22]: Key.F22, 136 | [UiohookKey.F23]: Key.F23, 137 | [UiohookKey.F24]: Key.F24, 138 | [UiohookKey.Semicolon]: Key.SEMICOLON, 139 | [UiohookKey.Equal]: Key.EQUAL, 140 | [UiohookKey.Comma]: Key.COMMA, 141 | [UiohookKey.Minus]: Key.MINUS, 142 | [UiohookKey.Period]: Key.PERIOD, 143 | [UiohookKey.Slash]: Key.SLASH, 144 | [UiohookKey.Backquote]: Key.BACKTICK, 145 | [UiohookKey.BracketLeft]: Key.LEFT_BRACKET, 146 | [UiohookKey.Backslash]: Key.BACKSLASH, 147 | [UiohookKey.BracketRight]: Key.RIGHT_BRACKET, 148 | [UiohookKey.Quote]: Key.QUOTE, 149 | [UiohookKey.Ctrl]: Key.LEFT_CONTROL, 150 | [UiohookKey.Alt]: Key.LEFT_ALT, 151 | [UiohookKey.Shift]: Key.LEFT_SHIFT, 152 | 153 | 70: Key.SCROLL_LOCK, 154 | 3653: Key.PAUSE_BREAK, 155 | 14: Key.BACKSPACE, 156 | 69: Key.NUM_LOCK, 157 | 158 | 54: Key.RIGHT_SHIFT, 159 | 3613: Key.RIGHT_CONTROL, 160 | 3640: Key.RIGHT_ALT, 161 | 162 | 3675: Key.LEFT_SUPER, 163 | 3676: Key.RIGHT_SUPER, 164 | 165 | 57376: Key.MUTE, 166 | 57390: Key.VOLUME_DOWN, 167 | 57392: Key.VOLUME_UP, 168 | }; 169 | 170 | function uioToKey(uioKey: number): Key | null { 171 | return uioToKeyMap[uioKey] || null; 172 | } 173 | 174 | // ------------ event emitter setup ------------- 175 | const events: Emitter<{ 176 | keydown: KeyboardEvent; 177 | keyup: KeyboardEvent; 178 | }> = mitt(); 179 | 180 | if (eventsDebug.enabled) { 181 | events.on("*", (type, event) => { 182 | eventsDebug("event emitted: %s %o", type, event); 183 | }); 184 | } 185 | 186 | uIOhook.on("keydown", (event) => { 187 | uIOhookEventsDebug("uIOhook keydown event: %o", event); 188 | 189 | const key = uioToKey(event.keycode); 190 | if (!key) { 191 | console.warn( 192 | "WARNING: received keydown for unsupported keycode:", 193 | event.keycode 194 | ); 195 | return; 196 | } 197 | 198 | setKeyState(key, "down"); 199 | 200 | const newEvent = new KeyboardEvent("down", key); 201 | events.emit("keydown", newEvent); 202 | }); 203 | 204 | uIOhook.on("keyup", (event) => { 205 | uIOhookEventsDebug("uIOhook keyup event: %o", event); 206 | 207 | const key = uioToKey(event.keycode); 208 | if (!key) { 209 | console.warn( 210 | "WARNING: received keyup for unsupported keycode:", 211 | event.keycode 212 | ); 213 | return; 214 | } 215 | 216 | setKeyState(key, "up"); 217 | 218 | const newEvent = new KeyboardEvent("up", key); 219 | events.emit("keyup", newEvent); 220 | }); 221 | 222 | // ------------ public API ------------- 223 | export const Keyboard = { 224 | onDown(key: Key, eventHandler: (event: KeyboardEvent) => void): Listener { 225 | debug("Keyboard.onDown called: %s, %o", key, eventHandler); 226 | 227 | const callback = (event) => { 228 | if (key === Key.ANY || event.key === key) { 229 | eventHandler(event); 230 | } 231 | }; 232 | 233 | events.on("keydown", callback); 234 | return { 235 | stop() { 236 | debug("Keyboard.onDown(%s, ...).stop called", key); 237 | events.off("keydown", callback); 238 | }, 239 | }; 240 | }, 241 | 242 | onUp(key: Key, eventHandler: (event: KeyboardEvent) => void): Listener { 243 | debug("Keyboard.onUp called: %s, %o", key, eventHandler); 244 | 245 | const callback = (event) => { 246 | if (key === Key.ANY || event.key === key) { 247 | eventHandler(event); 248 | } 249 | }; 250 | 251 | events.on("keyup", callback); 252 | return { 253 | stop() { 254 | debug("Keyboard.onUp(%s, ...).stop called", key); 255 | events.off("keyup", callback); 256 | }, 257 | }; 258 | }, 259 | 260 | isDown(key: Key): boolean { 261 | const result = isKeyDown(key); 262 | debug("Keyboard.isDown(%s) -> %o", key, result); 263 | return result; 264 | }, 265 | 266 | isUp(key: Key): boolean { 267 | const result = isKeyUp(key); 268 | debug("Keyboard.isUp(%s) -> %o", key, result); 269 | return result; 270 | }, 271 | }; 272 | -------------------------------------------------------------------------------- /src/input/mouse.ts: -------------------------------------------------------------------------------- 1 | import { MouseButton, Listener } from "../types"; 2 | import { uIOhook } from "uiohook-napi"; 3 | import mitt, { Emitter } from "mitt"; 4 | import { getModifierKeysState, KeyboardModifierKeysState } from "./held-keys"; 5 | import { 6 | setButtonState, 7 | isButtonDown, 8 | isButtonUp, 9 | getMouseButtonsState, 10 | MouseButtonsState, 11 | } from "./held-mouse-buttons"; 12 | import makeDebug from "debug"; 13 | 14 | const debug = makeDebug("suchibot:input/mouse"); 15 | const eventsDebug = makeDebug("suchibot:input/mouse/events"); 16 | const uIOhookEventsDebug = makeDebug("suchibot:input/mouse/events/uiohook"); 17 | 18 | // ------------ mouse stuff ------------- 19 | const IS_MOUSE_EVENT = Symbol("IS_MOUSE_EVENT"); 20 | 21 | export function isMouseEvent(event: any): event is MouseEvent { 22 | return typeof event === "object" && event != null && event[IS_MOUSE_EVENT]; 23 | } 24 | 25 | export class MouseEvent { 26 | type: "click" | "down" | "up" | "move"; 27 | button: MouseButton | null; 28 | x: number; 29 | y: number; 30 | modifierKeys: KeyboardModifierKeysState; 31 | mouseButtons: MouseButtonsState; 32 | [IS_MOUSE_EVENT]: true; 33 | 34 | constructor( 35 | type: "click" | "down" | "up" | "move", 36 | button: MouseButton | null, 37 | x: number, 38 | y: number, 39 | modifierKeys: KeyboardModifierKeysState = getModifierKeysState(), 40 | mouseButtons: MouseButtonsState = getMouseButtonsState() 41 | ) { 42 | this.type = type; 43 | this.button = button; 44 | this.x = x; 45 | this.y = y; 46 | this.modifierKeys = modifierKeys; 47 | this.mouseButtons = mouseButtons; 48 | this[IS_MOUSE_EVENT] = true; 49 | } 50 | } 51 | 52 | const uioToMouseButtonMap = { 53 | 1: MouseButton.LEFT, 54 | 2: MouseButton.MIDDLE, 55 | 3: MouseButton.RIGHT, 56 | 4: MouseButton.MOUSE4, 57 | 5: MouseButton.MOUSE5, 58 | }; 59 | 60 | function uioToMouseButton(uioButton: number): MouseButton | null { 61 | return uioToMouseButtonMap[uioButton] || null; 62 | } 63 | 64 | // ------------ event emitter setup ------------- 65 | const events: Emitter<{ 66 | mousedown: MouseEvent; 67 | mouseup: MouseEvent; 68 | mousemove: MouseEvent; 69 | click: MouseEvent; 70 | }> = mitt(); 71 | 72 | if (eventsDebug.enabled) { 73 | events.on("*", (type, event) => { 74 | eventsDebug("event emitted: %s %o", type, event); 75 | }); 76 | } 77 | 78 | uIOhook.on("click", (event) => { 79 | uIOhookEventsDebug("uIOhook click event: %o", event); 80 | 81 | const button = uioToMouseButton(event.button as number); 82 | if (!button) { 83 | console.warn( 84 | "WARNING: received click for unsupported mouse button:", 85 | event.button 86 | ); 87 | return; 88 | } 89 | 90 | setButtonState(button, "up"); 91 | 92 | const newEvent = new MouseEvent("click", button, event.x, event.y); 93 | events.emit("click", newEvent); 94 | }); 95 | 96 | uIOhook.on("mousedown", (event) => { 97 | uIOhookEventsDebug("uIOhook mousedown event: %o", event); 98 | 99 | const button = uioToMouseButton(event.button as number); 100 | if (!button) { 101 | console.warn( 102 | "WARNING: received mousedown for unsupported mouse button:", 103 | event.button 104 | ); 105 | return; 106 | } 107 | 108 | setButtonState(button, "down"); 109 | 110 | const newEvent = new MouseEvent("down", button, event.x, event.y); 111 | events.emit("mousedown", newEvent); 112 | }); 113 | 114 | uIOhook.on("mouseup", (event) => { 115 | uIOhookEventsDebug("uIOhook mouseup event: %o", event); 116 | 117 | const button = uioToMouseButton(event.button as number); 118 | if (!button) { 119 | console.warn( 120 | "WARNING: received mouseup for unsupported mouse button:", 121 | event.button 122 | ); 123 | return; 124 | } 125 | 126 | setButtonState(button, "up"); 127 | 128 | const newEvent = new MouseEvent("up", button, event.x, event.y); 129 | events.emit("mouseup", newEvent); 130 | }); 131 | 132 | uIOhook.on("mousemove", (event) => { 133 | uIOhookEventsDebug("uIOhook mousemove event: %o", event); 134 | 135 | const newEvent = new MouseEvent("move", null, event.x, event.y); 136 | events.emit("mousemove", newEvent); 137 | }); 138 | 139 | // ------------ public API ------------- 140 | export const Mouse = { 141 | onDown( 142 | button: MouseButton, 143 | eventHandler: (event: MouseEvent) => void 144 | ): Listener { 145 | debug("Mouse.onDown called: %s, %o", button, eventHandler); 146 | 147 | const callback = (event: MouseEvent) => { 148 | if ( 149 | String(event.button) === String(button) || 150 | String(button) === String(MouseButton.ANY) 151 | ) { 152 | eventHandler(event); 153 | } 154 | }; 155 | events.on("mousedown", callback); 156 | return { 157 | stop() { 158 | debug("Mouse.onDown(%s, ...).stop called", button); 159 | events.off("mousedown", callback); 160 | }, 161 | }; 162 | }, 163 | onUp( 164 | button: MouseButton, 165 | eventHandler: (event: MouseEvent) => void 166 | ): Listener { 167 | debug("Mouse.onUp called: %s, %o", button, eventHandler); 168 | 169 | const callback = (event: MouseEvent) => { 170 | if ( 171 | String(event.button) === String(button) || 172 | String(button) === String(MouseButton.ANY) 173 | ) { 174 | eventHandler(event); 175 | } 176 | }; 177 | events.on("mouseup", callback); 178 | return { 179 | stop() { 180 | debug("Mouse.onUp(%s, ...).stop called", button); 181 | events.off("mouseup", callback); 182 | }, 183 | }; 184 | }, 185 | onClick( 186 | button: MouseButton, 187 | eventHandler: (event: MouseEvent) => void 188 | ): Listener { 189 | debug("Mouse.onClick called: %s, %o", button, eventHandler); 190 | 191 | const callback = (event: MouseEvent) => { 192 | if ( 193 | String(event.button) === String(button) || 194 | String(button) === String(MouseButton.ANY) 195 | ) { 196 | eventHandler(event); 197 | } 198 | }; 199 | events.on("click", callback); 200 | return { 201 | stop() { 202 | debug("Mouse.onClick(%s, ...).stop called", button); 203 | events.off("click", callback); 204 | }, 205 | }; 206 | }, 207 | onMove(eventHandler: (event: MouseEvent) => void): Listener { 208 | debug("Mouse.onMove called: %o", eventHandler); 209 | 210 | const callback = (event: MouseEvent) => { 211 | eventHandler(event); 212 | }; 213 | events.on("mousemove", callback); 214 | return { 215 | stop() { 216 | debug("Mouse.onMove().stop called"); 217 | events.off("mousemove", callback); 218 | }, 219 | }; 220 | }, 221 | 222 | isDown(button: MouseButton) { 223 | const result = isButtonDown(button); 224 | debug("Mouse.isDown(%s) -> %o", button, result); 225 | return result; 226 | }, 227 | 228 | isUp(button: MouseButton) { 229 | const result = isButtonUp(button); 230 | debug("Mouse.isUp(%s) -> %o", button, result); 231 | return result; 232 | }, 233 | }; 234 | -------------------------------------------------------------------------------- /src/libnut.d.ts: -------------------------------------------------------------------------------- 1 | // Doesn't matter which platform we use here as they all have the same types 2 | export * from "@suchipi/libnut-darwin"; 3 | -------------------------------------------------------------------------------- /src/libnut.js: -------------------------------------------------------------------------------- 1 | switch (process.platform) { 2 | case "win32": { 3 | module.exports = require("@suchipi/libnut-win32"); 4 | break; 5 | } 6 | case "linux": { 7 | module.exports = require("@suchipi/libnut-linux"); 8 | break; 9 | } 10 | case "darwin": { 11 | require("./mac-permissions").requestPermissions(); 12 | 13 | module.exports = require("@suchipi/libnut-darwin"); 14 | break; 15 | } 16 | default: { 17 | throw new Error("Unsupported platform: " + process.platform); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mac-permissions.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from "debug"; 2 | const debug = makeDebug("suchibot:mac-permissions"); 3 | 4 | export async function requestPermissions() { 5 | // @ts-ignore not present on non-mac 6 | const permissions: typeof import("@suchipi/node-mac-permissions") = require("@suchipi/node-mac-permissions"); 7 | 8 | const inputMonitoringPermission = 9 | permissions.getAuthStatus("input-monitoring"); 10 | 11 | debug("Input monitoring status: %s", inputMonitoringPermission); 12 | 13 | if (inputMonitoringPermission !== "authorized") { 14 | console.warn( 15 | `Warning: Input monitoring access was: ${inputMonitoringPermission}. Requesting access.` 16 | ); 17 | await permissions.askForInputMonitoringAccess(); 18 | } 19 | 20 | const accessibilityPermission = permissions.getAuthStatus("accessibility"); 21 | 22 | debug("Accessibility access status: %s", accessibilityPermission); 23 | 24 | if (accessibilityPermission !== "authorized") { 25 | console.warn( 26 | `Warning: Accessibility access was: ${accessibilityPermission}. Opening preference pane; please grant access.` 27 | ); 28 | permissions.askForAccessibilityAccess(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/output/index.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard } from "./keyboard"; 2 | import { Mouse } from "./mouse"; 3 | 4 | export { Keyboard, Mouse }; 5 | -------------------------------------------------------------------------------- /src/output/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { Key } from "../types"; 2 | import * as libnut from "../libnut"; 3 | import { sleep } from "a-mimir"; 4 | import makeDebug from "debug"; 5 | 6 | const debug = makeDebug("suchibot:output/keyboard"); 7 | const libNutDebug = makeDebug("suchibot:output/keyboard/libnut"); 8 | 9 | const keyToNutMap: { [key in keyof typeof Key]: string | null } = { 10 | BACKSPACE: "backspace", 11 | DELETE: "delete", 12 | ENTER: "enter", 13 | TAB: "tab", 14 | ESCAPE: "escape", 15 | UP: "up", 16 | DOWN: "down", 17 | RIGHT: "right", 18 | LEFT: "left", 19 | HOME: "home", 20 | END: "end", 21 | PAGE_UP: "pageup", 22 | PAGE_DOWN: "pagedown", 23 | F1: "f1", 24 | F2: "f2", 25 | F3: "f3", 26 | F4: "f4", 27 | F5: "f5", 28 | F6: "f6", 29 | F7: "f7", 30 | F8: "f8", 31 | F9: "f9", 32 | F10: "f10", 33 | F11: "f11", 34 | F12: "f12", 35 | F13: "f13", 36 | F14: "f14", 37 | F15: "f15", 38 | F16: "f16", 39 | F17: "f17", 40 | F18: "f18", 41 | F19: "f19", 42 | F20: "f20", 43 | F21: "f21", 44 | F22: "f22", 45 | F23: "f23", 46 | F24: "f24", 47 | 48 | LEFT_ALT: "alt", 49 | RIGHT_ALT: "alt", 50 | 51 | LEFT_CONTROL: "control", 52 | RIGHT_CONTROL: "control", 53 | 54 | LEFT_SHIFT: "shift", 55 | RIGHT_SHIFT: "space", 56 | 57 | SPACE: "space", 58 | PRINT_SCREEN: "printscreen", 59 | INSERT: "insert", 60 | VOLUME_DOWN: "audio_vol_down", 61 | VOLUME_UP: "audio_vol_up", 62 | MUTE: "audio_mute", 63 | NUMPAD_0: "numpad_0", 64 | NUMPAD_1: "numpad_1", 65 | NUMPAD_2: "numpad_2", 66 | NUMPAD_3: "numpad_3", 67 | NUMPAD_4: "numpad_4", 68 | NUMPAD_5: "numpad_5", 69 | NUMPAD_6: "numpad_6", 70 | NUMPAD_7: "numpad_7", 71 | NUMPAD_8: "numpad_8", 72 | NUMPAD_9: "numpad_9", 73 | 74 | A: "a", 75 | B: "b", 76 | C: "c", 77 | D: "d", 78 | E: "e", 79 | F: "f", 80 | G: "g", 81 | H: "h", 82 | I: "i", 83 | J: "j", 84 | K: "k", 85 | L: "l", 86 | M: "m", 87 | N: "n", 88 | O: "o", 89 | P: "p", 90 | Q: "q", 91 | R: "r", 92 | S: "s", 93 | T: "t", 94 | U: "u", 95 | V: "v", 96 | W: "w", 97 | X: "x", 98 | Y: "y", 99 | Z: "z", 100 | 101 | ZERO: "0", 102 | ONE: "1", 103 | TWO: "2", 104 | THREE: "3", 105 | FOUR: "4", 106 | FIVE: "5", 107 | SIX: "6", 108 | SEVEN: "7", 109 | EIGHT: "8", 110 | NINE: "9", 111 | 112 | ANY: null, 113 | 114 | CAPS_LOCK: null, 115 | NUMPAD_MULTIPLY: null, 116 | NUMPAD_ADD: null, 117 | NUMPAD_SUBTRACT: null, 118 | NUMPAD_DECIMAL: null, 119 | NUMPAD_DIVIDE: null, 120 | NUMPAD_ENTER: "enter", 121 | SEMICOLON: ";", 122 | 123 | EQUAL: "=", 124 | COMMA: ",", 125 | MINUS: "-", 126 | PERIOD: ".", 127 | SLASH: "/", 128 | BACKTICK: "~", 129 | LEFT_BRACKET: "[", 130 | BACKSLASH: "\\", 131 | RIGHT_BRACKET: "]", 132 | QUOTE: "'", 133 | 134 | SCROLL_LOCK: null, 135 | PAUSE_BREAK: null, 136 | NUM_LOCK: null, 137 | 138 | LEFT_COMMAND: "command", 139 | LEFT_WINDOWS: "command", 140 | LEFT_SUPER: "command", 141 | LEFT_META: "command", 142 | 143 | RIGHT_COMMAND: "command", 144 | RIGHT_WINDOWS: "command", 145 | RIGHT_SUPER: "command", 146 | RIGHT_META: "command", 147 | }; 148 | 149 | function keyToNut(key: Key): string { 150 | if (key === Key.ANY) { 151 | throw new Error( 152 | `The "ANY" key is for input listeners only; it can't be pressed/released` 153 | ); 154 | } 155 | 156 | const result = keyToNutMap[key]; 157 | 158 | if (result == null) { 159 | throw new Error("Pressing/releasing key is not yet supported: " + key); 160 | } 161 | return result; 162 | } 163 | 164 | export const Keyboard = { 165 | tap(key: Key) { 166 | debug("Keyboard.tap(%s)", key); 167 | const nutKey = keyToNut(key); 168 | 169 | libNutDebug("libnut.keyToggle(%s, down)", nutKey); 170 | libnut.keyToggle(nutKey, "down"); 171 | sleep.sync(10); 172 | libNutDebug("libnut.keyToggle(%s, up)", nutKey); 173 | libnut.keyToggle(nutKey, "up"); 174 | }, 175 | 176 | hold(key: Key) { 177 | debug("Keyboard.hold(%s)", key); 178 | const nutKey = keyToNut(key); 179 | libNutDebug("libnut.keyToggle(%s, down)", nutKey); 180 | libnut.keyToggle(nutKey, "down"); 181 | }, 182 | 183 | release(key: Key) { 184 | debug("Keyboard.release(%s)", key); 185 | const nutKey = keyToNut(key); 186 | libNutDebug("libnut.keyToggle(%s, up)", nutKey); 187 | libnut.keyToggle(nutKey, "up"); 188 | }, 189 | 190 | type(textToType: string, delayBetweenKeyPresses: number = 10) { 191 | debug( 192 | "Keyboard.type(%s, %d)", 193 | JSON.stringify(textToType), 194 | delayBetweenKeyPresses 195 | ); 196 | 197 | textToType.split("").forEach((char, index, all) => { 198 | libNutDebug("libnut.typeString(%s)", char); 199 | libnut.typeString(char); 200 | if (index != all.length - 1) { 201 | sleep.sync(delayBetweenKeyPresses); 202 | } 203 | }); 204 | }, 205 | }; 206 | -------------------------------------------------------------------------------- /src/output/mouse.ts: -------------------------------------------------------------------------------- 1 | import { MouseButton } from "../types"; 2 | import * as libnut from "../libnut"; 3 | import { sleep } from "a-mimir"; 4 | import makeDebug from "debug"; 5 | 6 | const debug = makeDebug("suchibot:output/mouse"); 7 | const libNutDebug = makeDebug("suchibot:output/mouse/libnut"); 8 | 9 | const mouseButtonToNutMap: { 10 | [key in keyof typeof MouseButton]: string | null; 11 | } = { 12 | LEFT: "left", 13 | RIGHT: "right", 14 | MIDDLE: "middle", 15 | MOUSE4: null, 16 | MOUSE5: null, 17 | 18 | ANY: null, 19 | }; 20 | 21 | function mouseButtonToNut(button: MouseButton): string { 22 | if (button === MouseButton.ANY) { 23 | throw new Error( 24 | `The "ANY" mouse button is for input listeners only; it can't be pressed/released` 25 | ); 26 | } 27 | 28 | const result = mouseButtonToNutMap[button]; 29 | if (result == null) { 30 | throw new Error( 31 | "Pressing/releasing the following mouse button is not yet supported: " + 32 | button 33 | ); 34 | } 35 | return result; 36 | } 37 | 38 | export const Mouse = { 39 | moveTo(x: number, y: number) { 40 | debug("Mouse.moveTo(%d, %d)", x, y); 41 | libNutDebug("libnut.moveMouse(%d, %d)", x, y); 42 | libnut.moveMouse(x, y); 43 | }, 44 | click(button: MouseButton = MouseButton.LEFT) { 45 | debug("Mouse.click(%s)", button); 46 | const nutButton = mouseButtonToNut(button); 47 | 48 | libNutDebug("libnut.mouseToggle(down, %s)", nutButton); 49 | libnut.mouseToggle("down", nutButton); 50 | sleep.sync(4); 51 | libNutDebug("libnut.mouseToggle(up, %s)", nutButton); 52 | libnut.mouseToggle("up", nutButton); 53 | }, 54 | doubleClick(button: MouseButton = MouseButton.LEFT) { 55 | debug("Mouse.doubleClick(%s)", button); 56 | const nutButton = mouseButtonToNut(button); 57 | 58 | libNutDebug("libnut.mouseToggle(down, %s)", nutButton); 59 | libnut.mouseToggle("down", nutButton); 60 | sleep.sync(4); 61 | libNutDebug("libnut.mouseToggle(up, %s)", nutButton); 62 | libnut.mouseToggle("up", nutButton); 63 | sleep.sync(4); 64 | libNutDebug("libnut.mouseToggle(down, %s)", nutButton); 65 | libnut.mouseToggle("down", nutButton); 66 | sleep.sync(4); 67 | libNutDebug("libnut.mouseToggle(up, %s)", nutButton); 68 | libnut.mouseToggle("up", nutButton); 69 | }, 70 | hold(button: MouseButton = MouseButton.LEFT) { 71 | debug("Mouse.hold(%s)", button); 72 | const nutButton = mouseButtonToNut(button); 73 | 74 | libNutDebug("libnut.mouseToggle(down, %s)", nutButton); 75 | libnut.mouseToggle("down", nutButton); 76 | }, 77 | release(button: MouseButton = MouseButton.LEFT) { 78 | debug("Mouse.release(%s)", button); 79 | const nutButton = mouseButtonToNut(button); 80 | 81 | libNutDebug("libnut.mouseToggle(up, %s)", nutButton); 82 | libnut.mouseToggle("up", nutButton); 83 | }, 84 | getPosition(): { x: number; y: number } { 85 | debug("Mouse.getPosition called"); 86 | libNutDebug("libnut.getMousePos called"); 87 | const result = libnut.getMousePos(); 88 | libNutDebug("libnut.getMousePos -> %o", result); 89 | debug("Mouse.getPosition -> %o", result); 90 | return result; 91 | }, 92 | scroll({ x = 0, y = 0 } = {}) { 93 | if (debug.enabled) { 94 | debug("Mouse.getPosition(%o)", { x, y }); 95 | } 96 | libNutDebug("libnut.scrollMouse(%d, %d)", x, y); 97 | libnut.scrollMouse(x, y); 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /src/patch-cjs.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Module } from "module"; 3 | import { register } from "esbuild-register/dist/node"; 4 | 5 | export function patchCjs() { 6 | const originalResolveFilename = (Module as any)._resolveFilename; 7 | (Module as any)._resolveFilename = function patchedResolveFilename( 8 | request: string, 9 | parent: any, 10 | isMain: boolean, 11 | options: any 12 | ) { 13 | if (request === "suchibot") { 14 | return path.resolve(__dirname, "..", "index.js"); 15 | } else { 16 | return originalResolveFilename(request, parent, isMain, options); 17 | } 18 | }; 19 | 20 | register({ 21 | target: `node${process.version.replace(/^v/i, "")}`, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/recording/event-filters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MouseEvent, 3 | KeyboardEvent, 4 | KeyboardModifierKeysState, 5 | isMouseEvent, 6 | isKeyboardEvent, 7 | } from "../input"; 8 | import { MouseButton, Key } from "../types"; 9 | 10 | export type MouseEventFilter = { 11 | filterType: "Mouse"; 12 | type?: "click" | "down" | "up" | "move"; 13 | button?: MouseButton; 14 | x?: number; 15 | y?: number; 16 | }; 17 | 18 | export type KeyboardEventFilter = { 19 | filterType: "Keyboard"; 20 | type?: "down" | "up"; 21 | key?: Key; 22 | modifierKeys?: Partial; 23 | }; 24 | 25 | export function mouseEventFilter( 26 | criteria: { 27 | type?: "click" | "down" | "up" | "move"; 28 | button?: MouseButton; 29 | x?: number; 30 | y?: number; 31 | } = {} 32 | ): MouseEventFilter { 33 | return { 34 | filterType: "Mouse", 35 | ...criteria, 36 | }; 37 | } 38 | 39 | export function keyboardEventFilter( 40 | criteria: { 41 | type?: "down" | "up"; 42 | key?: Key; 43 | modifierKeys?: Partial; 44 | } = {} 45 | ): KeyboardEventFilter { 46 | return { 47 | filterType: "Keyboard", 48 | ...criteria, 49 | }; 50 | } 51 | 52 | function mouseEventMatchesFilter( 53 | event: MouseEvent, 54 | filter: MouseEventFilter 55 | ): boolean { 56 | let doesMatch = true; 57 | 58 | if (filter.type != null) { 59 | doesMatch = doesMatch && filter.type === event.type; 60 | } 61 | 62 | if (filter.button != null && filter.button !== MouseButton.ANY) { 63 | doesMatch = doesMatch && filter.button === event.button; 64 | } 65 | 66 | if (filter.x != null) { 67 | doesMatch = doesMatch && filter.x === event.x; 68 | } 69 | 70 | if (filter.y != null) { 71 | doesMatch = doesMatch && filter.y === event.y; 72 | } 73 | 74 | return doesMatch; 75 | } 76 | 77 | function keyboardEventMatchesFilter( 78 | event: KeyboardEvent, 79 | filter: KeyboardEventFilter 80 | ): boolean { 81 | let doesMatch = true; 82 | 83 | if (filter.type != null) { 84 | doesMatch = doesMatch && filter.type === event.type; 85 | } 86 | 87 | if (filter.key != null && filter.key !== Key.ANY) { 88 | doesMatch = doesMatch && filter.key === event.key; 89 | } 90 | 91 | if (doesMatch && filter.modifierKeys != null) { 92 | for (const key in filter.modifierKeys) { 93 | if (Object.prototype.hasOwnProperty.call(filter.modifierKeys, key)) { 94 | doesMatch = 95 | doesMatch && event.modifierKeys[key] === filter.modifierKeys[key]; 96 | 97 | if (!doesMatch) { 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | 104 | return doesMatch; 105 | } 106 | 107 | export function eventMatchesFilter( 108 | event: MouseEvent | KeyboardEvent, 109 | filter: MouseEventFilter | KeyboardEventFilter 110 | ): boolean { 111 | if (isMouseEvent(event) && filter.filterType === "Mouse") { 112 | return mouseEventMatchesFilter(event, filter); 113 | } else if (isKeyboardEvent(event) && filter.filterType === "Keyboard") { 114 | return keyboardEventMatchesFilter(event, filter); 115 | } else { 116 | return false; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/recording/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KeyboardEventFilter, 3 | MouseEventFilter, 4 | eventMatchesFilter, 5 | keyboardEventFilter, 6 | mouseEventFilter, 7 | } from "./event-filters"; 8 | import { Tape, SerializedTape } from "./tape"; 9 | 10 | export { 11 | KeyboardEventFilter, 12 | MouseEventFilter, 13 | eventMatchesFilter, 14 | keyboardEventFilter, 15 | mouseEventFilter, 16 | Tape, 17 | SerializedTape, 18 | }; 19 | -------------------------------------------------------------------------------- /src/recording/tape-data.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent, KeyboardEvent, isMouseEvent } from "../input"; 2 | 3 | export type RecordedAction = { 4 | event: MouseEvent | KeyboardEvent; 5 | time: number; 6 | }; 7 | 8 | export class TapeData { 9 | actions: Array; 10 | 11 | get length(): number { 12 | const { actions } = this; 13 | 14 | if (actions.length > 0) { 15 | const lastAction = actions[actions.length - 1]; 16 | return lastAction.time; 17 | } else { 18 | return 0; 19 | } 20 | } 21 | 22 | constructor(actions: Array) { 23 | this.actions = actions; 24 | } 25 | 26 | static deserialize(data: SerializedTapeData) { 27 | const actions = data.actions.map((serializedAction) => { 28 | const { time, event } = serializedAction; 29 | 30 | return { 31 | time: time, 32 | event: 33 | event.$kind === "MouseEvent" 34 | ? new MouseEvent(event.type, event.button, event.x, event.y) 35 | : new KeyboardEvent(event.type, event.key), 36 | }; 37 | }); 38 | 39 | return new TapeData(actions); 40 | } 41 | 42 | serialize(): SerializedTapeData { 43 | return { 44 | $kind: "TapeData", 45 | actions: this.actions.map((action) => { 46 | const { time, event } = action; 47 | 48 | return { 49 | $kind: "RecordedAction", 50 | time, 51 | event: isMouseEvent(event) 52 | ? { 53 | $kind: "MouseEvent", 54 | type: event.type, 55 | button: event.button, 56 | x: event.x, 57 | y: event.y, 58 | } 59 | : { 60 | $kind: "KeyboardEvent", 61 | type: event.type, 62 | key: event.key, 63 | }, 64 | }; 65 | }), 66 | }; 67 | } 68 | } 69 | 70 | export type SerializedMouseEvent = { 71 | $kind: "MouseEvent"; 72 | type: MouseEvent["type"]; 73 | button: MouseEvent["button"]; 74 | x: number; 75 | y: number; 76 | }; 77 | 78 | export type SerializedKeyboardEvent = { 79 | $kind: "KeyboardEvent"; 80 | type: KeyboardEvent["type"]; 81 | key: KeyboardEvent["key"]; 82 | }; 83 | 84 | export type SerializedTapeData = { 85 | $kind: "TapeData"; 86 | actions: Array<{ 87 | $kind: "RecordedAction"; 88 | time: number; 89 | event: SerializedMouseEvent | SerializedKeyboardEvent; 90 | }>; 91 | }; 92 | -------------------------------------------------------------------------------- /src/recording/tape-player.ts: -------------------------------------------------------------------------------- 1 | import kleur from "kleur"; 2 | import Defer from "@suchipi/defer"; 3 | import { formatError } from "pretty-print-error"; 4 | import * as input from "../input"; 5 | import * as output from "../output"; 6 | import { TapeData } from "./tape-data"; 7 | 8 | const DATA = Symbol("DATA"); 9 | const IS_PLAYING = Symbol("IS_PLAYING"); 10 | const TIMEOUTS = Symbol("TIMEOUTS"); 11 | const PLAYBACK_DEFER = Symbol("PLAYBACK_DEFER"); 12 | 13 | export class TapePlayer { 14 | get isPlaying() { 15 | return this[IS_PLAYING]; 16 | } 17 | 18 | private [DATA]: TapeData; 19 | private [IS_PLAYING]: boolean = false; 20 | private [TIMEOUTS]: Set>; 21 | private [PLAYBACK_DEFER]: Defer | null = null; 22 | 23 | constructor(data: TapeData) { 24 | this[DATA] = data; 25 | this[TIMEOUTS] = new Set(); 26 | } 27 | 28 | play(): Promise { 29 | if (this[IS_PLAYING]) { 30 | throw new Error( 31 | "Attempted to play a tape that was already playing. This is disallowed." 32 | ); 33 | } 34 | 35 | for (const action of this[DATA].actions) { 36 | const { event, time } = action; 37 | const timeout = setTimeout(() => { 38 | try { 39 | this[TIMEOUTS].delete(timeout); 40 | 41 | if (input.isMouseEvent(event)) { 42 | switch (event.type) { 43 | case "click": { 44 | // We intentionally don't play these back, because the down/up events will do the same thing. 45 | // If we play both down/up and click events, we'll double-click for every single click. 46 | // output.Mouse.click(event.button!); 47 | break; 48 | } 49 | case "down": { 50 | output.Mouse.hold(event.button!); 51 | break; 52 | } 53 | case "up": { 54 | output.Mouse.release(event.button!); 55 | break; 56 | } 57 | case "move": { 58 | output.Mouse.moveTo(event.x, event.y); 59 | break; 60 | } 61 | } 62 | } else if (input.isKeyboardEvent(event)) { 63 | switch (event.type) { 64 | case "down": { 65 | output.Keyboard.hold(event.key); 66 | break; 67 | } 68 | case "up": { 69 | output.Keyboard.release(event.key); 70 | break; 71 | } 72 | } 73 | } 74 | } catch (err) { 75 | console.log( 76 | kleur.yellow( 77 | "An error was thrown while playing back a recorded action:" 78 | ) 79 | ); 80 | console.log(formatError(err)); 81 | console.log( 82 | kleur.yellow( 83 | "Playback of future recorded actions will continue despite the error." 84 | ) 85 | ); 86 | } 87 | }, time); 88 | 89 | this[TIMEOUTS].add(timeout); 90 | } 91 | 92 | this[IS_PLAYING] = true; 93 | 94 | const defer = new Defer(); 95 | this[PLAYBACK_DEFER] = defer; 96 | 97 | const afterSleep = () => { 98 | this[IS_PLAYING] = false; 99 | this[TIMEOUTS] = new Set>(); 100 | }; 101 | 102 | const timeToSleep = (this[DATA].length || 0) + 2; 103 | 104 | const sleepTimeout = setTimeout(defer.resolve, timeToSleep); 105 | this[TIMEOUTS].add(sleepTimeout); 106 | 107 | return defer.promise.then(afterSleep, afterSleep); 108 | } 109 | 110 | stop() { 111 | if (!this[IS_PLAYING]) { 112 | throw new Error( 113 | "Attempted to stop playing a tape that wasn't being played. This isn't allowed." 114 | ); 115 | } 116 | 117 | for (const timeout of this[TIMEOUTS]) { 118 | clearTimeout(timeout); 119 | this[TIMEOUTS].delete(timeout); 120 | } 121 | 122 | const defer = this[PLAYBACK_DEFER]; 123 | if (defer != null) { 124 | defer.reject(new Error("Playback cancelled by `stop` call")); 125 | this[PLAYBACK_DEFER] = null; 126 | } 127 | 128 | this[IS_PLAYING] = false; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/recording/tape-recorder.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent, KeyboardEvent, Mouse, Keyboard } from "../input"; 2 | import { 3 | MouseEventFilter, 4 | KeyboardEventFilter, 5 | eventMatchesFilter, 6 | } from "./event-filters"; 7 | import { TapeData } from "./tape-data"; 8 | import { MouseButton, Key, Listener } from "../types"; 9 | 10 | const DATA = Symbol("DATA"); 11 | const RECORDING_STARTED_AT = Symbol("RECORDING_STARTED_AT"); 12 | const EVENTS_TO_IGNORE = Symbol("EVENTS_TO_IGNORE"); 13 | const IS_RECORDING = Symbol("IS_RECORDING"); 14 | const PUSH_EVENT = Symbol("PUSH_EVENT"); 15 | const LISTENERS = Symbol("LISTENERS"); 16 | const STOP_LISTENING = Symbol("STOP_LISTENING"); 17 | 18 | export class TapeRecorder { 19 | get isRecording() { 20 | return this[IS_RECORDING]; 21 | } 22 | 23 | private [IS_RECORDING]: boolean = false; 24 | private [DATA]: TapeData; 25 | private [RECORDING_STARTED_AT]: number = -1; 26 | private [EVENTS_TO_IGNORE]: Array; 27 | private [LISTENERS]: Array = []; 28 | 29 | constructor( 30 | data: TapeData, 31 | eventsToIgnore: Array 32 | ) { 33 | this[DATA] = data; 34 | this[EVENTS_TO_IGNORE] = eventsToIgnore; 35 | } 36 | 37 | start(): void { 38 | if (this[IS_RECORDING]) { 39 | throw new Error( 40 | "Attempted to start a tape recorder that was already recording. This isn't allowed." 41 | ); 42 | } 43 | 44 | this[RECORDING_STARTED_AT] = Date.now(); 45 | 46 | const boundPushEvent = this[PUSH_EVENT].bind(this); 47 | this[LISTENERS] = [ 48 | Mouse.onClick(MouseButton.ANY, boundPushEvent), 49 | Mouse.onDown(MouseButton.ANY, boundPushEvent), 50 | Mouse.onUp(MouseButton.ANY, boundPushEvent), 51 | Mouse.onMove(boundPushEvent), 52 | 53 | Keyboard.onDown(Key.ANY, boundPushEvent), 54 | Keyboard.onUp(Key.ANY, boundPushEvent), 55 | ]; 56 | 57 | this[IS_RECORDING] = true; 58 | } 59 | 60 | private [PUSH_EVENT](event: MouseEvent | KeyboardEvent) { 61 | const now = Date.now(); 62 | let shouldPush = true; 63 | 64 | for (const filter of this[EVENTS_TO_IGNORE]) { 65 | shouldPush = shouldPush && !eventMatchesFilter(event, filter); 66 | } 67 | 68 | if (shouldPush) { 69 | this[DATA].actions.push({ 70 | event, 71 | time: now - this[RECORDING_STARTED_AT], 72 | }); 73 | } 74 | } 75 | 76 | private [STOP_LISTENING]() { 77 | for (const listener of this[LISTENERS]) { 78 | listener.stop(); 79 | } 80 | this[LISTENERS] = []; 81 | } 82 | 83 | finish(): void { 84 | if (!this[IS_RECORDING]) { 85 | throw new Error( 86 | "Attempted to stop a tape recorder that was already stopped. This isn't allowed." 87 | ); 88 | } 89 | 90 | this[STOP_LISTENING](); 91 | this[IS_RECORDING] = false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/recording/tape.ts: -------------------------------------------------------------------------------- 1 | import { SerializedTapeData, TapeData } from "./tape-data"; 2 | import { TapePlayer } from "./tape-player"; 3 | import { TapeRecorder } from "./tape-recorder"; 4 | import { KeyboardEventFilter, MouseEventFilter } from "./event-filters"; 5 | 6 | const DATA = Symbol("DATA"); 7 | const CURRENT_STATE = Symbol("CURRENT_STATE"); 8 | const EVENTS_TO_IGNORE = Symbol("EVENTS_TO_IGNORE"); 9 | const RECORDER = Symbol("RECORDER"); 10 | const PLAYER = Symbol("PLAYER"); 11 | 12 | type TapeState = string & { __type: "TapeState" }; 13 | const TapeState = { 14 | RECORDING: "RECORDING" as TapeState, 15 | PLAYING: "PLAYING" as TapeState, 16 | IDLE: "IDLE" as TapeState, 17 | }; 18 | 19 | export type SerializedTape = { 20 | $kind: "Tape"; 21 | eventsToIgnore: Array; 22 | data: SerializedTapeData; 23 | }; 24 | 25 | export class Tape { 26 | static State = TapeState; 27 | 28 | private [DATA]: TapeData; 29 | private [CURRENT_STATE]: TapeState = TapeState.IDLE; 30 | private [EVENTS_TO_IGNORE]: Array; 31 | 32 | private [RECORDER]: TapeRecorder; 33 | private [PLAYER]: TapePlayer; 34 | 35 | constructor( 36 | eventsToIgnore: Array = [], 37 | data: TapeData = new TapeData([]) 38 | ) { 39 | this[DATA] = data; 40 | this[EVENTS_TO_IGNORE] = eventsToIgnore; 41 | 42 | this[RECORDER] = new TapeRecorder(data, eventsToIgnore); 43 | this[PLAYER] = new TapePlayer(data); 44 | } 45 | 46 | get state(): TapeState { 47 | return this[CURRENT_STATE]; 48 | } 49 | 50 | record() { 51 | if (this[CURRENT_STATE] === TapeState.RECORDING) { 52 | throw new Error( 53 | "Attempted to record to a tape that was already being recorded to. This isn't allowed." 54 | ); 55 | } 56 | 57 | if (this[CURRENT_STATE] === TapeState.PLAYING) { 58 | throw new Error( 59 | "Attempted to record to a tape that was being played. This isn't allowed." 60 | ); 61 | } 62 | 63 | this[RECORDER].start(); 64 | this[CURRENT_STATE] = TapeState.RECORDING; 65 | } 66 | stopRecording() { 67 | if (this[CURRENT_STATE] !== TapeState.RECORDING) { 68 | throw new Error( 69 | "Attempted to stop recording a tape that wasn't being recorded to. This isn't allowed." 70 | ); 71 | } 72 | 73 | this[RECORDER].finish(); 74 | this[CURRENT_STATE] = TapeState.IDLE; 75 | } 76 | 77 | play(): Promise { 78 | if (this[CURRENT_STATE] === TapeState.PLAYING) { 79 | throw new Error( 80 | "Attempted to play a tape that was already being played. This isn't allowed." 81 | ); 82 | } 83 | 84 | if (this[CURRENT_STATE] === TapeState.RECORDING) { 85 | throw new Error( 86 | "Attempted to play a tape that was being recorded to. This isn't allowed." 87 | ); 88 | } 89 | 90 | const promise = this[PLAYER].play(); 91 | this[CURRENT_STATE] = TapeState.PLAYING; 92 | promise.then(() => { 93 | this[CURRENT_STATE] = TapeState.IDLE; 94 | }); 95 | return promise; 96 | } 97 | stopPlaying() { 98 | if (this[CURRENT_STATE] !== TapeState.PLAYING) { 99 | throw new Error( 100 | "Attempted to stop playing a tape that wasn't being played. This isn't allowed." 101 | ); 102 | } 103 | 104 | this[PLAYER].stop(); 105 | this[CURRENT_STATE] = TapeState.IDLE; 106 | } 107 | 108 | serialize(): SerializedTape { 109 | return { 110 | $kind: "Tape", 111 | eventsToIgnore: this[EVENTS_TO_IGNORE], 112 | data: this[DATA].serialize(), 113 | }; 114 | } 115 | 116 | static deserialize(serialized: SerializedTape): Tape { 117 | return new Tape( 118 | serialized.eventsToIgnore, 119 | TapeData.deserialize(serialized.data) 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/screen.ts: -------------------------------------------------------------------------------- 1 | import * as libnut from "./libnut"; 2 | import makeDebug from "debug"; 3 | 4 | const debug = makeDebug("suchibot:screen"); 5 | 6 | export const Screen = { 7 | getSize(): { width: number; height: number } { 8 | debug("Screen.getSize called"); 9 | const result = libnut.getScreenSize(); 10 | debug("Screen.getSize result: %o", result); 11 | return result; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // TODO: change these to enums once kame can do typescript enums (kame uses @babel/preset-typescript) 2 | 3 | export type Key = string & { __type__: "Key" }; 4 | export const Key = { 5 | BACKSPACE: "BACKSPACE" as Key, 6 | DELETE: "DELETE" as Key, 7 | ENTER: "ENTER" as Key, 8 | TAB: "TAB" as Key, 9 | ESCAPE: "ESCAPE" as Key, 10 | UP: "UP" as Key, 11 | DOWN: "DOWN" as Key, 12 | RIGHT: "RIGHT" as Key, 13 | LEFT: "LEFT" as Key, 14 | HOME: "HOME" as Key, 15 | INSERT: "INSERT" as Key, 16 | END: "END" as Key, 17 | PAGE_UP: "PAGE_UP" as Key, 18 | PAGE_DOWN: "PAGE_DOWN" as Key, 19 | SPACE: "SPACE" as Key, 20 | 21 | F1: "F1" as Key, 22 | F2: "F2" as Key, 23 | F3: "F3" as Key, 24 | F4: "F4" as Key, 25 | F5: "F5" as Key, 26 | F6: "F6" as Key, 27 | F7: "F7" as Key, 28 | F8: "F8" as Key, 29 | F9: "F9" as Key, 30 | F10: "F10" as Key, 31 | F11: "F11" as Key, 32 | F12: "F12" as Key, 33 | F13: "F13" as Key, 34 | F14: "F14" as Key, 35 | F15: "F15" as Key, 36 | F16: "F16" as Key, 37 | F17: "F17" as Key, 38 | F18: "F18" as Key, 39 | F19: "F19" as Key, 40 | F20: "F20" as Key, 41 | F21: "F21" as Key, 42 | F22: "F22" as Key, 43 | F23: "F23" as Key, 44 | F24: "F24" as Key, 45 | 46 | LEFT_ALT: "LEFT_ALT" as Key, 47 | LEFT_CONTROL: "LEFT_CONTROL" as Key, 48 | LEFT_SHIFT: "LEFT_SHIFT" as Key, 49 | 50 | RIGHT_ALT: "RIGHT_ALT" as Key, 51 | RIGHT_CONTROL: "RIGHT_CONTROL" as Key, 52 | RIGHT_SHIFT: "RIGHT_SHIFT" as Key, 53 | 54 | LEFT_WINDOWS: "LEFT_SUPER" as Key, 55 | LEFT_COMMAND: "LEFT_SUPER" as Key, 56 | LEFT_META: "LEFT_SUPER" as Key, 57 | LEFT_SUPER: "LEFT_SUPER" as Key, 58 | 59 | RIGHT_WINDOWS: "RIGHT_SUPER" as Key, 60 | RIGHT_COMMAND: "RIGHT_SUPER" as Key, 61 | RIGHT_META: "RIGHT_SUPER" as Key, 62 | RIGHT_SUPER: "RIGHT_SUPER" as Key, 63 | 64 | PRINT_SCREEN: "PRINT_SCREEN" as Key, 65 | VOLUME_DOWN: "VOLUME_DOWN" as Key, 66 | VOLUME_UP: "VOLUME_UP" as Key, 67 | MUTE: "MUTE" as Key, 68 | PAUSE_BREAK: "PAUSE_BREAK" as Key, 69 | 70 | NUMPAD_0: "NUMPAD_0" as Key, 71 | NUMPAD_1: "NUMPAD_1" as Key, 72 | NUMPAD_2: "NUMPAD_2" as Key, 73 | NUMPAD_3: "NUMPAD_3" as Key, 74 | NUMPAD_4: "NUMPAD_4" as Key, 75 | NUMPAD_5: "NUMPAD_5" as Key, 76 | NUMPAD_6: "NUMPAD_6" as Key, 77 | NUMPAD_7: "NUMPAD_7" as Key, 78 | NUMPAD_8: "NUMPAD_8" as Key, 79 | NUMPAD_9: "NUMPAD_9" as Key, 80 | NUMPAD_MULTIPLY: "NUMPAD_MULTIPLY" as Key, 81 | NUMPAD_ADD: "NUMPAD_ADD" as Key, 82 | NUMPAD_SUBTRACT: "NUMPAD_SUBTRACT" as Key, 83 | NUMPAD_DECIMAL: "NUMPAD_DECIMAL" as Key, 84 | NUMPAD_DIVIDE: "NUMPAD_DIVIDE" as Key, 85 | NUMPAD_ENTER: "NUMPAD_ENTER" as Key, 86 | 87 | CAPS_LOCK: "CAPS_LOCK" as Key, 88 | NUM_LOCK: "NUM_LOCK" as Key, 89 | SCROLL_LOCK: "SCROLL_LOCK" as Key, 90 | 91 | SEMICOLON: "SEMICOLON" as Key, 92 | EQUAL: "EQUAL" as Key, 93 | COMMA: "COMMA" as Key, 94 | MINUS: "MINUS" as Key, 95 | PERIOD: "PERIOD" as Key, 96 | SLASH: "SLASH" as Key, 97 | BACKTICK: "BACKTICK" as Key, 98 | LEFT_BRACKET: "LEFT_BRACKET" as Key, 99 | BACKSLASH: "BACKSLASH" as Key, 100 | RIGHT_BRACKET: "RIGHT_BRACKET" as Key, 101 | QUOTE: "QUOTE" as Key, 102 | 103 | A: "A" as Key, 104 | B: "B" as Key, 105 | C: "C" as Key, 106 | D: "D" as Key, 107 | E: "E" as Key, 108 | F: "F" as Key, 109 | G: "G" as Key, 110 | H: "H" as Key, 111 | I: "I" as Key, 112 | J: "J" as Key, 113 | K: "K" as Key, 114 | L: "L" as Key, 115 | M: "M" as Key, 116 | N: "N" as Key, 117 | O: "O" as Key, 118 | P: "P" as Key, 119 | Q: "Q" as Key, 120 | R: "R" as Key, 121 | S: "S" as Key, 122 | T: "T" as Key, 123 | U: "U" as Key, 124 | V: "V" as Key, 125 | W: "W" as Key, 126 | X: "X" as Key, 127 | Y: "Y" as Key, 128 | Z: "Z" as Key, 129 | 130 | ZERO: "ZERO" as Key, 131 | ONE: "ONE" as Key, 132 | TWO: "TWO" as Key, 133 | THREE: "THREE" as Key, 134 | FOUR: "FOUR" as Key, 135 | FIVE: "FIVE" as Key, 136 | SIX: "SIX" as Key, 137 | SEVEN: "SEVEN" as Key, 138 | EIGHT: "EIGHT" as Key, 139 | NINE: "NINE" as Key, 140 | 141 | ANY: "ANY" as Key, 142 | }; 143 | 144 | export type MouseButton = string & { __type__: "MouseButton" }; 145 | export const MouseButton = { 146 | LEFT: "LEFT" as MouseButton, 147 | RIGHT: "RIGHT" as MouseButton, 148 | MIDDLE: "MIDDLE" as MouseButton, 149 | MOUSE4: "MOUSE4" as MouseButton, 150 | MOUSE5: "MOUSE5" as MouseButton, 151 | 152 | ANY: "ANY" as MouseButton, 153 | }; 154 | 155 | export type Listener = { 156 | stop(): void; 157 | }; 158 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 3 | "exclude": ["./src/**/*.test.ts"], 4 | 5 | "compilerOptions": { 6 | "lib": ["es2019", "dom"], 7 | "target": "es5", 8 | "module": "CommonJS", 9 | "outDir": "./dist", 10 | "declaration": true, 11 | 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "downlevelIteration": true, 24 | 25 | "moduleResolution": "node", 26 | "esModuleInterop": true, 27 | 28 | "jsx": "react" 29 | } 30 | } 31 | --------------------------------------------------------------------------------