Enhance keyboard shortcut handling with debouncing and modifier key normalization
This commit is contained in:
+101
-29
@@ -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)
|
||||
Reference in New Issue
Block a user