keybinds
Flexible global keyboard & mouse hotkeys for Windows.
keybinds is a Python library for global keyboard and mouse binds on Windows, with support for hotkeys, logical key binds, text matching, and text abbreviations.
It supports:
- global keyboard binds: single keys, chords (
ctrl+e), and sequences (g,k,i) - global mouse binds:
left,right,middle,x1,x2 - rich triggers: press, release, click, hold, repeat, double tap, and sequence
- suppression and injected-input policies
- sync and async callbacks
- decorator helpers and a simple API
- experimental logical binds, typed-text matching, and text abbreviations
- unified unbind helpers for bind handles, decorated functions, and bind collections
Powered by a bundled/modified winput for reliable input suppression and precise control.
Installation
pip install keybinds
Requirements
- Windows
- Python 3.7+
typing_extensionson Python 3.7
Quick Start
from keybinds import Hook
hook = Hook()
hook.bind("ctrl+e", lambda: print("Inventory"))
hook.bind_mouse("left", lambda: print("Fire"))
hook.join()
Decorator Style
Stable decorator helpers:
import keybinds
from keybinds.decorators import bind_key, bind_mouse
@bind_key("ctrl+e")
def inventory():
print("Inventory")
@bind_mouse("left")
def fire():
print("Bang")
keybinds.join()
Decorators use the default hook automatically.
Experimental logical/text decorators
import keybinds
from keybinds.decorators import bind_logical, bind_text, bind_abbreviation
# Experimental API: may change in future releases.
@bind_logical("ctrl+A")
def logical_inventory():
print("Logical inventory")
@bind_text("hello")
def saw_hello():
print("Hello typed")
@bind_abbreviation("brb", "be right back")
def expanded_brb():
print("Expanded brb")
keybinds.join()
For non-decorator usage, keybinds.add_abbreviation(...) is also available.
When to use what
bind(...)— regular keyboard shortcuts based on key expressionsbind_mouse(...)— mouse button bindsbind_logical(...)— experimental layout-aware logical key bindsbind_text(...)— experimental typed-text matchingadd_abbreviation(...)/bind_abbreviation(...)— experimental text expansion
For normal shortcuts, prefer bind(...). For typed text, prefer bind_text(...) instead of text-like expressions passed to bind_logical(...).
Unbinding
Bind methods return bind handles directly:
b = hook.bind("ctrl+e", callback)
hook.unbind(b)
Decorators attach created bind handles to the function:
func.bindsalways contains a list of all created bind objectsfunc.bindis kept as a compatibility alias:- single bind →
func.bindis that bind object - multiple binds →
func.bindis a list of bind objects
- single bind →
You can unbind by handle, by function, or by a bind collection:
hook.unbind(my_callback)
hook.unbind(my_callback.binds)
hook.unbind(my_callback.bind)
keybinds.unbind(my_callback)
Top-level helpers use the default hook. If you use multiple hooks, prefer hook.unbind(...) on the specific hook.
Bind state and waiting
All bind objects expose small runtime helpers:
if bind.is_pressed():
print("full chord is currently held")
if bind.any_pressed():
print("at least one key/button of this bind is held")
print(bind.pressed_keys()) # held VKs for this bind, or [button] for mouse
bind.wait() # wait until the bind fires
bind.wait(0.5) # wait up to 0.5s, returns True/False
is_pressed()— the full chord is held.any_pressed()— any key/button of this bind is held.pressed_keys()— those keys (VKs, or the mouse button).wait(timeout=None)— block until the bind fires (Falseon timeout).
Keyboard Basics
Press (default)
hook.bind("ctrl+e", lambda: print("Pressed"))
Release
from keybinds.types import BindConfig, Trigger
hook.bind(
"ctrl+t",
lambda: print("Released"),
config=BindConfig(trigger=Trigger.ON_RELEASE),
)
Hold
from keybinds.types import BindConfig, Trigger, Timing
hook.bind(
"h",
lambda: print("Held"),
config=BindConfig(
trigger=Trigger.ON_HOLD,
timing=Timing(hold_ms=400),
),
)
Repeat (auto-fire while held)
from keybinds.types import BindConfig, Trigger, Timing
hook.bind(
"space",
lambda: print("Tick"),
config=BindConfig(
trigger=Trigger.ON_REPEAT,
timing=Timing(hold_ms=200, repeat_interval_ms=80),
),
)
Double tap
from keybinds.types import BindConfig, Trigger
hook.bind(
"g",
lambda: print("Dash"),
config=BindConfig(trigger=Trigger.ON_DOUBLE_TAP),
)
Sequence
from keybinds.types import BindConfig, Trigger
hook.bind(
"g,k,i",
lambda: print("Secret combo"),
config=BindConfig(trigger=Trigger.ON_SEQUENCE),
)
Mouse Basics
from keybinds.types import MouseBindConfig, Trigger
hook.bind_mouse(
"middle",
lambda: print("Middle pressed"),
config=MouseBindConfig(trigger=Trigger.ON_PRESS),
)
Mouse buttons:
leftrightmiddlex1x2
Suppression (block input from apps)
from keybinds.types import BindConfig, SuppressPolicy
hook.bind(
"ctrl+r",
lambda: print("Reload"),
config=BindConfig(suppress=SuppressPolicy.WHEN_MATCHED),
)
Policies:
NEVERWHEN_MATCHEDWHILE_ACTIVEWHILE_EVALUATINGALWAYS
Injected (synthetic) input policy
Control how injected input (macros, SendInput, automation tools) is handled:
from keybinds.types import BindConfig, InjectedPolicy
hook.bind(
"f1",
lambda: print("Only physical"),
config=BindConfig(injected=InjectedPolicy.IGNORE),
)
Policies:
ALLOWIGNOREONLY
Strict Chords
from keybinds.types import BindConfig, Constraints, ChordPolicy
hook.bind(
"ctrl+shift+u",
lambda: print("Strict"),
config=BindConfig(
constraints=Constraints(chord_policy=ChordPolicy.STRICT)
),
)
Checks / Predicates
def not_injected(event, state):
return not event.injected
hook.bind("f1", lambda: print("Checked"), checks=not_injected)
You can also pass:
- a single callable
- a list/tuple of callables
Checks(...)
Async Callbacks
Callbacks may be async def. If a callback returns an awaitable, keybinds schedules it on an asyncio loop.
import asyncio
from keybinds import Hook
from keybinds.decorators import bind_key
hook = Hook()
@bind_key("f1", hook=hook)
async def ping():
await asyncio.sleep(0.1)
print("async ok")
hook.join()
If your app already runs its own event loop, pass it via Hook(asyncio_loop=...) and avoid calling blocking join() on that thread.
Presets (shortcut configs)
from keybinds.presets import press, release, click, hold, repeat, double_tap, sequence
hook.bind("ctrl+e", lambda: print("press"), config=press())
hook.bind("ctrl+e", lambda: print("release"), config=release())
hook.bind("k", lambda: print("tap"), config=click(220))
hook.bind("k", lambda: print("hold"), config=hold(450))
hook.bind("space", lambda: print("tick"), config=repeat(delay_ms=200, interval_ms=80))
hook.bind("d", lambda: print("dash"), config=double_tap(window_ms=250))
hook.bind("g,k,i", lambda: print("combo"), config=sequence(timeout_ms=600))
Simple API
For common cases, use the lightweight decorator wrapper:
from keybinds.simple import hotkey, mouse, run
@hotkey("ctrl+e")
def inventory():
print("Inventory")
@hotkey("space", repeat=80, delay=200)
def autofire():
print("Bang")
@hotkey("f", hold=400)
def charge():
print("Charged")
@mouse("left")
def on_left():
print("Mouse pressed")
run()
Supports common patterns with simple flags:
release=Truehold=400repeat=80(optionallydelay=200)sequence=True(optionallytimeout=600)double_tap=Truesuppress=True
Documentation
- Advanced Usage.md — advanced triggers, constraints, suppression, callbacks, and API details
- Diagnostics.md — troubleshooting and runtime introspection
- Logical Binds and Abbreviations.md — experimental logical binds, typed text, and abbreviations
- Developer Notes - Logical.md — developer-oriented notes on logical internals
Notes
Logical binds, typed-text matching, and abbreviations are currently experimental. The API and behavior may still change in future releases.
Suppression limitations
On Windows, suppression depends on low-level hook order. See Advanced Usage — Hook chain limitations and reinstall_hooks().
License
MIT License
Third-party Components
This project bundles a modified copy of winput (originally by Zuzu_Typ, zlib/libpng license), extended so keybinds can detect injected keyboard and mouse hook events.
The original license text is included in keybinds/winput/LICENSE.
Contributing
PRs and issues are welcome:
- bug fixes
- performance improvements
- new triggers
- documentation
- examples
⭐ If you like it
Star the repo — it helps a lot.
Metadata
Release files for keybinds 1.3.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keybinds-1.3.4.tar.gz | 83.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keybinds-1.3.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 167.2 kB
Release files / keybinds-1.3.4.tar.gz
| Download URL | keybinds-1.3.4.tar.gz |
|---|---|
| Size | 83.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
839ba8a6e1f313115ff7d95a459aaa04df58cec19fe66867fa62cf9d3a272e9c
|
|
BLAKE2b-256 checksum How to use checksums |
da5c052f9fa2a0e9eea4284d720cc659f2d1b630a45eb4c68fd58d225f56d24f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.0
|
Release files / keybinds-1.3.4-py3-none-any.whl
| Download URL | keybinds-1.3.4-py3-none-any.whl |
|---|---|
| Size | 83.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee26c4ad435044ed06e678922f69718ccd66c68b7bca2c9405ade828d34ed706
|
|
BLAKE2b-256 checksum How to use checksums |
5a454be65904db02e1dc2c4639dd70d23d9ee974713838dd6e1910ffeaf9c7a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.0
|