├── .gitignore ├── CHANGES.md ├── LICENSE.txt ├── Makefile ├── README.md ├── make_release.py ├── mouse ├── __init__.py ├── __main__.py ├── _darwinmouse.py ├── _generic.py ├── _mouse_event.py ├── _mouse_tests.py ├── _nixcommon.py ├── _nixmouse.py └── _winmouse.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # 0.7.1 2 | 3 | - Fixed errors and incorrect information on setup.py. 4 | - Fixed Windows segfault. 5 | - Applied pending bug fixes. 6 | 7 | 8 | # 0.7.0 9 | 10 | - [All] Fix Windows hook error (#1). 11 | - [All] Add __main__ module, allowing `python -m mouse` to save and load events. 12 | 13 | 14 | # 0.6.1 15 | 16 | - [Windows] Fixed ctypes type-checking error. 17 | - [All] Misc fixes to release process. 18 | 19 | 20 | # 0.6.0 21 | 22 | - Added README and API docs. 23 | - Bump version to replace old library. 24 | 25 | 26 | # 0.0.1 27 | 28 | - Initial release, migrated from `keyboard` package. 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 BoppreH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: tests 2 | tests: 3 | python2 -m coverage run -m mouse._mouse_tests 4 | python2 -m coverage run -am mouse._mouse_tests 5 | python -m coverage run -am mouse._mouse_tests 6 | python -m coverage run -am mouse._mouse_tests 7 | python -m coverage report && coverage3 html 8 | 9 | build: tests mouse setup.py 10 | python ../docstring2markdown/docstring2markdown.py mouse "https://github.com/boppreh/mouse/blob/master" > README.md 11 | find . \( -name "*.py" -o -name "*.sh" -o -name "* .md" \) -exec dos2unix {} \; 12 | python setup.py sdist --format=zip bdist_wheel --universal bdist_wininst && twine check dist/* 13 | 14 | release: 15 | python make_release.py 16 | 17 | clean: 18 | rm -rfv dist build coverage_html_report mouse.egg-info -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | mouse 3 | ===== 4 | 5 | Take full control of your mouse with this small Python library. Hook global events, register hotkeys, simulate mouse movement and clicks, and much more. 6 | 7 | _Huge thanks to [Kirill Pavlov](http://kirillpavlov.com/) for donating the package name. If you are looking for the Cheddargetter.com client implementation, [`pip install mouse==0.5.0`](https://pypi.python.org/pypi/mouse/0.5.0)._ 8 | 9 | ## Features 10 | 11 | - Global event hook on all mice devices (captures events regardless of focus). 12 | - **Listen** and **sends** mouse events. 13 | - Works with **Windows** and **Linux** (requires sudo). 14 | - Works with **MacOS** (requires granting accessibility permissions to terminal/python in System Preferences -> Security \& Privacy) 15 | - **Pure Python**, no C modules to be compiled. 16 | - **Zero dependencies** on Windows and Linux. Trivial to install and deploy, just copy the files. 17 | - **Python 2 and 3**. 18 | - Includes **high level API** (e.g. [record](#mouse.record) and [play](#mouse.play). 19 | - Events automatically captured in separate thread, doesn't block main program. 20 | - Tested and documented. 21 | 22 | This program makes no attempt to hide itself, so don't use it for keyloggers. 23 | 24 | ## Usage 25 | 26 | Install the [PyPI package](https://pypi.python.org/pypi/mouse/): 27 | 28 | $ sudo pip install mouse 29 | 30 | or clone the repository (no installation required, source files are sufficient): 31 | 32 | $ git clone https://github.com/boppreh/mouse 33 | 34 | Then check the [API docs](https://github.com/boppreh/mouse#api) to see what features are available. 35 | 36 | 37 | ## Known limitations: 38 | 39 | - Events generated under Windows don't report device id (`event.device == None`). [#21](https://github.com/boppreh/keyboard/issues/21) 40 | - To avoid depending on X the Linux parts reads raw device files (`/dev/input/input*`) but this requires root. 41 | - Other applications, such as some games, may register hooks that swallow all key events. In this case `mouse` will be unable to report events. 42 | 43 | 44 | 45 | # API 46 | #### Table of Contents 47 | 48 | - [mouse.**ButtonEvent**](#mouse.ButtonEvent) 49 | - [mouse.**DOUBLE**](#mouse.DOUBLE) 50 | - [mouse.**DOWN**](#mouse.DOWN) 51 | - [mouse.**LEFT**](#mouse.LEFT) 52 | - [mouse.**MIDDLE**](#mouse.MIDDLE) 53 | - [mouse.**MoveEvent**](#mouse.MoveEvent) 54 | - [mouse.**RIGHT**](#mouse.RIGHT) 55 | - [mouse.**UP**](#mouse.UP) 56 | - [mouse.**WheelEvent**](#mouse.WheelEvent) 57 | - [mouse.**X**](#mouse.X) 58 | - [mouse.**X2**](#mouse.X2) 59 | - [mouse.**version**](#mouse.version) 60 | - [mouse.**is\_pressed**](#mouse.is_pressed) 61 | - [mouse.**press**](#mouse.press) *(aliases: `hold`)* 62 | - [mouse.**release**](#mouse.release) 63 | - [mouse.**click**](#mouse.click) 64 | - [mouse.**double\_click**](#mouse.double_click) 65 | - [mouse.**right\_click**](#mouse.right_click) 66 | - [mouse.**wheel**](#mouse.wheel) 67 | - [mouse.**move**](#mouse.move) 68 | - [mouse.**drag**](#mouse.drag) 69 | - [mouse.**on\_button**](#mouse.on_button) 70 | - [mouse.**on\_click**](#mouse.on_click) 71 | - [mouse.**on\_double\_click**](#mouse.on_double_click) 72 | - [mouse.**on\_right\_click**](#mouse.on_right_click) 73 | - [mouse.**on\_middle\_click**](#mouse.on_middle_click) 74 | - [mouse.**wait**](#mouse.wait) 75 | - [mouse.**get\_position**](#mouse.get_position) 76 | - [mouse.**hook**](#mouse.hook) 77 | - [mouse.**unhook**](#mouse.unhook) 78 | - [mouse.**unhook\_all**](#mouse.unhook_all) 79 | - [mouse.**record**](#mouse.record) 80 | - [mouse.**play**](#mouse.play) *(aliases: `replay`)* 81 | 82 | 83 | 84 | 85 | ## class mouse.**ButtonEvent** 86 | 87 | ButtonEvent(event_type, button, time) 88 | 89 | 90 | 91 | 92 | ### ButtonEvent.**button** 93 | 94 | Alias for field number 1 95 | 96 | 97 | 98 | 99 | ### ButtonEvent.**count**(self, value, /) 100 | 101 | Return number of occurrences of value. 102 | 103 | 104 | 105 | 106 | ### ButtonEvent.**event\_type** 107 | 108 | Alias for field number 0 109 | 110 | 111 | 112 | 113 | ### ButtonEvent.**index**(self, value, start=0, stop=9223372036854775807, /) 114 | 115 | Return first index of value. 116 | 117 | Raises ValueError if the value is not present. 118 | 119 | 120 | 121 | 122 | ### ButtonEvent.**time** 123 | 124 | Alias for field number 2 125 | 126 | 127 | 128 | 129 | 130 | 131 | ## mouse.**DOUBLE** 132 | ```py 133 | = 'double' 134 | ``` 135 | 136 | 137 | 138 | ## mouse.**DOWN** 139 | ```py 140 | = 'down' 141 | ``` 142 | 143 | 144 | 145 | ## mouse.**LEFT** 146 | ```py 147 | = 'left' 148 | ``` 149 | 150 | 151 | 152 | ## mouse.**MIDDLE** 153 | ```py 154 | = 'middle' 155 | ``` 156 | 157 | 158 | 159 | ## class mouse.**MoveEvent** 160 | 161 | MoveEvent(x, y, time) 162 | 163 | 164 | 165 | 166 | ### MoveEvent.**count**(self, value, /) 167 | 168 | Return number of occurrences of value. 169 | 170 | 171 | 172 | 173 | ### MoveEvent.**index**(self, value, start=0, stop=9223372036854775807, /) 174 | 175 | Return first index of value. 176 | 177 | Raises ValueError if the value is not present. 178 | 179 | 180 | 181 | 182 | ### MoveEvent.**time** 183 | 184 | Alias for field number 2 185 | 186 | 187 | 188 | 189 | ### MoveEvent.**x** 190 | 191 | Alias for field number 0 192 | 193 | 194 | 195 | 196 | ### MoveEvent.**y** 197 | 198 | Alias for field number 1 199 | 200 | 201 | 202 | 203 | 204 | 205 | ## mouse.**RIGHT** 206 | ```py 207 | = 'right' 208 | ``` 209 | 210 | 211 | 212 | ## mouse.**UP** 213 | ```py 214 | = 'up' 215 | ``` 216 | 217 | 218 | 219 | ## class mouse.**WheelEvent** 220 | 221 | WheelEvent(delta, time) 222 | 223 | 224 | 225 | 226 | ### WheelEvent.**count**(self, value, /) 227 | 228 | Return number of occurrences of value. 229 | 230 | 231 | 232 | 233 | ### WheelEvent.**delta** 234 | 235 | Alias for field number 0 236 | 237 | 238 | 239 | 240 | ### WheelEvent.**index**(self, value, start=0, stop=9223372036854775807, /) 241 | 242 | Return first index of value. 243 | 244 | Raises ValueError if the value is not present. 245 | 246 | 247 | 248 | 249 | ### WheelEvent.**time** 250 | 251 | Alias for field number 1 252 | 253 | 254 | 255 | 256 | 257 | 258 | ## mouse.**X** 259 | ```py 260 | = 'x' 261 | ``` 262 | 263 | 264 | 265 | ## mouse.**X2** 266 | ```py 267 | = 'x2' 268 | ``` 269 | 270 | 271 | 272 | ## mouse.**version** 273 | ```py 274 | = '0.7.1' 275 | ``` 276 | 277 | 278 | 279 | ## mouse.**is\_pressed**(button='left') 280 | 281 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L78) 282 | 283 | Returns True if the given button is currently pressed. 284 | 285 | 286 | 287 | 288 | ## mouse.**press**(button='left') 289 | 290 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L83) 291 | 292 | Presses the given button (but doesn't release). 293 | 294 | 295 | 296 | 297 | ## mouse.**release**(button='left') 298 | 299 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L87) 300 | 301 | Releases the given button. 302 | 303 | 304 | 305 | 306 | ## mouse.**click**(button='left') 307 | 308 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L91) 309 | 310 | Sends a click with the given button. 311 | 312 | 313 | 314 | 315 | ## mouse.**double\_click**(button='left') 316 | 317 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L96) 318 | 319 | Sends a double click with the given button. 320 | 321 | 322 | 323 | 324 | ## mouse.**right\_click**() 325 | 326 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L101) 327 | 328 | Sends a right click with the given button. 329 | 330 | 331 | 332 | 333 | ## mouse.**wheel**(delta=1) 334 | 335 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L105) 336 | 337 | Scrolls the wheel `delta` clicks. Sign indicates direction. 338 | 339 | 340 | 341 | 342 | ## mouse.**move**(x, y, absolute=True, duration=0, steps_per_second=120.0) 343 | 344 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L109) 345 | 346 | 347 | Moves the mouse. If `absolute`, to position (x, y), otherwise move relative 348 | to the current position. If `duration` is non-zero, animates the movement. 349 | The fps of the animation is determined by 'steps_per_second', default is 120. 350 | 351 | 352 | 353 | 354 | ## mouse.**drag**(start\_x, start\_y, end\_x, end\_y, absolute=True, duration=0) 355 | 356 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L143) 357 | 358 | 359 | Holds the left mouse button, moving from start to end position, then 360 | releases. `absolute` and `duration` are parameters regarding the mouse 361 | movement. 362 | 363 | 364 | 365 | 366 | 367 | ## mouse.**on\_button**(callback, args=(), buttons=('left', 'middle', 'right', 'x', 'x2'), types=('up', 'down', 'double')) 368 | 369 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L156) 370 | 371 | Invokes `callback` with `args` when the specified event happens. 372 | 373 | 374 | 375 | 376 | ## mouse.**on\_click**(callback, args=()) 377 | 378 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L170) 379 | 380 | Invokes `callback` with `args` when the left button is clicked. 381 | 382 | 383 | 384 | 385 | ## mouse.**on\_double\_click**(callback, args=()) 386 | 387 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L174) 388 | 389 | 390 | Invokes `callback` with `args` when the left button is double clicked. 391 | 392 | 393 | 394 | 395 | 396 | ## mouse.**on\_right\_click**(callback, args=()) 397 | 398 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L180) 399 | 400 | Invokes `callback` with `args` when the right button is clicked. 401 | 402 | 403 | 404 | 405 | ## mouse.**on\_middle\_click**(callback, args=()) 406 | 407 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L184) 408 | 409 | Invokes `callback` with `args` when the middle button is clicked. 410 | 411 | 412 | 413 | 414 | ## mouse.**wait**(button='left', target\_types=('up', 'down', 'double')) 415 | 416 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L188) 417 | 418 | 419 | Blocks program execution until the given button performs an event. 420 | 421 | 422 | 423 | 424 | 425 | ## mouse.**get\_position**() 426 | 427 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L199) 428 | 429 | Returns the (x, y) mouse position. 430 | 431 | 432 | 433 | 434 | ## mouse.**hook**(callback) 435 | 436 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L203) 437 | 438 | 439 | Installs a global listener on all available mouses, invoking `callback` 440 | each time it is moved, a key status changes or the wheel is spun. A mouse 441 | event is passed as argument, with type either `mouse.ButtonEvent`, 442 | `mouse.WheelEvent` or `mouse.MoveEvent`. 443 | 444 | Returns the given callback for easier development. 445 | 446 | 447 | 448 | 449 | 450 | ## mouse.**unhook**(callback) 451 | 452 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L215) 453 | 454 | 455 | Removes a previously installed hook. 456 | 457 | 458 | 459 | 460 | 461 | ## mouse.**unhook\_all**() 462 | 463 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L221) 464 | 465 | 466 | Removes all hooks registered by this application. Note this may include 467 | hooks installed by high level functions, such as [`record`](#mouse.record). 468 | 469 | 470 | 471 | 472 | 473 | ## mouse.**record**(button='right', target\_types=('down',)) 474 | 475 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L228) 476 | 477 | 478 | Records all mouse events until the user presses the given button. 479 | Then returns the list of events recorded. Pairs well with [`play(events)`](#mouse.play). 480 | 481 | Note: this is a blocking function. 482 | Note: for more details on the mouse hook and events see [`hook`](#mouse.hook). 483 | 484 | 485 | 486 | 487 | 488 | ## mouse.**play**(events, speed\_factor=1.0, include\_clicks=True, include\_moves=True, include\_wheel=True) 489 | 490 | [\[source\]](https://github.com/boppreh/mouse/blob/master/mouse/__init__.py#L242) 491 | 492 | 493 | Plays a sequence of recorded events, maintaining the relative time 494 | intervals. If speed_factor is <= 0 then the actions are replayed as fast 495 | as the OS allows. Pairs well with [`record()`](#mouse.record). 496 | 497 | The parameters `include_*` define if events of that type should be included 498 | in the replay or ignored. 499 | -------------------------------------------------------------------------------- /make_release.py: -------------------------------------------------------------------------------- 1 | """ 2 | This little guy streamlines the release process of Python packages. 3 | 4 | By running `python3 make_release.py` it'll do the following tasks automatically: 5 | 6 | - Update README by calling `make_readme.sh` if this file exists. 7 | - Check PyPI RST long_description syntax. 8 | - Show the latest version from CHANGES.md and ask for a new version number. 9 | - Open vim to allow you to edit the list of changes for this new version, showing a list of commits since the last version. 10 | - Prepend your list of changes to CHANGES.md (and ask if you want to commit it now). 11 | - Add a git tag to the current commit. 12 | - Push tag to GitHub. 13 | - Publish a new release to GitHub, asking for the authentication token (optional). 14 | - Publish a new release on PyPI. 15 | 16 | Suggested way to organize your project for a smooth process: 17 | 18 | - Use Markdown everywhere. 19 | - Keep a description of your project in the package's docstring. 20 | - Generate your README from the package docstring plus API docs. 21 | - Convert your package docstring to RST in setup.py and use that as long_description. 22 | - Use raw semantic versioning for CHANGES.md and PyPI (e.g. 2.3.1), and prepend 'v' for git tags and releases (e.g. v2.3.1). 23 | 24 | """ 25 | import re 26 | import sys 27 | import os 28 | from subprocess import run, check_output 29 | import atexit 30 | import requests 31 | import mouse 32 | 33 | run(['make', 'clean', 'build'], check=True) 34 | 35 | assert re.fullmatch(r'\d+\.\d+\.\d+', mouse.version) 36 | last_version = check_output(['git', 'describe', '--abbrev=0'], universal_newlines=True).strip('v\n') 37 | assert mouse.version != last_version, 'Must update mouse.version first.' 38 | 39 | commits = check_output(['git', 'log', 'v{}..HEAD'.format(last_version), '--oneline'], universal_newlines=True) 40 | with open('message.txt', 'w') as message_file: 41 | atexit.register(lambda: os.remove('message.txt')) 42 | 43 | message_file.write('\n\n\n') 44 | message_file.write('# Enter changes one per line like this:\n') 45 | message_file.write('# - Added `foobar`.\n\n\n') 46 | message_file.write('# As a reminder, here\'s the last commits since version {}:\n\n'.format(last_version)) 47 | for line in commits.strip().split('\n'): 48 | message_file.write('# {}\n'.format(line)) 49 | 50 | run(['vim', 'message.txt']) 51 | with open('message.txt') as message_file: 52 | lines = [line for line in message_file.readlines() if not line.startswith('#')] 53 | message = ''.join(lines).strip() 54 | if not message: 55 | print('Aborting release due to empty message.') 56 | exit() 57 | with open('message.txt', 'w') as message_file: 58 | message_file.write(message) 59 | 60 | with open('CHANGES.md') as changes_file: 61 | old_changes = changes_file.read() 62 | with open('CHANGES.md', 'w') as changes_file: 63 | changes_file.write('# {}\n\n{}\n\n\n{}'.format(mouse.version, message, old_changes)) 64 | 65 | 66 | tag_name = 'v' + mouse.version 67 | if input('Commit README.md and CHANGES.md files? ').lower().startswith('y'): 68 | run(['git', 'add', 'CHANGES.md', 'README.md']) 69 | run(['git', 'commit', '-m', 'Update changes for {}'.format(tag_name)]) 70 | run(['git', 'push']) 71 | run(['git', 'tag', '-a', tag_name, '--file', 'message.txt'], check=True) 72 | run(['git', 'push', 'origin', tag_name], check=True) 73 | 74 | token = input('To make a release enter your GitHub repo authorization token: ').strip() 75 | if token: 76 | git_remotes = check_output(['git', 'remote', '-v']).decode('utf-8') 77 | repo_path = re.search(r'github.com[:/](.+?)(?:\.git)? \(push\)', git_remotes).group(1) 78 | releases_url = 'https://api.github.com/repos/{}/releases'.format(repo_path) 79 | print(releases_url) 80 | release = { 81 | "tag_name": tag_name, 82 | "target_commitish": "master", 83 | "name": tag_name, 84 | "body": message, 85 | "draft": False, 86 | "prerelease": False, 87 | } 88 | response = requests.post(releases_url, json=release, headers={'Authorization': 'token ' + token}) 89 | print(response.status_code, response.text) 90 | 91 | run(['twine', 'upload', 'dist/*'], check=True, shell=True) 92 | -------------------------------------------------------------------------------- /mouse/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | mouse 4 | ===== 5 | 6 | Take full control of your mouse with this small Python library. Hook global events, register hotkeys, simulate mouse movement and clicks, and much more. 7 | 8 | _Huge thanks to [Kirill Pavlov](http://kirillpavlov.com/) for donating the package name. If you are looking for the Cheddargetter.com client implementation, [`pip install mouse==0.5.0`](https://pypi.python.org/pypi/mouse/0.5.0)._ 9 | 10 | ## Features 11 | 12 | - Global event hook on all mice devices (captures events regardless of focus). 13 | - **Listen** and **sends** mouse events. 14 | - Works with **Windows** and **Linux** (requires sudo). 15 | - Works with **MacOS** (requires granting accessibility permissions to terminal/python in System Preferences -> Security) 16 | - **Pure Python**, no C modules to be compiled. 17 | - **Zero dependencies** on Windows and Linux. Trivial to install and deploy, just copy the files. 18 | - **Python 2 and 3**. 19 | - Includes **high level API** (e.g. [record](#mouse.record) and [play](#mouse.play). 20 | - Events automatically captured in separate thread, doesn't block main program. 21 | - Tested and documented. 22 | 23 | This program makes no attempt to hide itself, so don't use it for keyloggers. 24 | 25 | ## Usage 26 | 27 | Install the [PyPI package](https://pypi.python.org/pypi/mouse/): 28 | 29 | $ sudo pip install mouse 30 | 31 | or clone the repository (no installation required, source files are sufficient): 32 | 33 | $ git clone https://github.com/boppreh/mouse 34 | 35 | Then check the [API docs](https://github.com/boppreh/mouse#api) to see what features are available. 36 | 37 | 38 | ## Known limitations: 39 | 40 | - Events generated under Windows don't report device id (`event.device == None`). [#21](https://github.com/boppreh/keyboard/issues/21) 41 | - To avoid depending on X the Linux parts reads raw device files (`/dev/input/input*`) but this requires root. 42 | - Other applications, such as some games, may register hooks that swallow all key events. In this case `mouse` will be unable to report events. 43 | """ 44 | # TODO 45 | # - infinite wait 46 | # - mouse.on_move 47 | version = '0.7.1' 48 | 49 | import time as _time 50 | 51 | import platform as _platform 52 | if _platform.system() == 'Windows': 53 | from. import _winmouse as _os_mouse 54 | elif _platform.system() == 'Linux': 55 | from. import _nixmouse as _os_mouse 56 | elif _platform.system() == 'Darwin': 57 | from. import _darwinmouse as _os_mouse 58 | else: 59 | raise OSError("Unsupported platform '{}'".format(_platform.system())) 60 | 61 | from ._mouse_event import ButtonEvent, MoveEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE 62 | from ._generic import GenericListener as _GenericListener 63 | 64 | _pressed_events = set() 65 | class _MouseListener(_GenericListener): 66 | def init(self): 67 | _os_mouse.init() 68 | def pre_process_event(self, event): 69 | if isinstance(event, ButtonEvent): 70 | if event.event_type in (UP, DOUBLE): 71 | _pressed_events.discard(event.button) 72 | else: 73 | _pressed_events.add(event.button) 74 | return True 75 | 76 | def listen(self): 77 | _os_mouse.listen(self.queue) 78 | 79 | _listener = _MouseListener() 80 | 81 | def is_pressed(button=LEFT): 82 | """ Returns True if the given button is currently pressed. """ 83 | _listener.start_if_necessary() 84 | return button in _pressed_events 85 | 86 | def press(button=LEFT): 87 | """ Presses the given button (but doesn't release). """ 88 | _os_mouse.press(button) 89 | 90 | def release(button=LEFT): 91 | """ Releases the given button. """ 92 | _os_mouse.release(button) 93 | 94 | def click(button=LEFT): 95 | """ Sends a click with the given button. """ 96 | _os_mouse.press(button) 97 | _os_mouse.release(button) 98 | 99 | def double_click(button=LEFT): 100 | """ Sends a double click with the given button. """ 101 | click(button) 102 | click(button) 103 | 104 | def right_click(): 105 | """ Sends a right click with the given button. """ 106 | click(RIGHT) 107 | 108 | def wheel(delta=1): 109 | """ Scrolls the wheel `delta` clicks. Sign indicates direction. """ 110 | _os_mouse.wheel(delta) 111 | 112 | def move(x, y, absolute=True, duration=0, steps_per_second=120.0): 113 | """ 114 | Moves the mouse. If `absolute`, to position (x, y), otherwise move relative 115 | to the current position. If `duration` is non-zero, animates the movement. 116 | The steps_per_second is only an approximation. Due to the internal sleep's 117 | unreliability it cannot be followed strictly. The less its value is, the more 118 | valid the number becomes. 119 | """ 120 | x = int(x) 121 | y = int(y) 122 | 123 | # Requires an extra system call on Linux, but `move_relative` is measured 124 | # in millimeters so we would lose precision. 125 | position_x, position_y = get_position() 126 | 127 | if not absolute: 128 | x = position_x + x 129 | y = position_y + y 130 | 131 | if not duration: 132 | _os_mouse.move_to(x, y) 133 | return 134 | 135 | start_x = position_x 136 | start_y = position_y 137 | dx = x - start_x 138 | dy = y - start_y 139 | 140 | if dx == 0 and dy == 0: 141 | _time.sleep(duration) 142 | return 143 | 144 | interval_time = 1.0/steps_per_second 145 | start_time = _time.perf_counter() 146 | end_time = start_time + float(duration) 147 | step_start_time = start_time 148 | iteration_start_time = start_time 149 | while iteration_start_time < end_time: 150 | # Sleep to enforce the fps cap, considering the last step's duration and remaining time 151 | last_step_duration = iteration_start_time - step_start_time 152 | remaining_time = end_time - iteration_start_time 153 | corrected_sleep_time = interval_time - last_step_duration 154 | actual_sleep_time = min(remaining_time, corrected_sleep_time) 155 | if actual_sleep_time > 0: 156 | _time.sleep(actual_sleep_time) 157 | step_start_time = _time.perf_counter() 158 | 159 | # Move based on the elapsed time to ensure that the duration is valid 160 | current_time = step_start_time - start_time 161 | progress = current_time / duration 162 | _os_mouse.move_to(start_x + dx*progress, start_y + dy*progress) 163 | iteration_start_time = _time.perf_counter() 164 | 165 | # Move to the destination to ensure the final position 166 | _os_mouse.move_to(start_x + dx, start_y + dy) 167 | 168 | def drag(start_x, start_y, end_x, end_y, absolute=True, duration=0): 169 | """ 170 | Holds the left mouse button, moving from start to end position, then 171 | releases. `absolute` and `duration` are parameters regarding the mouse 172 | movement. 173 | """ 174 | if is_pressed(): 175 | release() 176 | move(start_x, start_y, absolute, 0) 177 | press() 178 | move(end_x, end_y, absolute, duration) 179 | release() 180 | 181 | def on_button(callback, args=(), buttons=(LEFT, MIDDLE, RIGHT, X, X2), types=(UP, DOWN, DOUBLE)): 182 | """ Invokes `callback` with `args` when the specified event happens. """ 183 | if not isinstance(buttons, (tuple, list)): 184 | buttons = (buttons,) 185 | if not isinstance(types, (tuple, list)): 186 | types = (types,) 187 | 188 | def handler(event): 189 | if isinstance(event, ButtonEvent): 190 | if event.event_type in types and event.button in buttons: 191 | callback(*args) 192 | _listener.add_handler(handler) 193 | return handler 194 | 195 | def on_pressed(callback, args=()): 196 | """ Invokes `callback` with `args` when the left button is pressed. """ 197 | return on_button(callback, args, [LEFT], [DOWN]) 198 | 199 | def on_click(callback, args=()): 200 | """ Invokes `callback` with `args` when the left button is clicked. """ 201 | return on_button(callback, args, [LEFT], [UP]) 202 | 203 | def on_double_click(callback, args=()): 204 | """ 205 | Invokes `callback` with `args` when the left button is double clicked. 206 | """ 207 | return on_button(callback, args, [LEFT], [DOUBLE]) 208 | 209 | def on_middle_double_click(callback, args=()): 210 | """ 211 | Invokes `callback` with `args` when the left button is double clicked. 212 | """ 213 | return on_button(callback, args, [MIDDLE], [DOUBLE]) 214 | 215 | 216 | 217 | def on_right_click(callback, args=()): 218 | """ Invokes `callback` with `args` when the right button is clicked. """ 219 | return on_button(callback, args, [RIGHT], [UP]) 220 | 221 | def on_middle_click(callback, args=()): 222 | """ Invokes `callback` with `args` when the middle button is clicked. """ 223 | return on_button(callback, args, [MIDDLE], [UP]) 224 | 225 | def wait(button=LEFT, target_types=(UP, DOWN, DOUBLE)): 226 | """ 227 | Blocks program execution until the given button performs an event. 228 | """ 229 | from threading import Lock 230 | lock = Lock() 231 | lock.acquire() 232 | handler = on_button(lock.release, (), [button], target_types) 233 | lock.acquire() 234 | _listener.remove_handler(handler) 235 | 236 | def get_position(): 237 | """ Returns the (x, y) mouse position. """ 238 | return _os_mouse.get_position() 239 | 240 | def hook(callback): 241 | """ 242 | Installs a global listener on all available mouses, invoking `callback` 243 | each time it is moved, a key status changes or the wheel is spun. A mouse 244 | event is passed as argument, with type either `mouse.ButtonEvent`, 245 | `mouse.WheelEvent` or `mouse.MoveEvent`. 246 | 247 | Returns the given callback for easier development. 248 | """ 249 | _listener.add_handler(callback) 250 | return callback 251 | 252 | def unhook(callback): 253 | """ 254 | Removes a previously installed hook. 255 | """ 256 | _listener.remove_handler(callback) 257 | 258 | def unhook_all(): 259 | """ 260 | Removes all hooks registered by this application. Note this may include 261 | hooks installed by high level functions, such as `record`. 262 | """ 263 | del _listener.handlers[:] 264 | 265 | def record(button=RIGHT, target_types=(DOWN,)): 266 | """ 267 | Records all mouse events until the user presses the given button. 268 | Then returns the list of events recorded. Pairs well with `play(events)`. 269 | 270 | Note: this is a blocking function. 271 | Note: for more details on the mouse hook and events see `hook`. 272 | """ 273 | recorded = [] 274 | hook(recorded.append) 275 | wait(button=button, target_types=target_types) 276 | unhook(recorded.append) 277 | return recorded 278 | 279 | def play(events, speed_factor=1.0, include_clicks=True, include_moves=True, include_wheel=True): 280 | """ 281 | Plays a sequence of recorded events, maintaining the relative time 282 | intervals. If speed_factor is <= 0 then the actions are replayed as fast 283 | as the OS allows. Pairs well with `record()`. 284 | 285 | The parameters `include_*` define if events of that type should be included 286 | in the replay or ignored. 287 | """ 288 | last_time = None 289 | for event in events: 290 | if speed_factor > 0 and last_time is not None: 291 | _time.sleep((event.time - last_time) / speed_factor) 292 | last_time = event.time 293 | 294 | if isinstance(event, ButtonEvent) and include_clicks: 295 | if event.event_type == UP: 296 | _os_mouse.release(event.button) 297 | else: 298 | _os_mouse.press(event.button) 299 | elif isinstance(event, MoveEvent) and include_moves: 300 | _os_mouse.move_to(event.x, event.y) 301 | elif isinstance(event, WheelEvent) and include_wheel: 302 | _os_mouse.wheel(event.delta) 303 | 304 | replay = play 305 | hold = press 306 | 307 | if __name__ == '__main__': 308 | print('Recording... Double click to stop and replay.') 309 | play(record()) 310 | -------------------------------------------------------------------------------- /mouse/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import mouse 3 | import fileinput 4 | import json 5 | import sys 6 | 7 | class_by_name = { 8 | 'ButtonEvent': mouse.ButtonEvent, 9 | 'WheelEvent': mouse.WheelEvent, 10 | 'MoveEvent': mouse.MoveEvent, 11 | } 12 | 13 | def print_event_json(event): 14 | # Could use json.dumps(event.__dict__()), but this way we guarantee semantic order. 15 | d = event._asdict() 16 | d['event_class'] = event.__class__.__name__ 17 | print(json.dumps(d)) 18 | sys.stdout.flush() 19 | mouse.hook(print_event_json) 20 | 21 | def load(line): 22 | d = json.loads(line) 23 | class_ = class_by_name[d['event_class']] 24 | del d['event_class'] 25 | return class_(**d) 26 | 27 | mouse.play(load(line) for line in fileinput.input()) -------------------------------------------------------------------------------- /mouse/_darwinmouse.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | import threading 4 | import time 5 | 6 | import Quartz 7 | 8 | from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN 9 | 10 | 11 | _button_mapping = { 12 | LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged), 13 | RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged), 14 | MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged) 15 | } 16 | _button_state = { 17 | LEFT: False, 18 | RIGHT: False, 19 | MIDDLE: False 20 | } 21 | _last_click = { 22 | "time": None, 23 | "button": None, 24 | "position": None, 25 | "click_count": 0 26 | } 27 | _mouse_button_mapping = { 28 | Quartz.kCGEventLeftMouseDown: LEFT, 29 | Quartz.kCGEventLeftMouseUp: LEFT, 30 | Quartz.kCGEventRightMouseDown: RIGHT, 31 | Quartz.kCGEventRightMouseUp: RIGHT, 32 | 1026: MIDDLE 33 | } 34 | _click_down_events = [Quartz.kCGEventLeftMouseDown, Quartz.kCGEventRightMouseDown, Quartz.kCGEventOtherMouseDown] 35 | _click_up_events = [Quartz.kCGEventLeftMouseUp, Quartz.kCGEventRightMouseUp, Quartz.kCGEventOtherMouseUp] 36 | 37 | 38 | class MouseEventListener(object): 39 | def __init__(self, callback, blocking=False): 40 | self.blocking = blocking 41 | self.callback = callback 42 | self.listening = True 43 | 44 | def run(self): 45 | """ Creates a listener and loops while waiting for an event. Intended to run as 46 | a background thread. """ 47 | self.tap = Quartz.CGEventTapCreate( 48 | Quartz.kCGSessionEventTap, 49 | Quartz.kCGHeadInsertEventTap, 50 | Quartz.kCGEventTapOptionDefault, 51 | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) | 52 | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) | 53 | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) | 54 | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) | 55 | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) | 56 | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) | 57 | Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) | 58 | Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel), 59 | self.handler, 60 | None) 61 | loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0) 62 | loop = Quartz.CFRunLoopGetCurrent() 63 | Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode) 64 | Quartz.CGEventTapEnable(self.tap, True) 65 | 66 | while self.listening: 67 | Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False) 68 | 69 | def handler(self, proxy, event_type, event, *args): 70 | if event_type in (_click_down_events + _click_up_events): 71 | direction = DOWN if event_type in _click_down_events else UP 72 | 73 | x, y = [int(i) for i in Quartz.CGEventGetLocation(event)] 74 | 75 | if event_type in _mouse_button_mapping: 76 | button = _mouse_button_mapping[event_type] 77 | else: 78 | button_number = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGMouseEventButtonNumber) 79 | 80 | if (button_number + 1024) in _mouse_button_mapping: 81 | button = _mouse_button_mapping[button_number + 1024] 82 | else: 83 | return event 84 | 85 | mouse_event = ButtonEvent(event_type=direction, button=button, time=time.time()) 86 | 87 | elif event_type == Quartz.kCGEventScrollWheel: 88 | x, y = [int(i) for i in Quartz.CGEventGetLocation(event)] 89 | 90 | velocity = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGScrollWheelEventDeltaAxis1) 91 | #direction = UP if velocity > 0 else DOWN 92 | 93 | mouse_event = WheelEvent(delta=velocity, time=time.time()) 94 | 95 | elif event_type == Quartz.kCGEventMouseMoved: 96 | x, y = [int(i) for i in Quartz.CGEventGetLocation(event)] 97 | 98 | mouse_event = MoveEvent(x=x, y=y, time=time.time()) 99 | 100 | else: 101 | return event 102 | 103 | self.callback(mouse_event) 104 | return event 105 | 106 | 107 | # Exports 108 | def init(): 109 | """ Initializes mouse state """ 110 | pass 111 | 112 | def listen(queue): 113 | """ Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """ 114 | # if not os.geteuid() == 0:3 115 | # raise OSError("Error 13 - Must be run as administrator") 116 | # root is not required, just grant accessibility permissions to terminal/python (System Preferences -> Security) 117 | listener = MouseEventListener(lambda e: queue.put(e)) 118 | t = threading.Thread(target=listener.run, args=()) 119 | t.daemon = True 120 | t.start() 121 | 122 | def press(button=LEFT): 123 | """ Sends a down event for the specified button, using the provided constants """ 124 | location = get_position() 125 | button_code, button_down, _, _ = _button_mapping[button] 126 | e = Quartz.CGEventCreateMouseEvent( 127 | None, 128 | button_down, 129 | location, 130 | button_code) 131 | 132 | # Check if this is a double-click (same location within the last 300ms) 133 | if _last_click["time"] is not None and datetime.datetime.now() - _last_click["time"] < datetime.timedelta(seconds=0.3) and _last_click["button"] == button and _last_click["position"] == location: 134 | # Repeated Click 135 | _last_click["click_count"] = min(3, _last_click["click_count"]+1) 136 | else: 137 | # Not a double-click - Reset last click 138 | _last_click["click_count"] = 1 139 | Quartz.CGEventSetIntegerValueField( 140 | e, 141 | Quartz.kCGMouseEventClickState, 142 | _last_click["click_count"]) 143 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) 144 | _button_state[button] = True 145 | _last_click["time"] = datetime.datetime.now() 146 | _last_click["button"] = button 147 | _last_click["position"] = location 148 | 149 | def release(button=LEFT): 150 | """ Sends an up event for the specified button, using the provided constants """ 151 | location = get_position() 152 | button_code, _, button_up, _ = _button_mapping[button] 153 | e = Quartz.CGEventCreateMouseEvent( 154 | None, 155 | button_up, 156 | location, 157 | button_code) 158 | 159 | if _last_click["time"] is not None and _last_click["time"] > datetime.datetime.now() - datetime.timedelta(microseconds=300000) and _last_click["button"] == button and _last_click["position"] == location: 160 | # Repeated Click 161 | Quartz.CGEventSetIntegerValueField( 162 | e, 163 | Quartz.kCGMouseEventClickState, 164 | _last_click["click_count"]) 165 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) 166 | _button_state[button] = False 167 | 168 | def wheel(delta=1): 169 | """ Sends a wheel event for the provided number of clicks. May be negative to reverse 170 | direction. """ 171 | location = get_position() 172 | e = Quartz.CGEventCreateMouseEvent( 173 | None, 174 | Quartz.kCGEventScrollWheel, 175 | location, 176 | Quartz.kCGMouseButtonLeft) 177 | e2 = Quartz.CGEventCreateScrollWheelEvent( 178 | None, 179 | Quartz.kCGScrollEventUnitLine, 180 | 1, 181 | delta) 182 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) 183 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2) 184 | 185 | def __wheel(self, dy=1, dx=0): 186 | #print('alternative scroll ..') 187 | dx = int(dx) 188 | dy = int(dy) 189 | speed = 5 190 | 191 | while dx != 0 or dy != 0: 192 | xval = 1 if dx > 0 else -1 if dx < 0 else 0 193 | dx -= xval 194 | yval = 1 if dy > 0 else -1 if dy < 0 else 0 195 | dy -= yval 196 | 197 | Quartz.CGEventPost( 198 | Quartz.kCGHIDEventTap, 199 | Quartz.CGEventCreateScrollWheelEvent( 200 | None, 201 | Quartz.kCGScrollEventUnitPixel, 202 | 2, 203 | yval * speed, 204 | xval * speed 205 | ) 206 | ) 207 | 208 | def move_to(x, y): 209 | """ Sets the mouse's location to the specified coordinates. """ 210 | for b in _button_state: 211 | if _button_state[b]: 212 | e = Quartz.CGEventCreateMouseEvent( 213 | None, 214 | _button_mapping[b][3], # Drag Event 215 | (x, y), 216 | _button_mapping[b][0]) 217 | break 218 | else: 219 | e = Quartz.CGEventCreateMouseEvent( 220 | None, 221 | Quartz.kCGEventMouseMoved, 222 | (x, y), 223 | Quartz.kCGMouseButtonLeft) 224 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) 225 | 226 | def get_position(): 227 | """ Returns the mouse's location as a tuple of (x, y). """ 228 | e = Quartz.CGEventCreate(None) 229 | point = Quartz.CGEventGetLocation(e) 230 | return (point.x, point.y) 231 | -------------------------------------------------------------------------------- /mouse/_generic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from threading import Thread, Lock 3 | import traceback 4 | import functools 5 | 6 | try: 7 | from queue import Queue 8 | except ImportError: 9 | from Queue import Queue 10 | 11 | class GenericListener(object): 12 | lock = Lock() 13 | 14 | def __init__(self): 15 | self.handlers = [] 16 | self.listening = False 17 | self.queue = Queue() 18 | 19 | def invoke_handlers(self, event): 20 | for handler in self.handlers: 21 | try: 22 | if handler(event): 23 | # Stop processing this hotkey. 24 | return 1 25 | except Exception as e: 26 | traceback.print_exc() 27 | 28 | def start_if_necessary(self): 29 | """ 30 | Starts the listening thread if it wasn't already. 31 | """ 32 | self.lock.acquire() 33 | try: 34 | if not self.listening: 35 | self.init() 36 | 37 | self.listening = True 38 | self.listening_thread = Thread(target=self.listen) 39 | self.listening_thread.daemon = True 40 | self.listening_thread.start() 41 | 42 | self.processing_thread = Thread(target=self.process) 43 | self.processing_thread.daemon = True 44 | self.processing_thread.start() 45 | finally: 46 | self.lock.release() 47 | 48 | def pre_process_event(self, event): 49 | raise NotImplementedError('This method should be implemented in the child class.') 50 | 51 | def process(self): 52 | """ 53 | Loops over the underlying queue of events and processes them in order. 54 | """ 55 | assert self.queue is not None 56 | while True: 57 | event = self.queue.get() 58 | if self.pre_process_event(event): 59 | self.invoke_handlers(event) 60 | self.queue.task_done() 61 | 62 | def add_handler(self, handler): 63 | """ 64 | Adds a function to receive each event captured, starting the capturing 65 | process if necessary. 66 | """ 67 | self.start_if_necessary() 68 | self.handlers.append(handler) 69 | 70 | def remove_handler(self, handler): 71 | """ Removes a previously added event handler. """ 72 | self.handlers.remove(handler) 73 | -------------------------------------------------------------------------------- /mouse/_mouse_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import namedtuple 3 | 4 | LEFT = 'left' 5 | RIGHT = 'right' 6 | MIDDLE = 'middle' 7 | WHEEL = 'wheel' 8 | X = 'x' 9 | X2 = 'x2' 10 | 11 | UP = 'up' 12 | DOWN = 'down' 13 | DOUBLE = 'double' 14 | VERTICAL = 'vertical' 15 | HORIZONTAL = 'horizontal' 16 | 17 | 18 | ButtonEvent = namedtuple('ButtonEvent', ['event_type', 'button', 'time']) 19 | WheelEvent = namedtuple('WheelEvent', ['delta', 'time']) 20 | MoveEvent = namedtuple('MoveEvent', ['x', 'y', 'time']) 21 | -------------------------------------------------------------------------------- /mouse/_mouse_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import time 4 | 5 | from ._mouse_event import MoveEvent, ButtonEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE 6 | import mouse 7 | 8 | class FakeOsMouse(object): 9 | def __init__(self): 10 | self.append = None 11 | self.position = (0, 0) 12 | self.queue = None 13 | self.init = lambda: None 14 | 15 | def listen(self, queue): 16 | self.listening = True 17 | self.queue = queue 18 | 19 | def press(self, button): 20 | self.append((DOWN, button)) 21 | 22 | def release(self, button): 23 | self.append((UP, button)) 24 | 25 | def get_position(self): 26 | return self.position 27 | 28 | def move_to(self, x, y): 29 | self.append(('move', (x, y))) 30 | self.position = (x, y) 31 | 32 | def wheel(self, delta): 33 | self.append(('wheel', delta)) 34 | 35 | def move_relative(self, x, y): 36 | self.position = (self.position[0] + x, self.position[1] + y) 37 | 38 | class TestMouse(unittest.TestCase): 39 | @staticmethod 40 | def setUpClass(): 41 | mouse._os_mouse= FakeOsMouse() 42 | mouse._listener.start_if_necessary() 43 | assert mouse._os_mouse.listening 44 | 45 | def setUp(self): 46 | self.events = [] 47 | mouse._pressed_events.clear() 48 | mouse._os_mouse.append = self.events.append 49 | 50 | def tearDown(self): 51 | mouse.unhook_all() 52 | # Make sure there's no spill over between tests. 53 | self.wait_for_events_queue() 54 | 55 | def wait_for_events_queue(self): 56 | mouse._listener.queue.join() 57 | 58 | def flush_events(self): 59 | self.wait_for_events_queue() 60 | events = list(self.events) 61 | # Ugly, but required to work in Python2. Python3 has list.clear 62 | del self.events[:] 63 | return events 64 | 65 | def press(self, button=LEFT): 66 | mouse._os_mouse.queue.put(ButtonEvent(DOWN, button, time.time())) 67 | self.wait_for_events_queue() 68 | 69 | def release(self, button=LEFT): 70 | mouse._os_mouse.queue.put(ButtonEvent(UP, button, time.time())) 71 | self.wait_for_events_queue() 72 | 73 | def double_click(self, button=LEFT): 74 | mouse._os_mouse.queue.put(ButtonEvent(DOUBLE, button, time.time())) 75 | self.wait_for_events_queue() 76 | 77 | def click(self, button=LEFT): 78 | self.press(button) 79 | self.release(button) 80 | 81 | def wheel(self, delta=1): 82 | mouse._os_mouse.queue.put(WheelEvent(delta, time.time())) 83 | self.wait_for_events_queue() 84 | 85 | def move(self, x=0, y=0): 86 | mouse._os_mouse.queue.put(MoveEvent(x, y, time.time())) 87 | self.wait_for_events_queue() 88 | 89 | def test_hook(self): 90 | events = [] 91 | self.press() 92 | mouse.hook(events.append) 93 | self.press() 94 | mouse.unhook(events.append) 95 | self.press() 96 | self.assertEqual(len(events), 1) 97 | 98 | def test_is_pressed(self): 99 | self.assertFalse(mouse.is_pressed()) 100 | self.press() 101 | self.assertTrue(mouse.is_pressed()) 102 | self.release() 103 | self.press(X2) 104 | self.assertFalse(mouse.is_pressed()) 105 | 106 | self.assertTrue(mouse.is_pressed(X2)) 107 | self.press(X2) 108 | self.assertTrue(mouse.is_pressed(X2)) 109 | self.release(X2) 110 | self.release(X2) 111 | self.assertFalse(mouse.is_pressed(X2)) 112 | 113 | def test_buttons(self): 114 | mouse.press() 115 | self.assertEqual(self.flush_events(), [(DOWN, LEFT)]) 116 | mouse.release() 117 | self.assertEqual(self.flush_events(), [(UP, LEFT)]) 118 | mouse.click() 119 | self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT)]) 120 | mouse.double_click() 121 | self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT), (DOWN, LEFT), (UP, LEFT)]) 122 | mouse.right_click() 123 | self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)]) 124 | mouse.click(RIGHT) 125 | self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)]) 126 | mouse.press(X2) 127 | self.assertEqual(self.flush_events(), [(DOWN, X2)]) 128 | 129 | def test_position(self): 130 | self.assertEqual(mouse.get_position(), mouse._os_mouse.get_position()) 131 | 132 | def test_move(self): 133 | mouse.move(0, 0) 134 | self.assertEqual(mouse._os_mouse.get_position(), (0, 0)) 135 | mouse.move(100, 500) 136 | self.assertEqual(mouse._os_mouse.get_position(), (100, 500)) 137 | mouse.move(1, 2, False) 138 | self.assertEqual(mouse._os_mouse.get_position(), (101, 502)) 139 | 140 | mouse.move(0, 0) 141 | mouse.move(100, 499, True, duration=0.01) 142 | self.assertEqual(mouse._os_mouse.get_position(), (100, 499)) 143 | mouse.move(100, 1, False, duration=0.01) 144 | self.assertEqual(mouse._os_mouse.get_position(), (200, 500)) 145 | mouse.move(0, 0, False, duration=0.01) 146 | self.assertEqual(mouse._os_mouse.get_position(), (200, 500)) 147 | 148 | def triggers(self, fn, events, **kwargs): 149 | self.triggered = False 150 | def callback(): 151 | self.triggered = True 152 | handler = fn(callback, **kwargs) 153 | 154 | for event_type, arg in events: 155 | if event_type == DOWN: 156 | self.press(arg) 157 | elif event_type == UP: 158 | self.release(arg) 159 | elif event_type == DOUBLE: 160 | self.double_click(arg) 161 | elif event_type == 'WHEEL': 162 | self.wheel() 163 | 164 | mouse._listener.remove_handler(handler) 165 | return self.triggered 166 | 167 | def test_on_button(self): 168 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)])) 169 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, RIGHT)])) 170 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, X)])) 171 | 172 | self.assertFalse(self.triggers(mouse.on_button, [('WHEEL', '')])) 173 | 174 | self.assertFalse(self.triggers(mouse.on_button, [(DOWN, X)], buttons=MIDDLE)) 175 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE)) 176 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE)) 177 | self.assertFalse(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE, types=UP)) 178 | self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=MIDDLE, types=UP)) 179 | 180 | self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) 181 | self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) 182 | self.assertFalse(self.triggers(mouse.on_button, [(UP, X)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) 183 | 184 | def test_ons(self): 185 | self.assertTrue(self.triggers(mouse.on_click, [(UP, LEFT)])) 186 | self.assertFalse(self.triggers(mouse.on_click, [(UP, RIGHT)])) 187 | self.assertFalse(self.triggers(mouse.on_click, [(DOWN, LEFT)])) 188 | self.assertFalse(self.triggers(mouse.on_click, [(DOWN, RIGHT)])) 189 | 190 | self.assertTrue(self.triggers(mouse.on_double_click, [(DOUBLE, LEFT)])) 191 | self.assertFalse(self.triggers(mouse.on_double_click, [(DOUBLE, RIGHT)])) 192 | self.assertFalse(self.triggers(mouse.on_double_click, [(DOWN, RIGHT)])) 193 | 194 | self.assertTrue(self.triggers(mouse.on_right_click, [(UP, RIGHT)])) 195 | self.assertTrue(self.triggers(mouse.on_middle_click, [(UP, MIDDLE)])) 196 | 197 | def test_wait(self): 198 | # If this fails it blocks. Unfortunately, but I see no other way of testing. 199 | from threading import Thread, Lock 200 | lock = Lock() 201 | lock.acquire() 202 | def t(): 203 | mouse.wait() 204 | lock.release() 205 | Thread(target=t).start() 206 | self.press() 207 | lock.acquire() 208 | 209 | def test_record_play(self): 210 | from threading import Thread, Lock 211 | lock = Lock() 212 | lock.acquire() 213 | def t(): 214 | self.recorded = mouse.record(RIGHT) 215 | lock.release() 216 | Thread(target=t).start() 217 | self.click() 218 | self.wheel(5) 219 | self.move(100, 50) 220 | self.press(RIGHT) 221 | lock.acquire() 222 | 223 | self.assertEqual(len(self.recorded), 5) 224 | self.assertEqual(self.recorded[0]._replace(time=None), ButtonEvent(DOWN, LEFT, None)) 225 | self.assertEqual(self.recorded[1]._replace(time=None), ButtonEvent(UP, LEFT, None)) 226 | self.assertEqual(self.recorded[2]._replace(time=None), WheelEvent(5, None)) 227 | self.assertEqual(self.recorded[3]._replace(time=None), MoveEvent(100, 50, None)) 228 | self.assertEqual(self.recorded[4]._replace(time=None), ButtonEvent(DOWN, RIGHT, None)) 229 | 230 | mouse.play(self.recorded, speed_factor=0) 231 | events = self.flush_events() 232 | self.assertEqual(len(events), 5) 233 | self.assertEqual(events[0], (DOWN, LEFT)) 234 | self.assertEqual(events[1], (UP, LEFT)) 235 | self.assertEqual(events[2], ('wheel', 5)) 236 | self.assertEqual(events[3], ('move', (100, 50))) 237 | self.assertEqual(events[4], (DOWN, RIGHT)) 238 | 239 | mouse.play(self.recorded) 240 | events = self.flush_events() 241 | self.assertEqual(len(events), 5) 242 | self.assertEqual(events[0], (DOWN, LEFT)) 243 | self.assertEqual(events[1], (UP, LEFT)) 244 | self.assertEqual(events[2], ('wheel', 5)) 245 | self.assertEqual(events[3], ('move', (100, 50))) 246 | self.assertEqual(events[4], (DOWN, RIGHT)) 247 | 248 | mouse.play(self.recorded, include_clicks=False) 249 | events = self.flush_events() 250 | self.assertEqual(len(events), 2) 251 | self.assertEqual(events[0], ('wheel', 5)) 252 | self.assertEqual(events[1], ('move', (100, 50))) 253 | 254 | mouse.play(self.recorded, include_moves=False) 255 | events = self.flush_events() 256 | self.assertEqual(len(events), 4) 257 | self.assertEqual(events[0], (DOWN, LEFT)) 258 | self.assertEqual(events[1], (UP, LEFT)) 259 | self.assertEqual(events[2], ('wheel', 5)) 260 | self.assertEqual(events[3], (DOWN, RIGHT)) 261 | 262 | mouse.play(self.recorded, include_wheel=False) 263 | events = self.flush_events() 264 | self.assertEqual(len(events), 4) 265 | self.assertEqual(events[0], (DOWN, LEFT)) 266 | self.assertEqual(events[1], (UP, LEFT)) 267 | self.assertEqual(events[2], ('move', (100, 50))) 268 | self.assertEqual(events[3], (DOWN, RIGHT)) 269 | 270 | if __name__ == '__main__': 271 | unittest.main() 272 | -------------------------------------------------------------------------------- /mouse/_nixcommon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import struct 3 | import os 4 | import atexit 5 | from time import time as now 6 | from threading import Thread 7 | from glob import glob 8 | try: 9 | from queue import Queue 10 | except ImportError: 11 | from Queue import Queue 12 | 13 | event_bin_format = 'llHHI' 14 | 15 | # Taken from include/linux/input.h 16 | # https://www.kernel.org/doc/Documentation/input/event-codes.txt 17 | EV_SYN = 0x00 18 | EV_KEY = 0x01 19 | EV_REL = 0x02 20 | EV_ABS = 0x03 21 | EV_MSC = 0x04 22 | 23 | INVALID_ARGUMENT_ERRNO = 22 24 | 25 | def make_uinput(): 26 | import fcntl, struct 27 | 28 | # Requires uinput driver, but it's usually available. 29 | uinput = open("/dev/uinput", 'wb') 30 | UI_SET_EVBIT = 0x40045564 31 | fcntl.ioctl(uinput, UI_SET_EVBIT, EV_KEY) 32 | 33 | UI_SET_KEYBIT = 0x40045565 34 | try: 35 | for i in range(0x300): 36 | fcntl.ioctl(uinput, UI_SET_KEYBIT, i) 37 | except OSError as e: 38 | if e.errno != INVALID_ARGUMENT_ERRNO: 39 | raise e 40 | 41 | BUS_USB = 0x03 42 | uinput_user_dev = "80sHHHHi64i64i64i64i" 43 | axis = [0] * 64 * 4 44 | uinput.write(struct.pack(uinput_user_dev, b"Virtual Keyboard", BUS_USB, 1, 1, 1, 0, *axis)) 45 | uinput.flush() # Without this you may get Errno 22: Invalid argument. 46 | 47 | UI_DEV_CREATE = 0x5501 48 | fcntl.ioctl(uinput, UI_DEV_CREATE) 49 | UI_DEV_DESTROY = 0x5502 50 | #fcntl.ioctl(uinput, UI_DEV_DESTROY) 51 | 52 | return uinput 53 | 54 | class EventDevice(object): 55 | def __init__(self, path): 56 | self.path = path 57 | self._input_file = None 58 | self._output_file = None 59 | 60 | @property 61 | def input_file(self): 62 | if self._input_file is None: 63 | try: 64 | self._input_file = open(self.path, 'rb') 65 | except IOError as e: 66 | if e.strerror == 'Permission denied': 67 | print('Permission denied ({}). You must be sudo to access global events.'.format(self.path)) 68 | exit() 69 | 70 | def try_close(): 71 | try: 72 | self._input_file.close 73 | except: 74 | pass 75 | atexit.register(try_close) 76 | return self._input_file 77 | 78 | @property 79 | def output_file(self): 80 | if self._output_file is None: 81 | self._output_file = open(self.path, 'wb') 82 | atexit.register(self._output_file.close) 83 | return self._output_file 84 | 85 | def read_event(self): 86 | data = self.input_file.read(struct.calcsize(event_bin_format)) 87 | seconds, microseconds, type, code, value = struct.unpack(event_bin_format, data) 88 | return seconds + microseconds / 1e6, type, code, value, self.path 89 | 90 | def write_event(self, type, code, value): 91 | integer, fraction = divmod(now(), 1) 92 | seconds = int(integer) 93 | microseconds = int(fraction * 1e6) 94 | data_event = struct.pack(event_bin_format, seconds, microseconds, type, code, value) 95 | 96 | # Send a sync event to ensure other programs update. 97 | sync_event = struct.pack(event_bin_format, seconds, microseconds, EV_SYN, 0, 0) 98 | 99 | self.output_file.write(data_event + sync_event) 100 | self.output_file.flush() 101 | 102 | class AggregatedEventDevice(object): 103 | def __init__(self, devices, output=None): 104 | self.event_queue = Queue() 105 | self.devices = devices 106 | self.output = output or self.devices[0] 107 | def start_reading(device): 108 | while True: 109 | self.event_queue.put(device.read_event()) 110 | for device in self.devices: 111 | thread = Thread(target=start_reading, args=[device]) 112 | thread.setDaemon(True) 113 | thread.start() 114 | 115 | def read_event(self): 116 | return self.event_queue.get(block=True) 117 | 118 | def write_event(self, type, code, value): 119 | self.output.write_event(type, code, value) 120 | 121 | import re 122 | from collections import namedtuple 123 | DeviceDescription = namedtuple('DeviceDescription', 'event_file is_mouse is_keyboard') 124 | device_pattern = r"""N: Name="([^"]+?)".+?H: Handlers=([^\n]+)""" 125 | def list_devices_from_proc(type_name): 126 | try: 127 | with open('/proc/bus/input/devices') as f: 128 | description = f.read() 129 | except FileNotFoundError: 130 | return 131 | 132 | devices = {} 133 | for name, handlers in re.findall(device_pattern, description, re.DOTALL): 134 | path = '/dev/input/event' + re.search(r'event(\d+)', handlers).group(1) 135 | if type_name in handlers: 136 | yield EventDevice(path) 137 | 138 | def list_devices_from_by_id(type_name): 139 | for path in glob('/dev/input/by-id/*-event-' + type_name): 140 | yield EventDevice(path) 141 | 142 | def aggregate_devices(type_name): 143 | # Some systems have multiple keyboards with different range of allowed keys 144 | # on each one, like a notebook with a "keyboard" device exclusive for the 145 | # power button. Instead of figuring out which keyboard allows which key to 146 | # send events, we create a fake device and send all events through there. 147 | uinput = make_uinput() 148 | fake_device = EventDevice('uinput Fake Device') 149 | fake_device._input_file = uinput 150 | fake_device._output_file = uinput 151 | 152 | # We don't aggregate devices from different sources to avoid 153 | # duplicates. 154 | 155 | devices_from_proc = list(list_devices_from_proc(type_name)) 156 | if devices_from_proc: 157 | return AggregatedEventDevice(devices_from_proc, output=fake_device) 158 | 159 | # breaks on mouse for virtualbox 160 | # was getting /dev/input/by-id/usb-VirtualBox_USB_Tablet-event-mouse 161 | devices_from_by_id = list(list_devices_from_by_id(type_name)) 162 | if devices_from_by_id: 163 | return AggregatedEventDevice(devices_from_by_id, output=fake_device) 164 | 165 | # If no keyboards were found we can only use the fake device to send keys. 166 | return fake_device 167 | 168 | 169 | def ensure_root(): 170 | if os.geteuid() != 0: 171 | raise ImportError('You must be root to use this library on linux.') 172 | -------------------------------------------------------------------------------- /mouse/_nixmouse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import struct 3 | from subprocess import check_output 4 | import re 5 | from ._nixcommon import EV_KEY, EV_REL, EV_MSC, EV_SYN, EV_ABS, aggregate_devices, ensure_root 6 | from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN 7 | 8 | import ctypes 9 | import ctypes.util 10 | from ctypes import c_uint32, c_uint, c_int, c_void_p, byref 11 | 12 | display = None 13 | window = None 14 | x11 = None 15 | def build_display(): 16 | global display, window, x11 17 | if display and window and x11: return 18 | x11 = ctypes.cdll.LoadLibrary(ctypes.util.find_library('X11')) 19 | # Required because we will have multiple threads calling x11, 20 | # such as the listener thread and then main using "move_to". 21 | x11.XInitThreads() 22 | # Explicitly set XOpenDisplay.restype to avoid segfault on 64 bit OS. 23 | # http://stackoverflow.com/questions/35137007/get-mouse-position-on-linux-pure-python 24 | x11.XOpenDisplay.restype = c_void_p 25 | display = c_void_p(x11.XOpenDisplay(0)) 26 | window = x11.XDefaultRootWindow(display) 27 | 28 | def get_position(): 29 | build_display() 30 | root_id, child_id = c_void_p(), c_void_p() 31 | root_x, root_y, win_x, win_y = c_int(), c_int(), c_int(), c_int() 32 | mask = c_uint() 33 | ret = x11.XQueryPointer(display, c_uint32(window), byref(root_id), byref(child_id), 34 | byref(root_x), byref(root_y), 35 | byref(win_x), byref(win_y), byref(mask)) 36 | return root_x.value, root_y.value 37 | 38 | def move_to(x, y): 39 | build_display() 40 | x11.XWarpPointer(display, None, window, 0, 0, 0, 0, x, y) 41 | x11.XFlush(display) 42 | 43 | REL_X = 0x00 44 | REL_Y = 0x01 45 | REL_Z = 0x02 46 | REL_HWHEEL = 0x06 47 | REL_WHEEL = 0x08 48 | 49 | ABS_X = 0x00 50 | ABS_Y = 0x01 51 | 52 | BTN_MOUSE = 0x110 53 | BTN_LEFT = 0x110 54 | BTN_RIGHT = 0x111 55 | BTN_MIDDLE = 0x112 56 | BTN_SIDE = 0x113 57 | BTN_EXTRA = 0x114 58 | 59 | button_by_code = { 60 | BTN_LEFT: LEFT, 61 | BTN_RIGHT: RIGHT, 62 | BTN_MIDDLE: MIDDLE, 63 | BTN_SIDE: X, 64 | BTN_EXTRA: X2, 65 | } 66 | code_by_button = {button: code for code, button in button_by_code.items()} 67 | 68 | device = None 69 | def build_device(): 70 | global device 71 | if device: return 72 | ensure_root() 73 | device = aggregate_devices('mouse') 74 | init = build_device 75 | 76 | def listen(queue): 77 | build_device() 78 | 79 | while True: 80 | time, type, code, value, device_id = device.read_event() 81 | if type == EV_SYN or type == EV_MSC: 82 | continue 83 | 84 | event = None 85 | arg = None 86 | 87 | if type == EV_KEY: 88 | event = ButtonEvent(DOWN if value else UP, button_by_code.get(code, '?'), time) 89 | elif type == EV_REL: 90 | value, = struct.unpack('i', struct.pack('I', value)) 91 | 92 | if code == REL_WHEEL: 93 | event = WheelEvent(value, time) 94 | elif code in (REL_X, REL_Y): 95 | x, y = get_position() 96 | event = MoveEvent(x, y, time) 97 | 98 | if event is None: 99 | # Unknown event type. 100 | continue 101 | 102 | queue.put(event) 103 | 104 | def press(button=LEFT): 105 | build_device() 106 | device.write_event(EV_KEY, code_by_button[button], 0x01) 107 | 108 | def release(button=LEFT): 109 | build_device() 110 | device.write_event(EV_KEY, code_by_button[button], 0x00) 111 | 112 | def move_relative(x, y): 113 | build_device() 114 | # Note relative events are not in terms of pixels, but millimeters. 115 | if x < 0: 116 | x += 2**32 117 | if y < 0: 118 | y += 2**32 119 | device.write_event(EV_REL, REL_X, x) 120 | device.write_event(EV_REL, REL_Y, y) 121 | 122 | def wheel(delta=1): 123 | build_device() 124 | if delta < 0: 125 | delta += 2**32 126 | device.write_event(EV_REL, REL_WHEEL, delta) 127 | 128 | 129 | if __name__ == '__main__': 130 | #listen(print) 131 | move_to(100, 200) 132 | -------------------------------------------------------------------------------- /mouse/_winmouse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ctypes 3 | import time 4 | from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, byref, Structure, CFUNCTYPE, POINTER 5 | from ctypes.wintypes import DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM 6 | LPMSG = POINTER(MSG) 7 | 8 | import atexit 9 | 10 | from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE, WHEEL, HORIZONTAL, VERTICAL 11 | 12 | #user32 = ctypes.windll.user32 13 | user32 = ctypes.WinDLL('user32', use_last_error = True) 14 | 15 | class MSLLHOOKSTRUCT(Structure): 16 | _fields_ = [("x", c_long), 17 | ("y", c_long), 18 | ('data', c_int32), 19 | ('reserved', c_int32), 20 | ("flags", DWORD), 21 | ("time", c_int), 22 | ] 23 | 24 | LowLevelMouseProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(MSLLHOOKSTRUCT)) 25 | 26 | SetWindowsHookEx = user32.SetWindowsHookExA 27 | #SetWindowsHookEx.argtypes = [c_int, LowLevelMouseProc, c_int, c_int] 28 | SetWindowsHookEx.restype = HHOOK 29 | 30 | CallNextHookEx = user32.CallNextHookEx 31 | #CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(MSLLHOOKSTRUCT)] 32 | CallNextHookEx.restype = c_int 33 | 34 | UnhookWindowsHookEx = user32.UnhookWindowsHookEx 35 | UnhookWindowsHookEx.argtypes = [HHOOK] 36 | UnhookWindowsHookEx.restype = BOOL 37 | 38 | GetMessage = user32.GetMessageW 39 | GetMessage.argtypes = [LPMSG, c_int, c_int, c_int] 40 | GetMessage.restype = BOOL 41 | 42 | TranslateMessage = user32.TranslateMessage 43 | TranslateMessage.argtypes = [LPMSG] 44 | TranslateMessage.restype = BOOL 45 | 46 | DispatchMessage = user32.DispatchMessageA 47 | DispatchMessage.argtypes = [LPMSG] 48 | 49 | GetDoubleClickTime = user32.GetDoubleClickTime 50 | 51 | # Beware, as of 2016-01-30 the official docs have a very incomplete list. 52 | # This one was compiled from experience and may be incomplete. 53 | WM_MOUSEMOVE = 0x200 54 | WM_LBUTTONDOWN = 0x201 55 | WM_LBUTTONUP = 0x202 56 | WM_LBUTTONDBLCLK = 0x203 57 | WM_RBUTTONDOWN = 0x204 58 | WM_RBUTTONUP = 0x205 59 | WM_RBUTTONDBLCLK = 0x206 60 | WM_MBUTTONDOWN = 0x207 61 | WM_MBUTTONUP = 0x208 62 | WM_MBUTTONDBLCLK = 0x209 63 | WM_MOUSEWHEEL = 0x20A 64 | WM_XBUTTONDOWN = 0x20B 65 | WM_XBUTTONUP = 0x20C 66 | WM_XBUTTONDBLCLK = 0x20D 67 | WM_NCXBUTTONDOWN = 0x00AB 68 | WM_NCXBUTTONUP = 0x00AC 69 | WM_NCXBUTTONDBLCLK = 0x00AD 70 | WM_MOUSEHWHEEL = 0x20E 71 | WM_LBUTTONDOWN = 0x0201 72 | WM_LBUTTONUP = 0x0202 73 | WM_MOUSEMOVE = 0x0200 74 | WM_MOUSEWHEEL = 0x020A 75 | WM_MOUSEHWHEEL = 0x020E 76 | WM_RBUTTONDOWN = 0x0204 77 | WM_RBUTTONUP = 0x0205 78 | 79 | buttons_by_wm_code = { 80 | WM_LBUTTONDOWN: (DOWN, LEFT), 81 | WM_LBUTTONUP: (UP, LEFT), 82 | WM_LBUTTONDBLCLK: (DOUBLE, LEFT), 83 | 84 | WM_RBUTTONDOWN: (DOWN, RIGHT), 85 | WM_RBUTTONUP: (UP, RIGHT), 86 | WM_RBUTTONDBLCLK: (DOUBLE, RIGHT), 87 | 88 | WM_MBUTTONDOWN: (DOWN, MIDDLE), 89 | WM_MBUTTONUP: (UP, MIDDLE), 90 | WM_MBUTTONDBLCLK: (DOUBLE, MIDDLE), 91 | 92 | WM_XBUTTONDOWN: (DOWN, X), 93 | WM_XBUTTONUP: (UP, X), 94 | WM_XBUTTONDBLCLK: (DOUBLE, X), 95 | } 96 | 97 | MOUSEEVENTF_ABSOLUTE = 0x8000 98 | MOUSEEVENTF_MOVE = 0x1 99 | MOUSEEVENTF_WHEEL = 0x800 100 | MOUSEEVENTF_HWHEEL = 0x1000 101 | MOUSEEVENTF_LEFTDOWN = 0x2 102 | MOUSEEVENTF_LEFTUP = 0x4 103 | MOUSEEVENTF_RIGHTDOWN = 0x8 104 | MOUSEEVENTF_RIGHTUP = 0x10 105 | MOUSEEVENTF_MIDDLEDOWN = 0x20 106 | MOUSEEVENTF_MIDDLEUP = 0x40 107 | MOUSEEVENTF_XDOWN = 0x0080 108 | MOUSEEVENTF_XUP = 0x0100 109 | 110 | simulated_mouse_codes = { 111 | (WHEEL, HORIZONTAL): MOUSEEVENTF_HWHEEL, 112 | (WHEEL, VERTICAL): MOUSEEVENTF_WHEEL, 113 | 114 | (DOWN, LEFT): MOUSEEVENTF_LEFTDOWN, 115 | (UP, LEFT): MOUSEEVENTF_LEFTUP, 116 | 117 | (DOWN, RIGHT): MOUSEEVENTF_RIGHTDOWN, 118 | (UP, RIGHT): MOUSEEVENTF_RIGHTUP, 119 | 120 | (DOWN, MIDDLE): MOUSEEVENTF_MIDDLEDOWN, 121 | (UP, MIDDLE): MOUSEEVENTF_MIDDLEUP, 122 | 123 | (DOWN, X): MOUSEEVENTF_XDOWN, 124 | (UP, X): MOUSEEVENTF_XUP, 125 | } 126 | 127 | NULL = c_int(0) 128 | 129 | WHEEL_DELTA = 120 130 | 131 | init = lambda: None 132 | 133 | previous_button_event = None # defined in global scope 134 | def listen(queue): 135 | 136 | def low_level_mouse_handler(nCode, wParam, lParam): 137 | global previous_button_event 138 | 139 | struct = lParam.contents 140 | # Can't use struct.time because it's usually zero. 141 | t = time.time() 142 | 143 | if wParam == WM_MOUSEMOVE: 144 | event = MoveEvent(struct.x, struct.y, t) 145 | elif wParam == WM_MOUSEWHEEL: 146 | event = WheelEvent(struct.data / (WHEEL_DELTA * (2<<15)), t) 147 | elif wParam in buttons_by_wm_code: 148 | type, button = buttons_by_wm_code.get(wParam, ('?', '?')) 149 | if wParam >= WM_XBUTTONDOWN: 150 | button = {0x10000: X, 0x20000: X2}[struct.data] 151 | event = ButtonEvent(type, button, t) 152 | 153 | if (event.event_type == DOWN) and previous_button_event is not None: 154 | # https://msdn.microsoft.com/en-us/library/windows/desktop/gg153548%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 155 | if event.time - previous_button_event.time <= GetDoubleClickTime() / 1000.0: 156 | event = ButtonEvent(DOUBLE, event.button, event.time) 157 | 158 | previous_button_event = event 159 | else: 160 | # Unknown event type. 161 | return CallNextHookEx(NULL, nCode, wParam, lParam) 162 | 163 | queue.put(event) 164 | return CallNextHookEx(NULL, nCode, wParam, lParam) 165 | 166 | WH_MOUSE_LL = c_int(14) 167 | mouse_callback = LowLevelMouseProc(low_level_mouse_handler) 168 | mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, NULL) 169 | 170 | # Register to remove the hook when the interpreter exits. Unfortunately a 171 | # try/finally block doesn't seem to work here. 172 | atexit.register(UnhookWindowsHookEx, mouse_hook) 173 | 174 | msg = LPMSG() 175 | while not GetMessage(msg, NULL, NULL, NULL): 176 | TranslateMessage(msg) 177 | DispatchMessage(msg) 178 | 179 | def _translate_button(button): 180 | if button.startswith(X): 181 | return X, 1 if X == button else 2 182 | else: 183 | return button, 0 184 | 185 | def press(button=LEFT): 186 | button, data = _translate_button(button) 187 | code = simulated_mouse_codes[(DOWN, button)] 188 | user32.mouse_event(code, 0, 0, data, 0) 189 | 190 | def release(button=LEFT): 191 | button, data = _translate_button(button) 192 | code = simulated_mouse_codes[(UP, button)] 193 | user32.mouse_event(code, 0, 0, data, 0) 194 | 195 | def wheel(delta=1): 196 | code = simulated_mouse_codes[(WHEEL, VERTICAL)] 197 | user32.mouse_event(code, 0, 0, int(delta * WHEEL_DELTA), 0) 198 | 199 | def move_to(x, y): 200 | user32.SetCursorPos(int(x), int(y)) 201 | 202 | def move_relative(x, y): 203 | user32.mouse_event(MOUSEEVENTF_MOVE, int(x), int(y), 0, 0) 204 | 205 | class POINT(Structure): 206 | _fields_ = [("x", c_long), ("y", c_long)] 207 | 208 | def get_position(): 209 | point = POINT() 210 | user32.GetCursorPos(byref(point)) 211 | return (point.x, point.y) 212 | 213 | if __name__ == '__main__': 214 | def p(e): 215 | print(e) 216 | listen(p) 217 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage instructions: 3 | 4 | - If you are installing: `python setup.py install` 5 | - If you are developing: `python setup.py sdist --format=zip bdist_wheel --universal bdist_wininst && twine check dist/*` 6 | """ 7 | import mouse 8 | 9 | from setuptools import setup 10 | setup( 11 | name='mouse', 12 | version=mouse.version, 13 | author='BoppreH', 14 | author_email='boppreh@gmail.com', 15 | packages=['mouse'], 16 | package_data={'mouse': ['*.md']}, 17 | url='https://github.com/boppreh/mouse', 18 | license='MIT', 19 | description='Hook and simulate mouse events on Windows and Linux', 20 | keywords = 'mouse hook simulate hotkey', 21 | 22 | # Wheel creation breaks with Windows newlines. 23 | # https://github.com/pypa/setuptools/issues/1126 24 | long_description=mouse.__doc__.replace('\r\n', '\n'), 25 | long_description_content_type='text/markdown', 26 | 27 | install_requires=["pyobjc-framework-Quartz; sys_platform=='darwin'"], # OSX-specific dependency 28 | classifiers=[ 29 | 'Development Status :: 4 - Beta', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: Microsoft :: Windows', 32 | 'Operating System :: Unix :: MacOS', 33 | 'Programming Language :: Python :: 2', 34 | 'Programming Language :: Python :: 3', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | 'Topic :: Utilities', 37 | ], 38 | ) 39 | --------------------------------------------------------------------------------