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 .

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.2)

  • 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.2.tar.gz (282.9 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.2-py3-none-any.whl (278.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pykeybindmanager-0.2.2.tar.gz
  • Upload date:
  • Size: 282.9 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.2.tar.gz
Algorithm Hash digest
SHA256 8093039d43df41140ed779011eec1fb2c1b9882f0a5562feaf18b698e8f1327f
MD5 09cc5d89e7783192ad545250bfc8ef5a
BLAKE2b-256 0cb84304c74ca1b83399f581957f03fa1b5de0b84b92e368a7f599df382b3a0f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for pykeybindmanager-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 733afc15fdc9e501e328507eb62fc7e19f3b90b9e5e226c61fd10fc538006f86
MD5 0593c0b91d7ce35dab483584ab972e80
BLAKE2b-256 c32022a700a57cafd95af5349ed08e39834aa7f1b0b503b3841c966fc47ee68c

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