diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2e262f5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,57 @@ +name: tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # The Linux backend only touches /dev/input from init(), so the suite + # imports and runs unprivileged against the fake backend, as on Windows. + os: [windows-latest, ubuntu-latest] + python-version: ['3.11', '3.13'] + include: + # ubuntu-latest no longer ships 3.8, the oldest version claimed by + # requires-python, so pin the older runner for that one job. + - os: ubuntu-22.04 + python-version: '3.8' + - os: windows-latest + python-version: '3.8' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -e . + + - name: Run tests + run: python -m pytest tests/ -v --ignore=tests/manual + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Build and check the distribution + run: | + python -m pip install --upgrade pip + pip install build twine + python -m build + twine check dist/* diff --git a/CHANGES.md b/CHANGES.md index 226b945..55987ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,26 @@ +# 1.0.0 + +First release of `directkeys`, a fork of [boppreh/keyboard](https://github.com/boppreh/keyboard) 0.13.5. +The API is unchanged, so migrating is usually just a matter of changing the import. + +New features: + +- [Windows] Configurable AltGr abstraction via `set_alt_gr_abstraction()` and + `get_alt_gr_abstraction_state()`. Enabled by default, it reports the + Right Alt + synthetic Left Ctrl pair as a single `alt gr` event; disable it + to see the raw events. +- [Windows] `get_stuck_keys()` reports modifiers left held down, and + `force_reset_keyboard()` releases them. Useful after a program crashes + without releasing a key it pressed. +- `KeyboardEvent.flags` exposes the low-level hook flags, and is included in + `to_json()`. On Windows it currently carries the `LLKHF_EXTENDED` bit. + +Packaging: + +- Renamed the package and the distribution to `directkeys`. +- Metadata moved to `pyproject.toml`; Python 3.8+ is required. + + # 0.13.5 - Added LICENSE.txt file to PyPI packages. diff --git a/LICENSE.txt b/LICENSE.txt index ac5c334..dfb1482 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 BoppreH +Copyright (c) 2025 WigoWigo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index bf0ce42..6691e35 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include CHANGES.md include README.md -include LICENSE.txt \ No newline at end of file +include LICENSE.txt +include pyproject.toml \ No newline at end of file diff --git a/Makefile b/Makefile index e6a587b..8767c8a 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ test: python -m pytest tests/ --cov=directkeys --cov-report=html -build: tests directkeys setup.py README.md CHANGES.md MANIFEST.in +build: tests directkeys pyproject.toml README.md CHANGES.md MANIFEST.in python ../docstring2markdown/docstring2markdown.py directkeys "https://github.com/WigoWigo10/keyboard/blob/master" > README.md find . \( -name "*.py" -o -name "*.sh" -o -name "* .md" \) -exec dos2unix {} \; - python setup.py sdist --format=zip bdist_wheel && twine check dist/* + python -m build && twine check dist/* release: python make_release.py diff --git a/README.md b/README.md index 860e5bd..2923c87 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ -**This project is currently unmaintained. It works for many cases, and I wish to pick it up again in the future, but you might encounter some friction and limited features using it.** - ---- - ---- - directkeys -======== +========== Take full control of your keyboard with this small Python library. Hook global events, register hotkeys, simulate key presses and much more. +`directkeys` is an actively maintained fork of [boppreh/keyboard](https://github.com/boppreh/keyboard), which has been dormant since 2021. It keeps the original API so existing code only needs to change the import, and focuses on low-level control of the Windows backend: a configurable AltGr abstraction, raw event flags and recovery from stuck modifier keys. + ## Features - **Global event hook** on all keyboards (captures keys regardless of focus). @@ -16,7 +12,6 @@ Take full control of your keyboard with this small Python library. Hook global e - Works with **Windows** and **Linux** (requires sudo), with experimental **OS X** support (thanks @glitchassassin!). - **Pure Python**, no C modules to be compiled. - **Zero dependencies**. Trivial to install and deploy, just copy the files. -- **Python 2 and 3**. - Complex hotkey support (e.g. `ctrl+shift+m, ctrl+space`) with controllable timeout. - Includes **high level API** (e.g. [record](#directkeys.record) and [play](#directkeys.play), [add_abbreviation](#directkeys.add_abbreviation)). - Maps keys as they actually are in your layout, with **full internationalization support** (e.g. `Ctrl+ç`). @@ -25,19 +20,36 @@ Take full control of your keyboard with this small Python library. Hook global e - Doesn't break accented dead keys (I'm looking at you, pyHook). - Mouse support available via project [mouse](https://github.com/boppreh/mouse) (`pip install mouse`). +### New in this fork + +- **Configurable AltGr abstraction** ([set_alt_gr_abstraction](#directkeys.set_alt_gr_abstraction)): report AltGr as a single event, or expose the raw Windows sequence. +- **Event flags**: `KeyboardEvent.flags` carries the low-level hook flags. On Windows it is currently masked down to the `LLKHF_EXTENDED` bit. +- **Stuck key recovery** ([get_stuck_keys](#directkeys.get_stuck_keys) and [force_reset_keyboard](#directkeys.force_reset_keyboard)): detect and release modifiers left pressed by a crashed program. + ## Usage -Install the [PyPI package](https://pypi.python.org/pypi/keyboard/): +Install the [PyPI package](https://pypi.python.org/pypi/directkeys/): pip install directkeys or clone the repository (no installation required, source files are sufficient): - git clone https://github.com/WigoWigo10/directkeys + git clone https://github.com/WigoWigo10/keyboard + +or [download and extract the zip](https://github.com/WigoWigo10/keyboard/archive/master.zip) into your project folder. -or [download and extract the zip](https://github.com/boppreh/keyboard/archive/master.zip) into your project folder. +Then check the [API docs below](https://github.com/WigoWigo10/keyboard#api) to see what features are available. -Then check the [API docs below](https://github.com/boppreh/keyboard#api) to see what features are available. +### Migrating from `keyboard` + +The whole API is unchanged, so in most cases only the import differs: + +```py +# before +import keyboard +# after +import directkeys as keyboard +``` ## Example @@ -75,7 +87,7 @@ Use as standalone module: ```bash # Save JSON events to a file until interrupted: -python -m keyboard > events.txt +python -m directkeys > events.txt cat events.txt # {"event_type": "down", "scan_code": 25, "name": "p", "time": 1622447562.2994788, "is_keypad": false} @@ -83,7 +95,7 @@ cat events.txt # ... # Replay events -python -m keyboard < events.txt +python -m directkeys < events.txt ``` ## Known limitations: @@ -92,9 +104,9 @@ python -m keyboard < events.txt - Media keys on Linux may appear nameless (scan-code only) or not at all. [#20](https://github.com/boppreh/keyboard/issues/20) - Key suppression/blocking only available on Windows. [#22](https://github.com/boppreh/keyboard/issues/22) - To avoid depending on X, the Linux parts reads raw device files (`/dev/input/input*`) but this requires root. -- Other applications, such as some games, may register hooks that swallow all key events. In this case `keyboard` will be unable to report events. +- Other applications, such as some games, may register hooks that swallow all key events. In this case `directkeys` will be unable to report events. - This program makes no attempt to hide itself, so don't use it for keyloggers or online gaming bots. Be responsible. -- SSH connections forward only the text typed, not keyboard events. Therefore if you connect to a server or Raspberry PI that is running `keyboard` via SSH, the server will not detect your key events. +- SSH connections forward only the text typed, not keyboard events. Therefore if you connect to a server or Raspberry PI that is running `directkeys` via SSH, the server will not detect your key events. ## Common patterns and mistakes @@ -183,7 +195,7 @@ while True: ### 'Press any key to continue' ```py -# Don't do this! The `keyboard` module is meant for global events, even when your program is not in focus. +# Don't do this! The `directkeys` module is meant for global events, even when your program is not in focus. #import directkeys #print('Press any key to continue...') #directkeys.get_event() @@ -195,8 +207,6 @@ input('Press enter to continue...') # https://stackoverflow.com/questions/983354/how-to-make-a-script-wait-for-a-pressed-key ``` - - # API #### Table of Contents @@ -309,7 +319,7 @@ input('Press enter to continue...') ### KeyboardEvent.**to\_json**(self, ensure\_ascii=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/_keyboard_event.py#L34) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/_keyboard_event.py#L34) @@ -341,7 +351,7 @@ input('Press enter to continue...') ## directkeys.**is\_modifier**(key) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L242) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L242) Returns True if `key` is a scan code or name of a modifier key. @@ -352,7 +362,7 @@ Returns True if `key` is a scan code or name of a modifier key. ## directkeys.**key\_to\_scan\_codes**(key, error\_if\_missing=True) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L405) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L405) Returns a list of scan codes associated with this key (name or scan code). @@ -363,7 +373,7 @@ Returns a list of scan codes associated with this key (name or scan code). ## directkeys.**parse\_hotkey**(hotkey) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L435) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L435) Parses a user-provided hotkey into nested tuples representing the @@ -388,7 +398,7 @@ parse_hotkey("alt+shift+a, alt+b, c") ## directkeys.**send**(hotkey, do\_press=True, do\_release=True) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L468) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L468) Sends OS events that perform the given *hotkey* hotkey. @@ -414,7 +424,7 @@ Note: keys are released in the opposite order they were pressed. ## directkeys.**press**(hotkey) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L501) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L501) Presses and holds down a hotkey (see [`send`](#directkeys.send)). @@ -423,7 +433,7 @@ Presses and holds down a hotkey (see [`send`](#directkeys.send)). ## directkeys.**release**(hotkey) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L505) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L505) Releases a hotkey (see [`send`](#directkeys.send)). @@ -432,7 +442,7 @@ Releases a hotkey (see [`send`](#directkeys.send)). ## directkeys.**is\_pressed**(hotkey) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L509) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L509) Returns True if the key is pressed. @@ -450,7 +460,7 @@ is_pressed('ctrl+space') #-> True ## directkeys.**call\_later**(fn, args=(), delay=0.001) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L536) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L536) Calls the provided function in a new thread after waiting some time. @@ -463,7 +473,7 @@ the current execution flow. ## directkeys.**hook**(callback, suppress=False, on\_remove=<lambda>) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L546) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L546) Installs a global listener on all available keyboards, invoking `callback` @@ -486,7 +496,7 @@ Returns the given callback for easier development. ## directkeys.**on\_press**(callback, suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L577) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L577) Invokes `callback` for every KEY_DOWN event. For details see [`hook`](#directkeys.hook). @@ -497,7 +507,7 @@ Invokes `callback` for every KEY_DOWN event. For details see [`hook`](#directkey ## directkeys.**on\_release**(callback, suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L583) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L583) Invokes `callback` for every KEY_UP event. For details see [`hook`](#directkeys.hook). @@ -508,7 +518,7 @@ Invokes `callback` for every KEY_UP event. For details see [`hook`](#directkeys. ## directkeys.**hook\_key**(key, callback, suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L589) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L589) Hooks key up and key down events for a single key. Returns the event handler @@ -524,7 +534,7 @@ affects it as well. ## directkeys.**on\_press\_key**(key, callback, suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L613) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L613) Invokes `callback` for KEY_DOWN event related to the given key. For details see [`hook`](#directkeys.hook). @@ -535,7 +545,7 @@ Invokes `callback` for KEY_DOWN event related to the given key. For details see ## directkeys.**on\_release\_key**(key, callback, suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L619) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L619) Invokes `callback` for KEY_UP event related to the given key. For details see [`hook`](#directkeys.hook). @@ -546,7 +556,7 @@ Invokes `callback` for KEY_UP event related to the given key. For details see [` ## directkeys.**unhook**(remove) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L625) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L625) Removes a previously added hook, either by callback or by the return value @@ -558,7 +568,7 @@ of [`hook`](#directkeys.hook). ## directkeys.**unhook\_all**() -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L633) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L633) Removes all keyboard hooks in use, including hotkeys, abbreviations, word @@ -570,7 +580,7 @@ listeners, [`record`](#directkeys.record)ers and [`wait`](#directkeys.wait)s. ## directkeys.**block\_key**(key) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L645) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L645) Suppresses all key events of the given key, regardless of modifiers. @@ -581,7 +591,7 @@ Suppresses all key events of the given key, regardless of modifiers. ## directkeys.**remap\_key**(src, dst) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L652) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L652) Whenever the key `src` is pressed or released, regardless of modifiers, @@ -593,7 +603,7 @@ press or release the hotkey `dst` instead. ## directkeys.**parse\_hotkey\_combinations**(hotkey) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L666) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L666) Parses a user-provided hotkey. Differently from [`parse_hotkey`](#directkeys.parse_hotkey), @@ -606,7 +616,7 @@ each step is a list of all possible combinations of those scan codes. ## directkeys.**add\_hotkey**(hotkey, callback, args=(), suppress=False, timeout=1, trigger\_on\_release=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L706) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L706) Invokes a callback every time a hotkey is pressed. The hotkey must @@ -653,7 +663,7 @@ add_hotkey('ctrl+alt+enter, space', some_callback) ## directkeys.**remove\_hotkey**(hotkey\_or\_callback) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L852) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L852) Removes a previously hooked hotkey. Must be called with the value returned @@ -665,7 +675,7 @@ by [`add_hotkey`](#directkeys.add_hotkey). ## directkeys.**unhook\_all\_hotkeys**() -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L860) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L860) Removes all keyboard hotkeys in use, including abbreviations, word listeners, @@ -677,7 +687,7 @@ Removes all keyboard hotkeys in use, including abbreviations, word listeners, ## directkeys.**remap\_hotkey**(src, dst, suppress=True, trigger\_on\_release=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L871) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L871) Whenever the hotkey `src` is pressed, suppress it and send @@ -696,7 +706,7 @@ remap('alt+w', 'ctrl+up') ## directkeys.**stash\_state**() -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L891) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L891) Builds a list of all currently pressed scan codes, releases them and returns @@ -708,7 +718,7 @@ the list. Pairs well with [`restore_state`](#directkeys.restore_state) and [`res ## directkeys.**restore\_state**(scan\_codes) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L903) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L903) Given a list of scan_codes ensures these keys, and only these keys, are @@ -720,7 +730,7 @@ pressed. Pairs well with [`stash_state`](#directkeys.stash_state), alternative t ## directkeys.**restore\_modifiers**(scan\_codes) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L920) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L920) Like [`restore_state`](#directkeys.restore_state), but only restores modifier keys. @@ -731,7 +741,7 @@ Like [`restore_state`](#directkeys.restore_state), but only restores modifier ke ## directkeys.**write**(text, delay=0, restore\_state\_after=True, exact=None) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L926) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L926) Sends artificial keyboard events to the OS, simulating the typing of a given @@ -756,7 +766,7 @@ value. ## directkeys.**wait**(hotkey=None, suppress=False, trigger\_on\_release=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L981) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L981) Blocks the program execution until the given hotkey is pressed or, @@ -768,7 +778,7 @@ if given no parameters, blocks forever. ## directkeys.**get\_hotkey\_name**(names=None) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L995) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L995) Returns a string representation of hotkey from the given key names, or @@ -795,7 +805,7 @@ get_hotkey_name(['+', 'left ctrl', 'shift']) ## directkeys.**read\_event**(suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1026) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1026) Blocks until a keyboard event happens, then returns that event. @@ -806,7 +816,7 @@ Blocks until a keyboard event happens, then returns that event. ## directkeys.**read\_key**(suppress=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1037) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1037) Blocks until a keyboard event happens, then returns that event's name or, @@ -818,7 +828,7 @@ if missing, its scan code. ## directkeys.**read\_hotkey**(suppress=True) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1045) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1045) Similar to [`read_key()`](#directkeys.read_key), but blocks until the user presses and releases a @@ -839,7 +849,7 @@ read_hotkey() ## directkeys.**get\_typed\_strings**(events, allow\_backspace=True) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1067) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1067) Given a sequence of events, tries to deduce what strings were typed. @@ -866,7 +876,7 @@ get_type_strings(record()) #-> ['This is what', 'I recorded', ''] ## directkeys.**start\_recording**(recorded\_events\_queue=None) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1114) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1114) Starts recording all keyboard events into a global variable, or the given @@ -880,7 +890,7 @@ Use [`stop_recording()`](#directkeys.stop_recording) or [`unhook(hooked_function ## directkeys.**stop\_recording**() -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1126) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1126) Stops the global recording of events and returns a list of the events @@ -892,7 +902,7 @@ captured. ## directkeys.**record**(until='escape', suppress=False, trigger\_on\_release=False) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1138) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1138) Records all keyboard events from all keyboards until the user presses the @@ -909,7 +919,7 @@ Note: for more details on the keyboard hook and events see [`hook`](#directkeys. ## directkeys.**play**(events, speed\_factor=1.0) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1152) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1152) Plays a sequence of recorded events, maintaining the relative time @@ -925,7 +935,7 @@ the end of the function. ## directkeys.**add\_word\_listener**(word, callback, triggers=['space'], match\_suffix=False, timeout=2) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1176) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1176) Invokes a callback every time a sequence of characters is typed (e.g. 'pet') @@ -957,7 +967,7 @@ Note: word matches are **case sensitive**. ## directkeys.**remove\_word\_listener**(word\_or\_handler) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1232) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1232) Removes a previously registered word listener. Accepts either the word used @@ -970,7 +980,7 @@ during registration (exact string) or the event handler returned by the ## directkeys.**add\_abbreviation**(source\_text, replacement\_text, match\_suffix=False, timeout=2) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/__init__.py#L1240) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/__init__.py#L1240) Registers a hotkey that replaces one typed text with another. For example @@ -997,7 +1007,7 @@ For more details see [`add_word_listener`](#directkeys.add_word_listener). ## directkeys.**normalize\_name**(name) -[\[source\]](https://github.com/boppreh/keyboard/blob/master/keyboard/_canonical_names.py#L1233) +[\[source\]](https://github.com/WigoWigo10/keyboard/blob/master/directkeys/_canonical_names.py#L1233) Given a key name (e.g. "LEFT CONTROL"), clean up the string and convert to diff --git a/directkeys/__init__.py b/directkeys/__init__.py index cf16a07..20111b6 100644 --- a/directkeys/__init__.py +++ b/directkeys/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- """ -keyboard -======== +directkeys +========== Take full control of your keyboard with this small Python library. Hook global events, register hotkeys, simulate key presses and much more. +`directkeys` is an actively maintained fork of [boppreh/keyboard](https://github.com/boppreh/keyboard), which has been dormant since 2021. It keeps the original API so existing code only needs to change the import, and focuses on low-level control of the Windows backend: a configurable AltGr abstraction, raw event flags and recovery from stuck modifier keys. + ## Features - **Global event hook** on all keyboards (captures keys regardless of focus). @@ -12,7 +14,6 @@ - Works with **Windows** and **Linux** (requires sudo), with experimental **OS X** support (thanks @glitchassassin!). - **Pure Python**, no C modules to be compiled. - **Zero dependencies**. Trivial to install and deploy, just copy the files. -- **Python 2 and 3**. - Complex hotkey support (e.g. `ctrl+shift+m, ctrl+space`) with controllable timeout. - Includes **high level API** (e.g. [record](#directkeys.record) and [play](#directkeys.play), [add_abbreviation](#directkeys.add_abbreviation)). - Maps keys as they actually are in your layout, with **full internationalization support** (e.g. `Ctrl+ç`). @@ -21,19 +22,36 @@ - Doesn't break accented dead keys (I'm looking at you, pyHook). - Mouse support available via project [mouse](https://github.com/boppreh/mouse) (`pip install mouse`). +### New in this fork + +- **Configurable AltGr abstraction** ([set_alt_gr_abstraction](#directkeys.set_alt_gr_abstraction)): report AltGr as a single event, or expose the raw Windows sequence. +- **Event flags**: `KeyboardEvent.flags` carries the low-level hook flags. On Windows it is currently masked down to the `LLKHF_EXTENDED` bit. +- **Stuck key recovery** ([get_stuck_keys](#directkeys.get_stuck_keys) and [force_reset_keyboard](#directkeys.force_reset_keyboard)): detect and release modifiers left pressed by a crashed program. + ## Usage -Install the [PyPI package](https://pypi.python.org/pypi/keyboard/): +Install the [PyPI package](https://pypi.python.org/pypi/directkeys/): - pip install keyboard + pip install directkeys or clone the repository (no installation required, source files are sufficient): - git clone https://github.com/boppreh/keyboard + git clone https://github.com/WigoWigo10/keyboard -or [download and extract the zip](https://github.com/boppreh/keyboard/archive/master.zip) into your project folder. +or [download and extract the zip](https://github.com/WigoWigo10/keyboard/archive/master.zip) into your project folder. -Then check the [API docs below](https://github.com/boppreh/keyboard#api) to see what features are available. +Then check the [API docs below](https://github.com/WigoWigo10/keyboard#api) to see what features are available. + +### Migrating from `keyboard` + +The whole API is unchanged, so in most cases only the import differs: + +```py +# before +import keyboard +# after +import directkeys as keyboard +``` ## Example @@ -71,7 +89,7 @@ ```bash # Save JSON events to a file until interrupted: -python -m keyboard > events.txt +python -m directkeys > events.txt cat events.txt # {"event_type": "down", "scan_code": 25, "name": "p", "time": 1622447562.2994788, "is_keypad": false} @@ -79,7 +97,7 @@ # ... # Replay events -python -m keyboard < events.txt +python -m directkeys < events.txt ``` ## Known limitations: @@ -88,9 +106,9 @@ - Media keys on Linux may appear nameless (scan-code only) or not at all. [#20](https://github.com/boppreh/keyboard/issues/20) - Key suppression/blocking only available on Windows. [#22](https://github.com/boppreh/keyboard/issues/22) - To avoid depending on X, the Linux parts reads raw device files (`/dev/input/input*`) but this requires root. -- Other applications, such as some games, may register hooks that swallow all key events. In this case `keyboard` will be unable to report events. +- Other applications, such as some games, may register hooks that swallow all key events. In this case `directkeys` will be unable to report events. - This program makes no attempt to hide itself, so don't use it for keyloggers or online gaming bots. Be responsible. -- SSH connections forward only the text typed, not keyboard events. Therefore if you connect to a server or Raspberry PI that is running `keyboard` via SSH, the server will not detect your key events. +- SSH connections forward only the text typed, not keyboard events. Therefore if you connect to a server or Raspberry PI that is running `directkeys` via SSH, the server will not detect your key events. ## Common patterns and mistakes @@ -179,7 +197,7 @@ def on_space(): ### 'Press any key to continue' ```py -# Don't do this! The `keyboard` module is meant for global events, even when your program is not in focus. +# Don't do this! The `directkeys` module is meant for global events, even when your program is not in focus. #import directkeys #print('Press any key to continue...') #directkeys.get_event() @@ -195,7 +213,7 @@ def on_space(): version = '1.0.0' -# Variável de estado para a abstração do AltGr, gerenciada centralmente. +# Centrally managed state for the AltGr abstraction, read by the backend. _ABSTRACT_ALT_GR = True import re as _re @@ -226,12 +244,12 @@ def wait(self): if _UninterruptibleEvent.wait(self, 0.5): break -# Carrega as dependências base antes do backend para evitar import circular. +# Load the base dependencies before the backend, to avoid a circular import. from ._keyboard_event import KEY_DOWN, KEY_UP, KeyboardEvent from ._generic import GenericListener as _GenericListener from ._canonical_names import all_modifiers, sided_modifiers, normalize_name -# Lógica de importação do backend específico do SO. +# Import the platform specific backend. import platform as _platform if _platform.system() == 'Windows': from . import _winkeyboard as _os_keyboard @@ -241,53 +259,56 @@ def wait(self): try: from . import _darwinkeyboard as _os_keyboard except ImportError: - # Pode acontecer durante a instalação, quando o setup.py importa este - # pacote para ler a versão antes de o pyobjc estar disponível. + # Can happen during installation, when setup.py imports this package to + # read the version before pyobjc is available. _os_keyboard = None else: raise OSError("Unsupported platform '{}'".format(_platform.system())) -# Funções públicas para controlar as novas funcionalidades. def set_alt_gr_abstraction(enabled): """ - Habilita ou desabilita a abstração da tecla AltGr no backend do Windows. + Enables or disables the AltGr abstraction in the Windows backend. + + By default the library reports the Windows event sequence for AltGr + (Right Alt + a synthetic Left Ctrl) as a single `alt gr` event. Disabling + this makes the raw events visible instead, which is useful when you need + to tell a real Ctrl press apart from the one Windows synthesises. - Por padrão, a biblioteca trata a sequência de eventos do Windows para 'AltGr' - (Right Alt + Left Ctrl) como um único evento 'alt gr'. Desabilitar esta - opção fará com que os eventos brutos sejam reportados. + Has no effect on other platforms. Args: - enabled (bool): True para habilitar a abstração (padrão), False para desabilitar. + enabled (bool): True to enable the abstraction (default), False to + report raw events. """ global _ABSTRACT_ALT_GR _ABSTRACT_ALT_GR = bool(enabled) - # Notifica o backend para se reconfigurar, se necessário. + # Let the backend reconfigure itself, if it needs to. if hasattr(_os_keyboard, 'rebuild_name_tables'): _os_keyboard.rebuild_name_tables() def get_alt_gr_abstraction_state(): - """ Retorna o estado atual da abstração do AltGr (True se habilitada). """ + """ Returns True if the AltGr abstraction is currently enabled. """ return _ABSTRACT_ALT_GR -# Importa as demais funções do backend. `_os_keyboard` é um alias para o módulo -# já importado acima, portanto a busca precisa ser feita por atributo: um -# `from ._os_keyboard import ...` procuraria por um submódulo inexistente e -# cairia sempre nos fallbacks, mesmo no Windows. +# Import the remaining backend functions. `_os_keyboard` is an alias bound to +# the module imported above, so these have to be looked up as attributes: a +# `from ._os_keyboard import ...` would search for a submodule that does not +# exist and always fall through to the fallbacks, even on Windows. def _fallback_force_reset_keyboard(): """ - Função de fallback para SOs sem suporte. Não faz nada. + Fallback for platforms without a stuck key implementation. Does nothing. """ pass def _fallback_get_stuck_keys(): """ - Função de fallback para SOs sem suporte. Retorna uma lista vazia. + Fallback for platforms without a stuck key implementation. Returns []. """ return [] def _fallback_reset_internal_state(): """ - Função de fallback para SOs sem suporte. Não faz nada. + Fallback for platforms with no internal state to reset. Does nothing. """ pass diff --git a/directkeys/_winkeyboard.py b/directkeys/_winkeyboard.py index 159d6ee..7f8628c 100644 --- a/directkeys/_winkeyboard.py +++ b/directkeys/_winkeyboard.py @@ -446,14 +446,12 @@ def order_key(line): for name, entries in list(from_name.items()): from_name[name] = sorted(set(entries), key=order_key) -# COLE ESTAS DUAS FUNÇÕES DEPOIS DE _setup_name_tables() - def get_modifiers(altgr_is_pressed): """ - Retorna uma tupla com os nomes dos modificadores atualmente ativos. + Returns a tuple with the names of the currently active modifiers. """ - # GetKeyState devolve 0x8000 (e não 1) no bit de "pressionado", portanto a - # conversão para bool é obrigatória antes de repetir a tupla. + # GetKeyState reports the "pressed" bit as 0x8000, not 1, so every flag has + # to be coerced to a bool before being used to repeat the tuple. return ( ('shift',) * bool(user32.GetKeyState(0x10) & 0x8000) + ('alt gr',) * bool(altgr_is_pressed) + @@ -464,38 +462,17 @@ def get_modifiers(altgr_is_pressed): def get_name(scan_code, vk, is_extended, modifiers): """ - Obtém o nome mais provável para um evento de tecla, dados os modificadores. + Returns the most likely name for a key event, given the active modifiers. """ entry = (scan_code, vk, is_extended, modifiers) - if entry not in to_name: - # Popula a tabela se a combinação for nova - to_name[entry] = list(get_event_names(*entry)) - - names = to_name[entry] + with tables_lock: + if entry not in to_name: + # Cache the combination the first time it is seen. + to_name[entry] = list(get_event_names(*entry)) + names = to_name[entry] return names[0] if names else None -# O resto do arquivo continua aqui (init = _setup_name_tables, keypad_keys = [...], etc.) - -def _remove_alt_gr_mapping(): - """ - Remove ativamente o mapeamento sintético 'alt gr' das tabelas de nomes - se elas já tiverem sido criadas. - """ - with tables_lock: - if 'alt gr' in from_name: - # Remove a entrada da tabela de tradução de nome para código - del from_name['alt gr'] - - # Encontra e remove todas as entradas da tabela de tradução de código para nome - # que correspondem ao 'alt gr' sintético (scan_code=541, vk_code=162). - keys_to_remove = [ - key for key in to_name - if key[0] == 541 and key[1] == 162 - ] - for key in keys_to_remove: - del to_name[key] - -# Called by keyboard/__init__.py +# Called by directkeys/__init__.py init = _setup_name_tables # List created manually. @@ -539,10 +516,7 @@ def _remove_alt_gr_mapping(): (83, 46, 0), ] -shift_is_pressed = False altgr_is_pressed = False -ignore_next_right_alt = False -shift_vks = set([0x10, 0xa0, 0xa1]) def prepare_intercept(callback): """ Registers a Windows low level keyboard hook. The provided callback will @@ -555,85 +529,84 @@ def prepare_intercept(callback): """ _setup_name_tables() - # Adicionado 'flags' como parâmetro da função 'process_key'. - # A função 'process_key' agora irá verificar o switch. def process_key(event_type, vk, scan_code, is_extended, flags): """ - Callback que processa os eventos, lendo o estado de abstração do módulo principal. + Processes a raw hook event, reading the AltGr abstraction setting from + the main module so it can be toggled at runtime. """ - # Importação local para evitar ciclo e ler o estado atualizado. + # Imported locally to break the import cycle and to always read the + # current value of the setting. import directkeys - - global altgr_is_pressed, shift_is_pressed - # Se a abstração estiver DESLIGADA, ignora o Ctrl sintético. + global altgr_is_pressed + + # With the abstraction OFF, drop the synthetic Ctrl entirely. if not directkeys._ABSTRACT_ALT_GR and scan_code == 541: - return True # Suprime o evento + return True # Suppress the event. - # Se a abstração estiver LIGADA, combina os eventos. + # With the abstraction ON, merge the pair into a single 'alt gr' event. if directkeys._ABSTRACT_ALT_GR: global _altgr_right_alt_scan_code, _altgr_right_alt_flags if _altgr_right_alt_scan_code is not None and event_type == KEY_DOWN: - if scan_code == 541: # É o Ctrl sintético + if scan_code == 541: # The synthetic Ctrl. altgr_is_pressed = True event = KeyboardEvent('down', _altgr_right_alt_scan_code, name='alt gr', is_keypad=False, flags=_altgr_right_alt_flags) callback(event) _altgr_right_alt_scan_code = None _altgr_right_alt_flags = None - return True # Suprime o Ctrl sintético - else: # Não era, libera o Right Alt que estava pendente. + return True # Suppress the synthetic Ctrl. + else: # It was not, so flush the pending Right Alt. event = KeyboardEvent('down', _altgr_right_alt_scan_code, name='right alt', is_keypad=False, flags=_altgr_right_alt_flags) callback(event) _altgr_right_alt_scan_code = None _altgr_right_alt_flags = None - - if vk == 165: # É um Right Alt (VK 165) - if event_type == KEY_DOWN: # Pressionado, segura e espera o Ctrl. + + if vk == 165: # Right Alt. + if event_type == KEY_DOWN: # Hold it back and wait for the Ctrl. _altgr_right_alt_scan_code = scan_code _altgr_right_alt_flags = flags - return True # Suprime o Right Alt temporariamente - else: # Solto, conclui o evento 'alt gr'. + return True # Suppress the Right Alt for now. + else: # Released, so complete the 'alt gr' event. altgr_is_pressed = False event = KeyboardEvent('up', scan_code, name='alt gr', is_keypad=False, flags=flags) callback(event) return True - - # Ignora o KeyUp do Ctrl sintético. + + # Ignore the key up of the synthetic Ctrl. if scan_code == 541 and event_type == KEY_UP: return True - # Lógica Padrão para todas as outras teclas. - if vk in shift_vks: - shift_is_pressed = event_type == KEY_DOWN - + # Standard path for every other key. modifiers = get_modifiers(altgr_is_pressed) - + if not directkeys._ABSTRACT_ALT_GR and vk == 165: name = 'alt gr' else: name = get_name(scan_code, vk, is_extended, modifiers) - # `is_extended` não é sinônimo de `is_keypad`: o numpad 1-9 não é - # extended, enquanto setas, ctrl direito e insert são. A tabela - # `keypad_keys` é a única fonte confiável. + # `is_extended` is not a synonym for `is_keypad`: numpad 1-9 are not + # extended, while the arrows, right ctrl and insert are. The + # `keypad_keys` table is the only reliable source. is_keypad = (scan_code, vk, is_extended) in keypad_keys - # Teclas sem scan code (eventos injetados, envio por virtual key) caem - # no negativo do vk para continuarem distinguíveis entre si. + # Keys with no scan code (injected events, sending by virtual key) fall + # back to the negated vk so they stay distinguishable from each other. event = KeyboardEvent(event_type=event_type, scan_code=scan_code or -vk, name=name, is_keypad=is_keypad, flags=flags) return callback(event) def low_level_keyboard_handler(nCode, wParam, lParam): try: vk = lParam.contents.vk_code + # Ignore the second `alt` DOWN observed in some cases. fake_alt = (LLKHF_INJECTED | 0x20) + # Ignore events generated by SendInput with Unicode. if vk != VK_PACKET and lParam.contents.flags & fake_alt != fake_alt: event_type = KEY_UP if wParam & 0x01 else KEY_DOWN - + raw_flags = lParam.contents.flags - processed_flags = raw_flags & 1 # Isola o bit LLKHF_EXTENDED + processed_flags = raw_flags & 1 # Isolate the LLKHF_EXTENDED bit. is_extended = processed_flags scan_code = lParam.contents.scan_code - + should_continue = process_key(event_type, vk, scan_code, is_extended, processed_flags) if not should_continue: @@ -655,7 +628,7 @@ def low_level_keyboard_handler(nCode, wParam, lParam): atexit.register(UnhookWindowsHookEx, keyboard_callback) def _clear_name_tables(): - """ Limpa as tabelas de nomes para que possam ser reconstruídas. """ + """ Empties the name tables so they can be rebuilt. """ with tables_lock: to_name.clear() from_name.clear() @@ -663,8 +636,8 @@ def _clear_name_tables(): def rebuild_name_tables(): """ - Força a limpeza e reconstrução das tabelas de nomes. - Chamado pelo __init__.py quando a configuração de abstração muda. + Forces the name tables to be cleared and rebuilt. Called by __init__.py + when the AltGr abstraction setting changes. """ _clear_name_tables() _setup_name_tables() @@ -724,69 +697,58 @@ def type_unicode(character): cbSize = c_int(ctypes.sizeof(INPUT)) SendInput(nInputs, pInputs, cbSize) -if __name__ == '__main__': - _setup_name_tables() - import pprint - pprint.pprint(to_name) - pprint.pprint(from_name) - #listen(lambda e: print(e.to_json()) or True) +# Virtual key codes of the modifiers that can be left stuck by a program that +# pressed them and exited without releasing. +_modifier_vk_names = { + 0x10: 'shift', 0xA0: 'left shift', 0xA1: 'right shift', + 0x11: 'ctrl', 0xA2: 'left ctrl', 0xA3: 'right ctrl', + 0x12: 'alt', 0xA4: 'left alt', 0xA5: 'right alt', + 0x5B: 'left windows', 0x5C: 'right windows', +} def force_reset_keyboard(): """ - Força o sistema operacional (Windows) a liberar quaisquer teclas modificadoras - que possam ter ficado "presas". + Forces Windows to release any modifier key that was left stuck down. - Para cada tecla detectada como presa, simula 5 cliques (pressionar e soltar) - rapidamente para garantir que o sistema operacional atualize seu estado. + Each key detected as stuck is clicked (pressed and released) five times in + quick succession, to make sure the operating system updates its state. """ - # Lista de virtual key codes para as principais teclas modificadoras - vk_codes = [ - 0x10, 0xA0, 0xA1, # Shift, Left Shift, Right Shift - 0x11, 0xA2, 0xA3, # Ctrl, Left Ctrl, Right Ctrl - 0x12, 0xA4, 0xA5, # Alt, Left Alt, Right Alt (AltGr) - 0x5B, 0x5C # Left Windows, Right Windows - ] - - for vk in vk_codes: - # Verifica se a tecla está "presa" (bit mais significativo está 1) + for vk in _modifier_vk_names: + # The high order bit is set while the key is held down. if user32.GetKeyState(vk) & 0x8000: for _ in range(5): - # Envia um evento de KEY DOWN (pressionar) user32.keybd_event(vk, 0, 0, 0) - # Envia um evento de KEY UP (soltar) user32.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0) - # Pequena pausa para o SO processar + # Give the OS a moment to process the pair. time.sleep(0.01) -# Esta função você já deve ter no final do arquivo, mantenha-a. def _reset_internal_state(): """ - Limpa as variáveis de estado internas da biblioteca usadas para o tratamento - do AltGr, garantindo um estado limpo entre execuções ou testes. + Clears the internal state used by the AltGr handling, so that a fresh run + or test does not inherit a half-finished key sequence. """ - global _altgr_right_alt_scan_code, _altgr_right_alt_flags, altgr_is_pressed, ignore_next_right_alt + global _altgr_right_alt_scan_code, _altgr_right_alt_flags, altgr_is_pressed _altgr_right_alt_scan_code = None _altgr_right_alt_flags = None altgr_is_pressed = False - ignore_next_right_alt = False def get_stuck_keys(): """ - Verifica o estado físico de todas as teclas modificadoras principais usando - GetAsyncKeyState e retorna uma lista com os nomes daquelas que estão presas. + Returns the names of the modifier keys that are currently held down. + + Uses GetAsyncKeyState, which reports the actual physical state, rather than + the state as seen by the message queue. """ stuck_keys = [] - vk_codes_with_names = { - 0x10: 'shift', 0xA0: 'left shift', 0xA1: 'right shift', - 0x11: 'ctrl', 0xA2: 'left ctrl', 0xA3: 'right ctrl', - 0x12: 'alt', 0xA4: 'left alt', 0xA5: 'right alt', - 0x5B: 'left windows', 0x5C: 'right windows' - } - - for vk, name in vk_codes_with_names.items(): - # A verificação de bit mais significativo (0x8000) funciona para ambas as funções. - # A diferença é que GetAsyncKeyState verifica o estado físico atual. + for vk, name in _modifier_vk_names.items(): if GetAsyncKeyState(vk) & 0x8000: stuck_keys.append(name) - + return stuck_keys + +if __name__ == '__main__': + _setup_name_tables() + import pprint + pprint.pprint(to_name) + pprint.pprint(from_name) + #listen(lambda e: print(e.to_json()) or True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4bf5502 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "directkeys" +dynamic = ["version"] +description ="A modern and robust keyboard hooking and simulation library for Windows and Linux, focusing on low-level control." +readme = { file = "README.md", content-type = "text/markdown" } +license = { file = "LICENSE.txt" } +requires-python = ">=3.8" +authors = [{ name = "WigoWigo", email = "hiigoor93@gmail.com" }] +keywords = ["directkeys", "keyboard", "hook", "simulate", "hotkey", "low-level", "win32", "sendinput"] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", +] +dependencies = ["pyobjc; sys_platform=='darwin'"] + +[project.urls] +Homepage = "https://github.com/WigoWigo10/keyboard" +Source = "https://github.com/WigoWigo10/keyboard" +Changelog = "https://github.com/WigoWigo10/keyboard/blob/master/CHANGES.md" +Issues = "https://github.com/WigoWigo10/keyboard/issues" + +[tool.setuptools] +packages = ["directkeys"] + +# Read statically from the module, so the version has a single source of truth +# and the package does not have to be imported at build time. +[tool.setuptools.dynamic] +version = { attr = "directkeys.version" } diff --git a/setup.py b/setup.py index 574df96..45c4a55 100644 --- a/setup.py +++ b/setup.py @@ -1,40 +1,10 @@ """ -Usage instructions: +Shim kept so that legacy `python setup.py ...` invocations keep working. -- If you are installing: `python setup.py install` -- If you are developing: `python setup.py sdist bdist_wheel && twine check dist/*` -""" -import directkeys +All packaging metadata lives in pyproject.toml. Prefer building with: + python -m build +""" from setuptools import setup -setup( - name='directkeys', - version=directkeys.version, - author='WigoWigo', - author_email='hiigoor93@gmail.com', - packages=['directkeys'], - url='https://github.com/WigoWigo10/keyboard', - license='MIT', - description='A modern and robust keyboard hooking and simulation library for Windows and Linux, focusing on low-level control.', - keywords='directkeys keyboard hook simulate hotkey low-level win32 sendinput', - long_description=directkeys.__doc__.replace('\r\n', '\n'), - long_description_content_type='text/markdown', - install_requires=["pyobjc; sys_platform=='darwin'"], - classifiers=[ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: Unix', - 'Operating System :: MacOS :: MacOS X', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - ], -) \ No newline at end of file +setup() diff --git a/run_test.py b/tests/manual/altgr_and_stuck_keys.py similarity index 86% rename from run_test.py rename to tests/manual/altgr_and_stuck_keys.py index d5ba703..876bb1a 100644 --- a/run_test.py +++ b/tests/manual/altgr_and_stuck_keys.py @@ -1,7 +1,16 @@ +""" +Testes manuais e interativos do backend do Windows: exigem um teclado real +(de preferência ABNT2 ou US-INTL, para exercitar o AltGr) e um operador para +seguir as instruções na tela. Rode com `python tests/manual/altgr_and_stuck_keys.py`. +""" import directkeys import time import subprocess import sys +import os + +# Resolvido a partir deste arquivo, para o script funcionar de qualquer cwd. +CRASH_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'simulate_stuck_key_crash.py') def read_next_keydown(suppress=True): """ @@ -53,8 +62,7 @@ def test_stuck_key_fix(): print("\n--- INICIANDO TESTE 2: Correção de Tecla Presa ---") print("--> Passo 2a: Simulando script que trava com 'Ctrl' pressionado...") - crash_script_path = "run_test_2a_crash.py" - process = subprocess.Popen([sys.executable, crash_script_path]) + process = subprocess.Popen([sys.executable, CRASH_SCRIPT]) process.wait() time.sleep(1) print("--> Script travado. A tecla 'Ctrl' deve estar 'presa' no sistema.") @@ -84,11 +92,8 @@ def test_stuck_key_fix(): print("\n✅ Teste 2: SUCESSO!") if __name__ == "__main__": - required_script = "run_test_2a_crash.py" - try: - with open(required_script, "r") as f: pass - except FileNotFoundError: - print(f"\nERRO: O script auxiliar '{required_script}' não foi encontrado.") + if not os.path.exists(CRASH_SCRIPT): + print(f"\nERRO: O script auxiliar '{CRASH_SCRIPT}' não foi encontrado.") sys.exit(1) try: diff --git a/run_test_2a_crash.py b/tests/manual/simulate_stuck_key_crash.py similarity index 100% rename from run_test_2a_crash.py rename to tests/manual/simulate_stuck_key_crash.py