From 551f590a564ceb1cce6d49997b30f82164c40d6a Mon Sep 17 00:00:00 2001 From: jatinkrmalik Date: Mon, 7 Apr 2025 18:03:35 -0500 Subject: [PATCH] Enhance keyboard shortcut handling with debouncing and modifier key normalization --- src/ui/keyboard_shortcuts.py | 130 +++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 29 deletions(-) diff --git a/src/ui/keyboard_shortcuts.py b/src/ui/keyboard_shortcuts.py index 0d9a90f..26977b1 100644 --- a/src/ui/keyboard_shortcuts.py +++ b/src/ui/keyboard_shortcuts.py @@ -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 \ No newline at end of file + # 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) \ No newline at end of file