Enhance keyboard shortcut handling with debouncing and modifier key normalization

This commit is contained in:
jatinkrmalik
2025-04-07 18:03:35 -05:00
parent 161816af03
commit 551f590a56
+101 -29
View File
@@ -7,12 +7,12 @@ start/stop speech recognition with a keystroke.
import logging
import threading
import time
from typing import Callable, Dict, Optional, Tuple
# Try to import X11 keyboard libraries first
try:
from pynput import keyboard
from Xlib import display
KEYBOARD_AVAILABLE = True
except ImportError:
KEYBOARD_AVAILABLE = False
@@ -33,6 +33,7 @@ class KeyboardShortcutManager:
self.shortcuts = {}
self.listener = None
self.active = False
self.last_trigger_time = 0 # Track last trigger time to prevent double triggers
if not KEYBOARD_AVAILABLE:
logger.error("Keyboard shortcut libraries not available. Shortcuts will not work.")
@@ -58,13 +59,24 @@ class KeyboardShortcutManager:
# Track currently pressed modifier keys
self.current_keys = set()
# Start keyboard listener in a separate thread
self.listener = keyboard.Listener(
on_press=self._on_press,
on_release=self._on_release
)
self.listener.daemon = True
self.listener.start()
try:
# Start keyboard listener in a separate thread
self.listener = keyboard.Listener(
on_press=self._on_press,
on_release=self._on_release
)
self.listener.daemon = True
self.listener.start()
# Verify the listener started successfully
if not self.listener.is_alive():
logger.error("Failed to start keyboard listener")
self.active = False
else:
logger.info("Keyboard shortcut listener started successfully")
except Exception as e:
logger.error(f"Error starting keyboard listener: {e}")
self.active = False
def stop(self):
"""Stop listening for keyboard shortcuts."""
@@ -75,9 +87,13 @@ class KeyboardShortcutManager:
self.active = False
if self.listener:
self.listener.stop()
self.listener.join(timeout=1.0)
self.listener = None
try:
self.listener.stop()
self.listener.join(timeout=1.0)
except Exception as e:
logger.error(f"Error stopping keyboard listener: {e}")
finally:
self.listener = None
def register_shortcut(self, modifiers: set, key, callback: Callable):
"""
@@ -88,8 +104,14 @@ class KeyboardShortcutManager:
key: The main key (e.g., keyboard.KeyCode.from_char('v'))
callback: Function to call when the shortcut is pressed
"""
self.shortcuts[(frozenset(modifiers), key)] = callback
logger.debug(f"Registered shortcut: {modifiers} + {key}")
shortcut_key = (frozenset(modifiers), key)
self.shortcuts[shortcut_key] = callback
# Create readable description of the shortcut for logging
mod_names = [self._get_key_name(mod) for mod in modifiers]
key_name = self._get_key_name(key)
shortcut_desc = "+".join(mod_names + [key_name])
logger.info(f"Registered shortcut: {shortcut_desc}")
def register_toggle_callback(self, callback: Callable):
"""
@@ -101,6 +123,15 @@ class KeyboardShortcutManager:
modifiers, key = self.default_shortcut
self.register_shortcut(modifiers, key, callback)
def _get_key_name(self, key):
"""Get a readable name for a key object."""
if hasattr(key, 'char') and key.char:
return key.char.upper()
elif hasattr(key, 'name'):
return key.name
else:
return str(key)
def _on_press(self, key):
"""
Handle key press events.
@@ -108,18 +139,41 @@ class KeyboardShortcutManager:
Args:
key: The pressed key
"""
# Add to currently pressed keys
if hasattr(key, 'char'):
# Skip modifier tracking for character keys
pass
else:
self.current_keys.add(key)
# Check for shortcuts
for (modifiers, trigger_key), callback in self.shortcuts.items():
if self.current_keys == modifiers and key == trigger_key:
logger.debug(f"Shortcut triggered: {modifiers} + {trigger_key}")
callback()
try:
# Add to currently pressed keys (only for modifier keys)
if key in {
keyboard.Key.alt, keyboard.Key.alt_l, keyboard.Key.alt_r,
keyboard.Key.shift, keyboard.Key.shift_l, keyboard.Key.shift_r,
keyboard.Key.ctrl, keyboard.Key.ctrl_l, keyboard.Key.ctrl_r,
keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r
}:
# Normalize left/right variants
normalized_key = self._normalize_modifier_key(key)
self.current_keys.add(normalized_key)
# Check for shortcuts (only once per 500ms to prevent double triggers)
current_time = time.time()
if current_time - self.last_trigger_time > 0.5: # 500ms debounce
for (modifiers, trigger_key), callback in self.shortcuts.items():
# Normalize the trigger key for character keys
if isinstance(trigger_key, keyboard.KeyCode) and hasattr(trigger_key, 'char'):
if isinstance(key, keyboard.KeyCode) and hasattr(key, 'char'):
key_matches = key.char.lower() == trigger_key.char.lower()
else:
key_matches = False
else:
key_matches = key == trigger_key
# Check if modifiers match and key matches
normalized_modifiers = {self._normalize_modifier_key(m) for m in modifiers}
if self.current_keys == normalized_modifiers and key_matches:
logger.debug(f"Shortcut triggered: {modifiers} + {trigger_key}")
self.last_trigger_time = current_time
# Run callback in a separate thread to avoid blocking
threading.Thread(target=callback, daemon=True).start()
except Exception as e:
logger.error(f"Error in keyboard shortcut handling: {e}")
def _on_release(self, key):
"""
@@ -128,8 +182,26 @@ class KeyboardShortcutManager:
Args:
key: The released key
"""
# Remove from currently pressed keys
try:
self.current_keys.discard(key)
except KeyError:
pass # Key wasn't in the set
# Normalize the key for left/right variants
normalized_key = self._normalize_modifier_key(key)
# Remove from currently pressed keys
self.current_keys.discard(normalized_key)
except Exception as e:
logger.error(f"Error in keyboard release handling: {e}")
def _normalize_modifier_key(self, key):
"""Normalize left/right variants of modifier keys to their base form."""
# Map left/right variants to their base key
key_mapping = {
keyboard.Key.alt_l: keyboard.Key.alt,
keyboard.Key.alt_r: keyboard.Key.alt,
keyboard.Key.shift_l: keyboard.Key.shift,
keyboard.Key.shift_r: keyboard.Key.shift,
keyboard.Key.ctrl_l: keyboard.Key.ctrl,
keyboard.Key.ctrl_r: keyboard.Key.ctrl,
keyboard.Key.cmd_l: keyboard.Key.cmd,
keyboard.Key.cmd_r: keyboard.Key.cmd
}
return key_mapping.get(key, key)