├── .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 |
--------------------------------------------------------------------------------