Flexible and high-performance keyboard & mouse hotkeys for Windows
Project description
keybinds
Flexible and high-performance keyboard & mouse hotkeys for Windows.
keybinds is a Python library for building fully customizable global keybinds and mouse binds using low-level Windows hooks.
It supports chords (ctrl+e), sequences (g,k,i), rich triggers (press / release / hold / repeat / double tap), strict constraints, suppress/injected policies, and user-defined checks — while keeping the API clean and configuration-driven.
Lightweight. Powered by winput for reliable input suppression and precise control.
✨ Features
Keyboard
- Single keys:
k,f1,space - Chords:
ctrl+e,ctrl+shift+x - Sequences:
g,k,i
Mouse
- Buttons:
left,right,middle,x1,x2
Triggers
ON_PRESSON_RELEASEON_CLICKON_HOLDON_REPEATON_DOUBLE_TAPON_SEQUENCEON_CHORD_RELEASED
Advanced
- Input suppression (block events from reaching apps)
- Injected policy: control whether synthetic (e.g. macro) events are handled or ignored.
- Strict chords
- Timing controls (hold/delay/intervals/windows)
- Predicates / checks
- Clean Config + Enum design
- Decorator support
- Very fast hook path (callbacks run outside hook thread)
Performance (examples/benchmark.py): p50 ~0.21 ms, p99 ~0.35 ms, max <0.7ms (rarely 3–5 ms).
🚀 Installation
From PyPI
pip install keybinds
Requirements
- Windows
- Python 3.9+
winput(bundled)
⚡ Quick Start
import time
from keybinds.bind import Hook
hook = Hook()
hook.bind("ctrl+e", lambda: print("Inventory"))
hook.bind_mouse("left", lambda: print("Fire"))
hook.join()
📦 Examples
Run any example directly:
python examples/quickstart.py
python examples/decorators.py
python examples/examples_presets.py
python examples/manual_test_all.py
Usage
Keyboard
Simple press
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)
hook.bind(
"space",
lambda: print("Tick"),
config=BindConfig(
trigger=Trigger.ON_REPEAT,
timing=Timing(hold_ms=200, repeat_interval_ms=80)
)
)
Double tap
hook.bind(
"g",
lambda: print("Dash"),
config=BindConfig(
trigger=Trigger.ON_DOUBLE_TAP
)
)
Sequence
hook.bind(
"g,k,i",
lambda: print("Secret combo"),
config=BindConfig(trigger=Trigger.ON_SEQUENCE)
)
Mouse
from keybinds.types import MouseBindConfig, Trigger
hook.bind_mouse(
"middle",
lambda: print("Middle pressed"),
config=MouseBindConfig(trigger=Trigger.ON_PRESS)
)
Suppress (block input)
Prevent the event from reaching applications:
from keybinds.types import SuppressPolicy
hook.bind(
"ctrl+r",
lambda: print("Reload"),
config=BindConfig(
suppress=SuppressPolicy.WHEN_MATCHED
)
)
Policies:
| Policy | Behavior |
|---|---|
| NEVER | never suppress |
| WHEN_MATCHED | suppress only when callback fires |
| WHILE_ACTIVE | suppress while chord active |
| WHILE_EVALUATING | suppress while matching |
| ALWAYS | always suppress |
Injected (synthetic) events
Control how synthetic / injected input (macros, SendInput, other tools) is handled:
from keybinds.types import InjectedPolicy
hook.bind(
"f1",
callback,
config=BindConfig(injected=InjectedPolicy.IGNORE)
)
| Policy | Behavior |
|---|---|
| ALLOW | handle both physical and injected input |
| IGNORE | ignore injected completely |
| ONLY | react only to injected events |
Strict chord
Require exact keys only:
from keybinds.types import Constraints, ChordPolicy
hook.bind(
"ctrl+shift+u",
lambda: print("Strict"),
config=BindConfig(
constraints=Constraints(chord_policy=ChordPolicy.STRICT)
)
)
Checks / Predicates
Add additional conditions to a keybind:
from keybinds.types import Checks
hook.bind(
"f1",
callback,
config=keybinds.BindConfig(
checks=lambda event, state: event.extra_info == 0xDEADBEEF
# checks=[check1, check2]
# checks=Checks([check1, check2])
)
)
Decorators
Cleaner syntax:
from keybinds.decorators import bind_key, bind_mouse
@bind_key("ctrl+e")
def inventory():
print("Inventory")
@bind_mouse("left")
def fire():
print("Bang")
Presets & Profiles
If you don't want to write BindConfig(...) / MouseBindConfig(...) everywhere, use presets:
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))
Ready-to-use profiles (practical bundles)
Profiles bundle multiple configs for common patterns.
Tap vs Hold on the same key
from keybinds.presets import tap_hold
th = tap_hold(tap_ms=220, hold_ms=450)
hook.bind("k", lambda: print("tap"), config=th.tap)
hook.bind("k", lambda: print("hold"), config=th.hold)
Push-to-talk (press = ON, release = OFF)
from keybinds.presets import ptt
p = ptt(suppress=True) # suppress while held (WHILE_ACTIVE)
hook.bind("v", lambda: print("PTT ON"), config=p.press)
hook.bind("v", lambda: print("PTT OFF"), config=p.release)
Mouse auto-fire (repeat while held)
from keybinds.presets import game_autofire
hook.bind_mouse(
"left",
lambda: print("tick"),
config=game_autofire(delay_ms=150, interval_ms=60, suppress=True),
)
Config composition
Use operators to combine configs:
+→ apply only changed fields (patch)|→ overwrite everything (force)
cfg = presets.ignore_injected() + BindConfig(suppress=SuppressPolicy.WHILE_ACTIVE)
cfg = cfg | BindConfig(suppress=SuppressPolicy.NEVER)
Timing Configuration
Timing(
hold_ms=400, # time (ms) the key must be held before ON_HOLD fires
repeat_delay_ms=200, # delay (ms) after press before ON_REPEAT starts
repeat_interval_ms=80, # interval (ms) between repeat ticks while held
double_tap_window_ms=300, # max time (ms) between two presses to count as a double tap
window_focus_cache_ms=50, # how long (ms) the active window is cached (fewer OS checks, better performance)
chord_timeout_ms=500, # max time (ms) allowed to finish a chord/sequence before it resets
cooldown_ms=100, # minimum time (ms) after a trigger during which new triggers are ignored (anti-spam)
debounce_ms=0 # ignore events occurring too close together (filters key bounce/noise)
)
FAQ
❓ What platforms are supported?
Windows only. Uses low-level WinAPI hooks via winput.
❓ What’s the difference between ON_RELEASE and ON_CHORD_RELEASED?
ON_RELEASE
Fires when any key in the chord is released after it was fully pressed.
Example:
Ctrl down
E down (full)
E up → fires
ON_CHORD_RELEASED
Fires only when all chord keys are released.
Example:
Ctrl down
E down
E up → no
Ctrl up → fires
Use:
ON_RELEASE→ immediate reactionON_CHORD_RELEASED→ finished gesture
❓ Why do some keys (like "`") fail to parse?
Key expressions are token-based. Letters/digits work out of the box, but punctuation often maps to OEM keys (layout-dependent).
If you need them, add a mapping for that token -> register_key_token(name, vk).
❓ Why can input feel laggy sometimes?
Common causes:
- heavy callbacks (sleep/IO/printing too much)
- too many repeat events
- blocking inside hook
Keep callbacks fast and lightweight.
❓ Can suppress break my input (mouse stops clicking / keys feel blocked)?
Yes — suppression is powerful.
SuppressPolicy.WHEN_MATCHEDis the safest default.- Avoid
SuppressPolicy.ALWAYSunless you know exactly what you're doing. - For mouse
ON_RELEASEbinds, some apps require suppressing both DOWN and the matching UP to fully block a click.
❓ Are callbacks threaded?
Yes. Callbacks are executed outside the low-level hook to avoid input lag. Avoid shared mutable state or protect it with locks.
❓ Can I dynamically enable/disable binds?
Yes.
You can keep references to Bind / MouseBind objects and register/unregister them manually via the Hook.
Best Practices
✅ Keep callbacks short
✅ Use timing configs for UX
✅ Prefer WHEN_MATCHED suppress
❌ Avoid blocking/sleeping inside callbacks
License
MIT License
Third-party components
This project bundles a modified copy of winput Copyright (c) 2017 Zuzu_Typ Licensed under the zlib/libpng license.
Changes made:
- x64 hook ABI fixes
- proper WINFUNCTYPE callbacks
- correct WinAPI signatures
- injected / lower_il_injected detection
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keybinds-0.0.3.tar.gz.
File metadata
- Download URL: keybinds-0.0.3.tar.gz
- Upload date:
- Size: 32.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfc858c5e96e9275d0514451623f8b8acfbfd33ff2b584f676c012f455757993
|
|
| MD5 |
6e6c8648693b0f9d6a69d3b53c055ec3
|
|
| BLAKE2b-256 |
c68f1c247559c2318ff0fee548db7784c5f5c05246181707c5aa9c68a452da63
|
File details
Details for the file keybinds-0.0.3-py3-none-any.whl.
File metadata
- Download URL: keybinds-0.0.3-py3-none-any.whl
- Upload date:
- Size: 34.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6ce2980bbb52ec27f0433abc454d0ebeefd58cdc39432ec112c4f44c5d888a3
|
|
| MD5 |
8348591dd756abb44714603e64bf34b8
|
|
| BLAKE2b-256 |
7cbd9eb0f5f634a58946de4b1c184ec0df80a6939ab1875052dc63348d874987
|