Skip to main content

keybinds

Flexible global keyboard & mouse hotkeys for Windows.

Python Platform License Status PyPI

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),
    ),
)

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

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.5
File Size Uploaded
keybinds-1.3.5.tar.gz 85.9 kB Details

Built distribution (wheel)

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

Total release size: 170.9 kB

Release files / keybinds-1.3.5.tar.gz

Download URL keybinds-1.3.5.tar.gz
Size 85.9 kB
Tags Source
SHA-256 checksum
How to use checksums
75de777ec6f456b5977d42f673bc517697d44868799f46b9a576b63a1c41fe6a
BLAKE2b-256 checksum
How to use checksums
46fd9ec3c117848b999955ed451c19c0ada3090a4c8eec9f499373634d7b7499
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.5-py3-none-any.whl

Download URL keybinds-1.3.5-py3-none-any.whl
Size 85.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4364dedc548e56739d5968600c29461df0d1be642ed1149d3d4b99077273504
BLAKE2b-256 checksum
How to use checksums
e2558462d28b1a35ecd3ceb88090c0b3cc5db204ab2fc8ab457b58399cd06fdb
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

1.3.6

2 release files

This release

1.3.5 This release

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