Skip to main content

keybinds

Flexible global keyboard & mouse hotkeys for Windows.

Python Platform License Status PyPI Vibe-coded

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, wheel_up, wheel_down
  • 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_extensions on 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 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 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 expressions
  • bind_mouse(...) — mouse button binds
  • bind_logical(...) — experimental layout-aware logical key binds
  • bind_text(...) — experimental typed-text matching
  • add_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.binds always contains a list of all created bind objects
  • func.bind is kept as a compatibility alias:
    • single bind → func.bind is that bind object
    • multiple binds → func.bind is a list of bind objects

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 (False on timeout).

Keyboard Basics

Press (default)

hook.bind("ctrl+e", lambda: print("Pressed"))

Release

from keybinds import BindConfig, Trigger

hook.bind(
    "ctrl+t",
    lambda: print("Released"),
    config=BindConfig(trigger=Trigger.ON_RELEASE),
)

Hold

from keybinds import BindConfig, Trigger, Timing

hook.bind(
    "h",
    lambda: print("Held"),
    config=BindConfig(
        trigger=Trigger.ON_HOLD,
        timing=Timing(hold_ms=400),
        restore_state_on_bind=True,
    ),
)

Already-held input is ignored by default; use restore_state_on_bind=True to include it, or call bind.restore_state() manually.

Repeat (auto-fire while held)

from keybinds 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 import BindConfig, Trigger

hook.bind(
    "g",
    lambda: print("Dash"),
    config=BindConfig(trigger=Trigger.ON_DOUBLE_TAP),
)

Sequence

from keybinds import BindConfig, Trigger

hook.bind(
    "g,k,i",
    lambda: print("Secret combo"),
    config=BindConfig(trigger=Trigger.ON_SEQUENCE),
)

Mouse Basics

from keybinds import MouseBindConfig, Trigger

hook.bind_mouse(
    "middle",
    lambda: print("Middle pressed"),
    config=MouseBindConfig(trigger=Trigger.ON_PRESS),
)

Mouse buttons:

  • left
  • right
  • middle
  • x1
  • x2

Mouse wheel (only Trigger.ON_PRESS):

  • wheel_up / wheel_down
  • wheel_left / wheel_right — horizontal (many mice never emit these)

Wheel units accumulate until MouseBindConfig.wheel_delta (default 120, the Win32 notch). Smaller wheel steps add up until they reach wheel_delta. Scrolling the other way clears the leftover amount.

hook.bind_mouse("wheel_up", lambda: print("scroll up"))
hook.bind_mouse(
    "wheel_down",
    lambda: print("scroll down"),
    config=MouseBindConfig(wheel_delta=40),
)

Suppression (block input from apps)

from keybinds import BindConfig, SuppressPolicy

hook.bind(
    "ctrl+r",
    lambda: print("Reload"),
    config=BindConfig(suppress=SuppressPolicy.WHEN_MATCHED),
)

Policies:

  • NEVER
  • WHEN_MATCHED
  • WHILE_ACTIVE
  • WHILE_EVALUATING
  • ALWAYS

Injected (synthetic) input policy

Control how injected input (macros, SendInput, automation tools) is handled:

from keybinds import BindConfig, InjectedPolicy

hook.bind(
    "f1",
    lambda: print("Only physical"),
    config=BindConfig(injected=InjectedPolicy.IGNORE),
)

Policies:

  • ALLOW
  • IGNORE
  • ONLY

Strict Chords

from keybinds 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, 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=True
  • hold=400
  • repeat=80 (optionally delay=200)
  • sequence=True (optionally timeout=600)
  • double_tap=True
  • suppress=True

Documentation


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.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keybinds 1.3.6
File Size Uploaded
keybinds-1.3.6.tar.gz 90.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keybinds 1.3.6
File Interpreter ABI Platform
keybinds-1.3.6-py3-none-any.whl Python 3 none any Details

Total release size: 178.1 kB

Release files / keybinds-1.3.6.tar.gz

Download URL keybinds-1.3.6.tar.gz
Size 90.1 kB
Tags Source
SHA-256 checksum
How to use checksums
247e1f31b4a53cf7c7a4a6d7f199edada20428a69c31cb7e924628aab64cecca
BLAKE2b-256 checksum
How to use checksums
7726cddf803c2f489c8d9ecae014e79360548b7a228a0e1be7591d1bd5d53dc5
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.6-py3-none-any.whl

Download URL keybinds-1.3.6-py3-none-any.whl
Size 88.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2b020fbae05f4567f566ee0c9f78383ed6e18c4049b854241c3da860958c903f
BLAKE2b-256 checksum
How to use checksums
ebbd15053da4dc07970f954373ce5625a0379cf916fa672f1c3b9b325a19af74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.0

Release history Release notifications | RSS feed

This release

1.3.6 This release

2 release files

1.3.5

2 release files

1.3.4

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.0.4

2 release files

0.0.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page