Skip to main content

A simple Python module for listening to keyboard keybinds (single keys or combinations) with toggle, double-press, and hold support.

Project description

PyKeybindManager

A simple Python module for listening to specific keyboard keybinds (single keys or combinations) using the pynput library. Supports toggle, double-press toggle, and press-and-hold activation types, suitable for applications like dictation control. Includes optional sound feedback.

Features

  • Listen for specific keyboard keys (e.g., F1, fn) or combinations (e.g., Ctrl+C, Cmd+Shift+P).
  • Supports multiple activation modes via trigger_type:
    • 'toggle': Activate on each press of the key/combination.
    • 'double_press_toggle': Activate differently for single vs. double presses (single keys only).
    • 'hold': Activate on press and again on release of the key/combination.
  • Run the listener in a background thread.
  • Trigger a user-defined callback function with the event type ('press', 'release', 'single', 'double').
  • Provide optional sound feedback ('start'/'stop' sounds) using platform-specific methods.
  • Helper function (parse_keybind_string) to easily convert keybind strings (e.g., "alt+t", "f1") into the required internal format.
  • Handles macOS-specific considerations like Input Monitoring permissions and the 'fn' key.
  • Minimal logging by default; relies on the application to configure logging levels.

Requirements

  • Python 3.x
  • pynput library (pip install pynput)

Platform Notes:

  • macOS: Requires "Input Monitoring" permission for the application/terminal running the script. The script attempts to handle the OBJC_DISABLE_INITIALIZE_FORK_SAFETY environment variable needed by pynput on macOS. The fn key is supported. cmd key is mapped correctly.
  • Linux: May require root privileges depending on the environment, or the user needs to be in the input group. meta key is mapped to ctrl.
  • Windows: Should generally work without special permissions. meta key is mapped to ctrl.
    • Sound Playback: Relies on common system utilities (afplay on macOS, winsound on Windows, aplay/paplay/mplayer/mpg123 on Linux). If these are not available, sound playback might fail silently or log a warning.

Installation

Install the package from PyPI using pip:

pip install pykeybindmanager

This will also automatically install the required pynput dependency.

Development Installation

If you have cloned the repository and want to install it for development (e.g., to make changes):

# Navigate to the repository root directory (where pyproject.toml is)
pip install -e .
pip install pynput

Usage

import time
import logging
import sys
from pykeybindmanager import KeybindManager, parse_keybind_string, play_sound_file
from pykeybindmanager.exceptions import PermissionError, PynputImportError, InvalidKeybindError, ListenerError

# --- Application State (Example for Dictation) ---
is_recording = False

# --- Configure Logging (Optional) ---
# The library uses NullHandler by default. Configure if you want to see logs.
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__) # Get logger for application messages

# --- Callback Functions ---

def handle_toggle_activation(event_type):
    """Handles 'toggle' type activation (e.g., press Ctrl+D to start/stop)."""
    global is_recording
    if event_type == 'press':
        is_recording = not is_recording
        status = "STARTED" if is_recording else "STOPPED"
        sound = 'start' if is_recording else 'stop'
        play_sound_file(sound)
        log.info(f"Toggle Keybind Pressed: Recording {status}")

def handle_double_press_activation(event_type):
    """Handles 'double_press_toggle' type activation (e.g., double-press F1 to start/stop)."""
    global is_recording
    # Example: Only toggle on double press
    if event_type == 'double':
        is_recording = not is_recording
        status = "STARTED" if is_recording else "STOPPED"
        sound = 'start' if is_recording else 'stop'
        play_sound_file(sound)
        log.info(f"Double Press Detected: Recording {status}")
    elif event_type == 'single':
        log.info("Single press detected (ignored by this handler).")

def handle_hold_activation(event_type):
    """Handles 'hold' type activation (e.g., hold 'fn' to record)."""
    global is_recording
    if event_type == 'press':
        if not is_recording:
            is_recording = True
            play_sound_file('start')
            log.info("Hold Key Pressed: Recording STARTED")
    elif event_type == 'release':
        if is_recording:
            is_recording = False
            play_sound_file('stop')
            log.info("Hold Key Released: Recording STOPPED")

def handle_error(exception):
    """Handles errors from the KeybindManager."""
    log.error(f"KeybindManager Error: {type(exception).__name__} - {exception}")
    if isinstance(exception, PermissionError):
        log.error("Please ensure the application has Input Monitoring permissions (macOS) or necessary privileges.")
    # Consider exiting or notifying the user based on the error

# --- Main Logic ---
if __name__ == "__main__":
    managers = []
    try:
        # --- Define Keybinds ---
        # Example 1: Toggle recording with Ctrl+D
        kb1_str = "ctrl+d"
        kb1_def = parse_keybind_string(kb1_str)
        manager1 = KeybindManager(kb1_def, handle_toggle_activation, trigger_type='toggle', on_error=handle_error)
        managers.append(manager1)
        log.info(f"Registered '{kb1_str}' with trigger 'toggle'")

        # Example 2: Double-press F1 to toggle recording
        kb2_str = "f1"
        kb2_def = parse_keybind_string(kb2_str)
        manager2 = KeybindManager(kb2_def, handle_double_press_activation, trigger_type='double_press_toggle', on_error=handle_error)
        managers.append(manager2)
        log.info(f"Registered '{kb2_str}' with trigger 'double_press_toggle'")

        # Example 3: Hold 'fn' key to record (macOS specific)
        if sys.platform == 'darwin':
            kb3_str = "fn"
            try:
                kb3_def = parse_keybind_string(kb3_str)
                manager3 = KeybindManager(kb3_def, handle_hold_activation, trigger_type='hold', on_error=handle_error)
                managers.append(manager3)
                log.info(f"Registered '{kb3_str}' with trigger 'hold'")
            except InvalidKeybindError as e:
                 log.warning(f"Could not register 'fn' key: {e}") # Might fail if pynput doesn't map vk 179

        # --- Start Listeners ---
        log.info("Starting listeners... Press Ctrl+C to exit.")
        for manager in managers:
            manager.start_listener()

        # Keep the main script running
        while True:
            time.sleep(1)

    except (PynputImportError, InvalidKeybindError, ValueError, ListenerError) as e:
        log.error(f"Initialization Error: {e}")
    except KeyboardInterrupt:
        log.info("KeyboardInterrupt received. Stopping listeners...")
    except Exception as e:
        log.error(f"An unexpected error occurred: {e}", exc_info=True)
    finally:
        # --- Stop Listeners Gracefully ---
        for manager in managers:
            manager.stop_listener()
        log.info("All listeners stopped.")

Key Concepts (v0.2.0)

  • parse_keybind_string(keybind_string):
    • Input: A string like "f1", "ctrl+c", "alt+shift+t", "fn". Uses + as a separator.
    • Output: A tuple (frozenset[modifier_keys], main_key). Modifiers are pynput.keyboard.Key objects (e.g., Key.ctrl). The main key is a Key or KeyCode object.
  • KeybindManager(keybind_definition, on_activated, trigger_type, on_error=None, double_press_threshold=0.3):
    • keybind_definition: The tuple returned by parse_keybind_string.
    • on_activated: Your callback function. Receives one argument: the event type string ('press', 'release', 'single', or 'double').
    • trigger_type: Specifies the activation behavior. Must be one of:
      • 'toggle': Callback gets 'press' on activation. Good for start/stop actions triggered by the same key/combo.
      • 'double_press_toggle': Callback gets 'single' or 'double'. Only valid for single keys (no modifiers). Good for distinguishing single vs. double taps.
      • 'hold': Callback gets 'press' when the key/combo goes down, and 'release' when the main key comes up. Ideal for push-to-talk/record.
    • on_error: Optional function to handle errors (like permission issues). Receives the exception object.
    • double_press_threshold: Time in seconds for 'double_press_toggle' detection (default: 0.3s).
  • manager.start_listener(): Starts listening in the background.
  • manager.stop_listener(): Stops the background listener thread.
  • play_sound_file(sound_type, blocking=False): Plays a sound.
    • sound_type: Either 'start' (plays doubleping.wav) or 'stop' (plays singleping.wav).
    • blocking: If True, waits for the sound to finish. Defaults to False.

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pykeybindmanager-0.2.1.tar.gz (6.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pykeybindmanager-0.2.1-py3-none-any.whl (5.6 kB view details)

Uploaded Python 3

File details

Details for the file pykeybindmanager-0.2.1.tar.gz.

File metadata

  • Download URL: pykeybindmanager-0.2.1.tar.gz
  • Upload date:
  • Size: 6.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.2

File hashes

Hashes for pykeybindmanager-0.2.1.tar.gz
Algorithm Hash digest
SHA256 55fc0ba780aa2d6fe2dc4bed0d9422240fa1b3a7075187362ed07ecf24b10008
MD5 05f20362ecc9abb50b1a9c8d5d06b247
BLAKE2b-256 72e814f577a27fa379c2dae6a8b6e71919285f5677a4ec5c5f55759642857041

See more details on using hashes here.

File details

Details for the file pykeybindmanager-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for pykeybindmanager-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2a4fa4796025cc6b09f93ff5c16549cb7fa6a50ba2be8e27e9a9f7c2ee7cb5e7
MD5 28789ef9cd964b4858256e290e035ab0
BLAKE2b-256 6b0d92829263f60782a412136530e1e5a4cb9e21dfc4d726b29d98551b83ae14

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page