From 2ce401faa83d26a6f21925a2c58df46b8b5cc64b Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:26:43 -0400 Subject: [PATCH 1/7] docs: rebrand package docstring and README for the fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup.py publishes `directkeys.__doc__` as the PyPI long_description, and the Makefile regenerates README.md from that same docstring — so the docstring was the single source still advertising the upstream project. It told readers to `pip install keyboard`, pointed every link at boppreh/keyboard, and the README carried the upstream "this project is currently unmaintained" banner, which is the opposite of this fork's premise. - Retitle to directkeys and state up front that this is a maintained fork. - Point install/clone/API links at this repository. - Add a "New in this fork" section and a migration note for `import keyboard`. - Drop the "Python 2 and 3" claim, which contradicts the 3.8+ classifiers. - Rewrite the generated [source] links to this repo and the directkeys package. References to boppreh/mouse and to upstream issues #20/#21/#22 are left alone: they point at a sibling project and at real upstream discussions. Co-Authored-By: Claude Opus 5 --- README.md | 132 ++++++++++++++++++++++------------------- directkeys/__init__.py | 44 ++++++++++---- 2 files changed, 102 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 860e5bd..655d6b7 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. +- **Raw event flags**: `KeyboardEvent.flags` carries the low-level hook flags. +- **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..60021b1 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. +- **Raw event flags**: `KeyboardEvent.flags` carries the low-level hook flags. +- **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/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 @@ -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() From 58fa3b28bb3fc4c9af8aa2bb49b709acfc9a64ac Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:29:33 -0400 Subject: [PATCH 2/7] refactor(windows): drop scaffolding comments and dead code from the backend The backend still carried editing instructions left over from when the AltGr support was pasted in ("COLE ESTAS DUAS FUNCOES DEPOIS DE...", "O resto do arquivo continua aqui...", "Esta funcao voce ja deve ter no final do arquivo"), which read as documentation to anyone else opening the file. Alongside that: - Remove `_remove_alt_gr_mapping`, which was never called. - Remove `shift_is_pressed` and `shift_vks`: the global was still being assigned on every shift event but nothing read it, since `get_modifiers` queries GetKeyState directly. Same for `ignore_next_right_alt`, only ever reset and never read. - Guard the `to_name` mutation in `get_name` with `tables_lock`, matching every other access to the tables. This also serialises the shared ctypes buffers used by `get_event_names`. - Fold the duplicated modifier vk lists in `force_reset_keyboard` and `get_stuck_keys` into a single `_modifier_vk_names` mapping. - Restore the two upstream comments explaining the `fake_alt` / VK_PACKET guard, and move the `__main__` block back to the end of the file. - Translate the new comments and docstrings to English, matching the rest of the codebase and the published API docs. No behaviour change. Co-Authored-By: Claude Opus 5 --- directkeys/_winkeyboard.py | 186 +++++++++++++++---------------------- 1 file changed, 74 insertions(+), 112 deletions(-) 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) From b6e15bcd0a74ced4655545201ca963a2c7c57f57 Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:30:39 -0400 Subject: [PATCH 3/7] docs(api): translate the new public API docstrings to English `set_alt_gr_abstraction`, `get_alt_gr_abstraction_state` and the fallback helpers are part of the published API: docstring2markdown renders them into README.md and setup.py ships the module docstring as the PyPI description, so they were the only Portuguese text in the user-facing documentation. Also correct the feature list: `KeyboardEvent.flags` is masked down to the LLKHF_EXTENDED bit on Windows, so describing it as the raw hook flags overstated what the attribute actually carries. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- directkeys/__init__.py | 45 ++++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 655d6b7..2923c87 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Take full control of your keyboard with this small Python library. Hook global e ### 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. -- **Raw event flags**: `KeyboardEvent.flags` carries the low-level hook flags. +- **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 diff --git a/directkeys/__init__.py b/directkeys/__init__.py index 60021b1..20111b6 100644 --- a/directkeys/__init__.py +++ b/directkeys/__init__.py @@ -25,7 +25,7 @@ ### 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. -- **Raw event flags**: `KeyboardEvent.flags` carries the low-level hook flags. +- **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 @@ -213,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 @@ -244,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 @@ -259,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. - 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. + 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. + + 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 From 267a5d43e5ce9798d802feee2bff9e4b52efcdfe Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:31:57 -0400 Subject: [PATCH 4/7] build: declare packaging metadata in pyproject.toml setup.py imported the package at build time to read the version and the docstring, which meant a build could fail before install_requires had a chance to provide pyobjc on macOS. Move all metadata to pyproject.toml and reduce setup.py to a shim so legacy invocations keep working. - Read the version statically via `[tool.setuptools.dynamic]`, keeping `directkeys.version` as the single source of truth without importing it. - Take long_description from README.md, which the Makefile already generates from the module docstring. - Declare requires-python >=3.8 to match the classifiers, add 3.13, and use the more specific "POSIX :: Linux" operating system classifier. - Point the Makefile at `python -m build`. Co-Authored-By: Claude Opus 5 --- MANIFEST.in | 3 ++- Makefile | 4 ++-- pyproject.toml | 44 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 40 +++++----------------------------------- 4 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 pyproject.toml 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/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() From 03e4393132bd3a16927540097415520f4da8e2b2 Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:32:23 -0400 Subject: [PATCH 5/7] docs: add the 1.0.0 changelog entry and the fork copyright CHANGES.md still started at 0.13.5, so the release that renamed the package and added the AltGr, stuck key and flags features had no entry at all. LICENSE.txt keeps the original BoppreH copyright, as the MIT terms require, with the fork's copyright added alongside it. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 23 +++++++++++++++++++++++ LICENSE.txt | 1 + 2 files changed, 24 insertions(+) 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 From c177894d977eb23853693631c9d182f0e61c2e82 Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:33:33 -0400 Subject: [PATCH 6/7] chore(tests): move the interactive scripts into tests/manual run_test.py and run_test_2a_crash.py sat at the repository root, where they read like the project's test entry point despite being interactive scripts that block on a human pressing keys on a physical keyboard. Neither keeps a `test_` prefix, so pytest cannot collect them: as test functions their `read_event()` calls would hang the suite waiting for input. The helper script is now resolved relative to __file__ rather than the current working directory, and a module docstring states what the scripts need to run. Co-Authored-By: Claude Opus 5 --- .../manual/altgr_and_stuck_keys.py | 19 ++++++++++++------- .../manual/simulate_stuck_key_crash.py | 0 2 files changed, 12 insertions(+), 7 deletions(-) rename run_test.py => tests/manual/altgr_and_stuck_keys.py (86%) rename run_test_2a_crash.py => tests/manual/simulate_stuck_key_crash.py (100%) 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 From 034efc4364d61963a71b5ca0910c7d01a433699a Mon Sep 17 00:00:00 2001 From: WigoWigo10 Date: Fri, 31 Jul 2026 22:34:58 -0400 Subject: [PATCH 7/7] ci: run the test suite on Windows and Linux The repository had no CI at all, which is an awkward gap for a fork whose pitch is that it is the maintained one. Adds a workflow that runs the suite on Windows and Linux across the Python versions declared in requires-python, plus a job that builds the distribution and runs `twine check` on it. tests/manual is excluded: those scripts need a physical keyboard and a human. Co-Authored-By: Claude Opus 5 --- .github/workflows/tests.yml | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/tests.yml 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/*