Simplify Keyboard Shortcuts to Exclusive Double-tap Ctrl Implementation (#9)
* Implement double-tap Ctrl keyboard shortcut feature * Simplify keyboard shortcuts to only use double-tap Ctrl * Update docs * Fix syntax * Fix CI build by adding portaudio19-dev dependency
This commit is contained in:
@@ -75,7 +75,8 @@ jobs:
|
||||
libgirepository1.0-dev \
|
||||
gobject-introspection \
|
||||
libcairo-gobject2 \
|
||||
gir1.2-gtk-3.0
|
||||
gir1.2-gtk-3.0 \
|
||||
portaudio19-dev
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v3
|
||||
|
||||
@@ -17,7 +17,7 @@ A seamless voice dictation system for Linux, comparable to the built-in solution
|
||||
|
||||
Vocalinux provides a user-friendly speech-to-text solution for Linux users with:
|
||||
|
||||
- Activation via customizable keyboard shortcuts (default: Alt+Shift+V)
|
||||
- Activation via double-tap Ctrl keyboard shortcut
|
||||
- Real-time transcription with minimal latency
|
||||
- Universal compatibility across applications
|
||||
- Offline operation for privacy and reliability
|
||||
@@ -118,10 +118,10 @@ vocalinux --model medium
|
||||
|
||||
### Using Voice Dictation
|
||||
|
||||
1. Press the keyboard shortcut (default: Alt+Shift+V) to start recording
|
||||
1. Double-tap the Ctrl key to start recording
|
||||
2. You'll hear a start sound when recording begins
|
||||
3. Speak clearly into your microphone
|
||||
4. Press the same shortcut again or pause speaking to stop recording
|
||||
4. Double-tap Ctrl again or pause speaking to stop recording
|
||||
5. You'll hear a stop sound when recording ends
|
||||
|
||||
### Using Voice Commands
|
||||
@@ -153,7 +153,7 @@ Configuration is stored in `~/.config/vocalinux/config.json` and includes:
|
||||
"timeout": 2.0
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "alt+shift+v"
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
|
||||
+22
-3
@@ -13,6 +13,9 @@ This guide provides instructions for installing and setting up Vocalinux.
|
||||
### 1. Install System Dependencies
|
||||
|
||||
```bash
|
||||
# Required system dependencies for PyAudio and PyGObject
|
||||
sudo apt install portaudio19-dev libgirepository1.0-dev libcairo2-dev pkg-config python3-dev
|
||||
|
||||
# For X11 environments
|
||||
sudo apt install xdotool python3-pip python3-gi gir1.2-appindicator3-0.1
|
||||
|
||||
@@ -32,19 +35,27 @@ cd vocalinux
|
||||
### 3. Install Python Dependencies
|
||||
|
||||
```bash
|
||||
# Create a virtual environment (recommended)
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Basic installation
|
||||
pip3 install --user .
|
||||
pip install --upgrade pip
|
||||
pip install .
|
||||
|
||||
# With Whisper support (requires more resources)
|
||||
pip3 install --user ".[whisper]"
|
||||
pip install ".[whisper]"
|
||||
|
||||
# For development
|
||||
pip3 install --user ".[dev]"
|
||||
pip install ".[dev]"
|
||||
```
|
||||
|
||||
### 4. Run the Application
|
||||
|
||||
```bash
|
||||
# If using virtual environment, make sure it's activated
|
||||
source venv/bin/activate
|
||||
|
||||
# Basic usage
|
||||
vocalinux
|
||||
|
||||
@@ -74,6 +85,14 @@ Note that larger models require more RAM and may have longer initialization time
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build errors related to PyAudio or PyGObject
|
||||
|
||||
If you encounter errors like `portaudio.h: No such file or directory` or issues with GTK/GObject, make sure you've installed all the required system dependencies:
|
||||
|
||||
```bash
|
||||
sudo apt install portaudio19-dev libgirepository1.0-dev libcairo2-dev pkg-config python3-dev
|
||||
```
|
||||
|
||||
### No audio input detected
|
||||
|
||||
- Check that your microphone is connected and working
|
||||
|
||||
+6
-7
@@ -12,9 +12,9 @@ After installing Vocalinux (see the [Installation Guide](INSTALL.md)), you can s
|
||||
|
||||
1. **Launch the application**: Run `vocalinux` in a terminal or launch it from your application menu
|
||||
2. **Find the tray icon**: Look for the microphone icon in your system tray
|
||||
3. **Start voice typing**: Click the tray icon and select "Start Voice Typing" from the menu
|
||||
3. **Start voice typing**: Click the tray icon and select "Start Voice Typing" from the menu, or use the double-tap Ctrl keyboard shortcut
|
||||
4. **Speak clearly**: As you speak, your words will be transcribed into the currently focused application
|
||||
5. **Stop voice typing**: Click the tray icon and select "Stop Voice Typing" when you're done
|
||||
5. **Stop voice typing**: Click the tray icon and select "Stop Voice Typing" when you're done, or use the double-tap Ctrl keyboard shortcut again
|
||||
|
||||
### Understanding the Status Icons
|
||||
|
||||
@@ -50,13 +50,12 @@ Vocalinux supports several commands that you can speak to control formatting:
|
||||
|
||||
## Customization
|
||||
|
||||
### Keyboard Shortcuts
|
||||
### Keyboard Shortcut
|
||||
|
||||
Vocalinux supports global keyboard shortcuts for starting and stopping voice typing:
|
||||
Vocalinux uses a double-tap Ctrl keyboard shortcut for starting and stopping voice typing:
|
||||
|
||||
1. Open the application settings from the tray icon menu
|
||||
2. Go to the "Shortcuts" tab
|
||||
3. Set your preferred keyboard shortcut (default is Alt+Shift+V)
|
||||
- **Double-tap Ctrl**: Quickly press the Ctrl key twice to toggle voice typing on or off
|
||||
- The time between taps should be less than 0.3 seconds to be recognized as a double-tap
|
||||
|
||||
### Model Settings
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
# This package requires several system dependencies that must be installed manually:
|
||||
# For PyAudio: portaudio19-dev (on Ubuntu/Debian)
|
||||
# For PyGObject: libgirepository1.0-dev, libcairo2-dev, pkg-config, python3-dev
|
||||
|
||||
setup(
|
||||
name="vocalinux",
|
||||
version="0.1.0",
|
||||
@@ -17,7 +21,8 @@ setup(
|
||||
"PyGObject", # For GTK UI
|
||||
"requests", # For downloading models
|
||||
"tqdm", # For progress bars during downloads
|
||||
"numpy" # For numerical operations
|
||||
"numpy", # For numerical operations
|
||||
"pyaudio", # For audio input/output
|
||||
],
|
||||
extras_require={
|
||||
"whisper": ["whisper", "torch"], # Optional Whisper AI support
|
||||
@@ -27,6 +32,7 @@ setup(
|
||||
"black",
|
||||
"isort",
|
||||
"flake8",
|
||||
"pre-commit", # For pre-commit hooks
|
||||
],
|
||||
},
|
||||
entry_points={
|
||||
|
||||
@@ -26,7 +26,7 @@ DEFAULT_CONFIG = {
|
||||
"timeout": 2.0, # Seconds of silence before stopping recognition
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "alt+shift+v",
|
||||
"toggle_recognition": "ctrl+ctrl", # Double-tap Ctrl
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": False,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
Keyboard shortcut manager for Vocalinux.
|
||||
|
||||
This module provides global keyboard shortcut functionality to
|
||||
start/stop speech recognition with a keystroke.
|
||||
start/stop speech recognition with a double-tap of the Ctrl key.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Dict, Optional, Tuple
|
||||
from typing import Callable
|
||||
|
||||
# Try to import X11 keyboard libraries first
|
||||
try:
|
||||
@@ -25,29 +25,27 @@ class KeyboardShortcutManager:
|
||||
"""
|
||||
Manages global keyboard shortcuts for the application.
|
||||
|
||||
This class allows registering global keyboard shortcuts that work
|
||||
across the desktop environment.
|
||||
This class allows registering the double-tap Ctrl shortcut to
|
||||
toggle voice typing on and off across the desktop environment.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the keyboard shortcut manager."""
|
||||
self.shortcuts = {}
|
||||
self.listener = None
|
||||
self.active = False
|
||||
self.last_trigger_time = 0 # Track last trigger time to prevent double triggers
|
||||
|
||||
# Double-tap tracking variables
|
||||
self.last_ctrl_press_time = 0
|
||||
self.double_tap_callback = None
|
||||
self.double_tap_threshold = 0.3 # seconds between taps to count as double-tap
|
||||
|
||||
if not KEYBOARD_AVAILABLE:
|
||||
logger.error(
|
||||
"Keyboard shortcut libraries not available. Shortcuts will not work."
|
||||
)
|
||||
return
|
||||
|
||||
# Default shortcut for toggling voice typing (Alt+Shift+V)
|
||||
self.default_shortcut = (
|
||||
{keyboard.Key.alt, keyboard.Key.shift},
|
||||
keyboard.KeyCode.from_char("v"),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Start listening for keyboard shortcuts."""
|
||||
if not KEYBOARD_AVAILABLE:
|
||||
@@ -97,42 +95,15 @@ class KeyboardShortcutManager:
|
||||
finally:
|
||||
self.listener = None
|
||||
|
||||
def register_shortcut(self, modifiers: set, key, callback: Callable):
|
||||
"""
|
||||
Register a keyboard shortcut.
|
||||
|
||||
Args:
|
||||
modifiers: Set of modifier keys (e.g., {keyboard.Key.alt, keyboard.Key.shift})
|
||||
key: The main key (e.g., keyboard.KeyCode.from_char('v'))
|
||||
callback: Function to call when the shortcut is pressed
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Register a callback for the default toggle shortcut.
|
||||
Register a callback for the double-tap Ctrl shortcut.
|
||||
|
||||
Args:
|
||||
callback: Function to call when the toggle shortcut is pressed
|
||||
callback: Function to call when the double-tap Ctrl is pressed
|
||||
"""
|
||||
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)
|
||||
self.double_tap_callback = callback
|
||||
logger.info("Registered shortcut: Double-tap Ctrl")
|
||||
|
||||
def _on_press(self, key):
|
||||
"""
|
||||
@@ -142,50 +113,34 @@ class KeyboardShortcutManager:
|
||||
key: The pressed key
|
||||
"""
|
||||
try:
|
||||
# Add to currently pressed keys (only for modifier keys)
|
||||
# Check for double-tap Ctrl
|
||||
normalized_key = self._normalize_modifier_key(key)
|
||||
if normalized_key == keyboard.Key.ctrl:
|
||||
current_time = time.time()
|
||||
if current_time - self.last_ctrl_press_time < self.double_tap_threshold:
|
||||
# This is a double-tap Ctrl
|
||||
if (
|
||||
self.double_tap_callback
|
||||
and current_time - self.last_trigger_time > 0.5
|
||||
):
|
||||
logger.debug("Double-tap Ctrl detected")
|
||||
self.last_trigger_time = current_time
|
||||
# Run callback in a separate thread to avoid blocking
|
||||
threading.Thread(
|
||||
target=self.double_tap_callback, daemon=True
|
||||
).start()
|
||||
self.last_ctrl_press_time = current_time
|
||||
|
||||
# Add to currently pressed modifier keys (only for tracking Ctrl)
|
||||
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}")
|
||||
|
||||
|
||||
@@ -43,11 +43,6 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
self.mock_keyboard.Key.cmd_l = "Key.cmd_l"
|
||||
self.mock_keyboard.Key.cmd_r = "Key.cmd_r"
|
||||
|
||||
# Mock KeyCode.from_char
|
||||
key_v = MagicMock()
|
||||
key_v.char = "v"
|
||||
self.mock_keyboard.KeyCode.from_char.return_value = key_v
|
||||
|
||||
# Create mock Listener
|
||||
self.mock_listener = MagicMock()
|
||||
self.mock_listener.is_alive.return_value = True
|
||||
@@ -63,12 +58,10 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
|
||||
def test_init(self):
|
||||
"""Test initialization of the keyboard shortcut manager."""
|
||||
# Verify default shortcut is set correctly
|
||||
modifiers, key = self.ksm.default_shortcut
|
||||
self.assertEqual(
|
||||
modifiers, {self.mock_keyboard.Key.alt, self.mock_keyboard.Key.shift}
|
||||
)
|
||||
self.assertEqual(key.char, "v")
|
||||
# Verify double-tap threshold is set
|
||||
self.assertEqual(self.ksm.double_tap_threshold, 0.3)
|
||||
# Verify double-tap callback is initially None
|
||||
self.assertIsNone(self.ksm.double_tap_callback)
|
||||
|
||||
def test_start_listener(self):
|
||||
"""Test starting the keyboard listener."""
|
||||
@@ -137,39 +130,16 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
if hasattr(self.ksm, "listener") and self.ksm.listener:
|
||||
self.mock_listener.stop.assert_not_called()
|
||||
|
||||
def test_register_shortcut(self):
|
||||
"""Test registering keyboard shortcuts."""
|
||||
# Create mock callback and shortcut
|
||||
callback = MagicMock()
|
||||
modifiers = {self.mock_keyboard.Key.ctrl, self.mock_keyboard.Key.shift}
|
||||
key = MagicMock()
|
||||
key.char = "s"
|
||||
|
||||
# Mock _get_key_name to avoid errors
|
||||
self.ksm._get_key_name = MagicMock(return_value="S")
|
||||
|
||||
# Register the shortcut
|
||||
self.ksm.register_shortcut(modifiers, key, callback)
|
||||
|
||||
# Verify it was registered
|
||||
shortcut_key = (frozenset(modifiers), key)
|
||||
self.assertEqual(self.ksm.shortcuts[shortcut_key], callback)
|
||||
|
||||
def test_register_toggle_callback(self):
|
||||
"""Test registering toggle callback."""
|
||||
"""Test registering toggle callback with double-tap shortcut."""
|
||||
# Create mock callback
|
||||
callback = MagicMock()
|
||||
|
||||
# Mock _get_key_name to avoid errors
|
||||
self.ksm._get_key_name = MagicMock(return_value="V")
|
||||
|
||||
# Register as toggle callback
|
||||
self.ksm.register_toggle_callback(callback)
|
||||
|
||||
# Verify it was registered with default shortcut
|
||||
modifiers, key = self.ksm.default_shortcut
|
||||
shortcut_key = (frozenset(modifiers), key)
|
||||
self.assertEqual(self.ksm.shortcuts[shortcut_key], callback)
|
||||
# Verify it was registered as double-tap callback
|
||||
self.assertEqual(self.ksm.double_tap_callback, callback)
|
||||
|
||||
def test_key_press_modifier(self):
|
||||
"""Test handling a modifier key press."""
|
||||
@@ -182,11 +152,41 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
# Make sure current_keys is set and initially empty
|
||||
self.ksm.current_keys = set()
|
||||
|
||||
# Simulate pressing Alt
|
||||
on_press(self.mock_keyboard.Key.alt)
|
||||
# Simulate pressing Ctrl
|
||||
on_press(self.mock_keyboard.Key.ctrl)
|
||||
|
||||
# Verify Alt was added to current keys
|
||||
self.assertIn(self.mock_keyboard.Key.alt, self.ksm.current_keys)
|
||||
# Verify Ctrl was added to current keys
|
||||
self.assertIn(self.mock_keyboard.Key.ctrl, self.ksm.current_keys)
|
||||
|
||||
def test_double_tap_ctrl(self):
|
||||
"""Test double-tap Ctrl detection."""
|
||||
# Initialize and start to set up the listener
|
||||
self.ksm.start()
|
||||
|
||||
# Get the on_press handler
|
||||
on_press = self.mock_keyboard.Listener.call_args[1]["on_press"]
|
||||
|
||||
# Register mock callback for double-tap Ctrl
|
||||
callback = MagicMock()
|
||||
self.ksm.register_toggle_callback(callback)
|
||||
|
||||
# Set up initial state
|
||||
self.ksm.last_ctrl_press_time = (
|
||||
time.time() - 0.2
|
||||
) # Recent press (within threshold)
|
||||
self.ksm.last_trigger_time = 0 # No recent triggers
|
||||
|
||||
# Simulate second Ctrl press (should trigger callback)
|
||||
on_press(self.mock_keyboard.Key.ctrl)
|
||||
|
||||
# Verify callback was triggered
|
||||
callback.assert_called_once()
|
||||
|
||||
# Reset and test when press is outside threshold (not a double-tap)
|
||||
callback.reset_mock()
|
||||
self.ksm.last_ctrl_press_time = time.time() - 0.5 # Outside threshold
|
||||
on_press(self.mock_keyboard.Key.ctrl)
|
||||
callback.assert_not_called()
|
||||
|
||||
def test_normalize_modifier_keys(self):
|
||||
"""Test normalizing left/right modifier keys."""
|
||||
@@ -210,54 +210,31 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
self.mock_keyboard.Key.alt,
|
||||
)
|
||||
|
||||
def test_get_key_name(self):
|
||||
"""Test getting readable name for a key."""
|
||||
# Create a key with .char attribute
|
||||
char_key = MagicMock()
|
||||
char_key.char = "a"
|
||||
self.assertEqual(self.ksm._get_key_name(char_key), "A")
|
||||
|
||||
# Create a key with .name attribute but no .char
|
||||
name_key = MagicMock()
|
||||
name_key.name = "shift"
|
||||
name_key.char = None
|
||||
self.assertEqual(self.ksm._get_key_name(name_key), "shift")
|
||||
|
||||
# Create a key with neither .char nor .name
|
||||
other_key = "other_key"
|
||||
self.assertEqual(self.ksm._get_key_name(other_key), "other_key")
|
||||
|
||||
def test_key_press_shortcut_trigger(self):
|
||||
"""Test triggering a shortcut."""
|
||||
def test_double_tap_ctrl_debounce(self):
|
||||
"""Test that double-tap Ctrl has debounce protection."""
|
||||
# Initialize and start to set up the listener
|
||||
self.ksm.start()
|
||||
|
||||
# Create a simplified on_press function to avoid complex type checking
|
||||
# This focuses on testing the shortcut logic, not the key detection
|
||||
def simplified_on_press(key):
|
||||
# Simply check for our specific key and trigger callback
|
||||
shortcut_key = (frozenset({self.mock_keyboard.Key.ctrl}), key)
|
||||
if shortcut_key in self.ksm.shortcuts:
|
||||
callback = self.ksm.shortcuts[shortcut_key]
|
||||
callback()
|
||||
# Get the on_press handler
|
||||
on_press = self.mock_keyboard.Listener.call_args[1]["on_press"]
|
||||
|
||||
# Register a test shortcut (Ctrl+S)
|
||||
# Register mock callback for double-tap Ctrl
|
||||
callback = MagicMock()
|
||||
self.ksm.register_toggle_callback(callback)
|
||||
|
||||
# Create keys
|
||||
ctrl_key = self.mock_keyboard.Key.ctrl
|
||||
s_key = MagicMock()
|
||||
s_key.char = "s"
|
||||
# Set up initial state (recent trigger)
|
||||
self.ksm.last_ctrl_press_time = (
|
||||
time.time() - 0.2
|
||||
) # Recent press (within threshold)
|
||||
self.ksm.last_trigger_time = (
|
||||
time.time() - 0.2
|
||||
) # Recent trigger, within debounce
|
||||
|
||||
# Set up the shortcut
|
||||
modifiers = {ctrl_key}
|
||||
self.ksm.register_shortcut(modifiers, s_key, callback)
|
||||
# Simulate second Ctrl press (should NOT trigger due to debounce)
|
||||
on_press(self.mock_keyboard.Key.ctrl)
|
||||
|
||||
# Simulate pressing 's'
|
||||
simplified_on_press(s_key)
|
||||
|
||||
# Verify callback was triggered
|
||||
callback.assert_called_once()
|
||||
# Verify callback was NOT triggered
|
||||
callback.assert_not_called()
|
||||
|
||||
def test_key_release(self):
|
||||
"""Test handling a key release."""
|
||||
@@ -268,10 +245,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
on_release = self.mock_keyboard.Listener.call_args[1]["on_release"]
|
||||
|
||||
# Add some keys
|
||||
self.ksm.current_keys = {
|
||||
self.mock_keyboard.Key.ctrl,
|
||||
self.mock_keyboard.Key.alt,
|
||||
}
|
||||
self.ksm.current_keys = {self.mock_keyboard.Key.ctrl}
|
||||
|
||||
# Simulate releasing Ctrl
|
||||
on_release(self.mock_keyboard.Key.ctrl)
|
||||
@@ -279,38 +253,6 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
# Verify Ctrl was removed
|
||||
self.assertNotIn(self.mock_keyboard.Key.ctrl, self.ksm.current_keys)
|
||||
|
||||
def test_shortcut_debounce(self):
|
||||
"""Test that shortcuts have debounce protection."""
|
||||
# Initialize and start to set up the listener
|
||||
self.ksm.start()
|
||||
|
||||
# Get the on_press handler
|
||||
on_press = self.mock_keyboard.Listener.call_args[1]["on_press"]
|
||||
|
||||
# Register a test shortcut
|
||||
callback = MagicMock()
|
||||
|
||||
# Create keys
|
||||
ctrl_key = self.mock_keyboard.Key.ctrl
|
||||
s_key = MagicMock()
|
||||
s_key.char = "s"
|
||||
|
||||
# Set up the shortcut
|
||||
modifiers = {ctrl_key}
|
||||
self.ksm.register_shortcut(modifiers, s_key, callback)
|
||||
|
||||
# Set up the current keys
|
||||
self.ksm.current_keys = {ctrl_key}
|
||||
|
||||
# Set last trigger time to now
|
||||
self.ksm.last_trigger_time = time.time()
|
||||
|
||||
# Simulate pressing 's'
|
||||
on_press(s_key)
|
||||
|
||||
# Callback should not be called due to debounce
|
||||
callback.assert_not_called()
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling in key event handlers."""
|
||||
# Initialize and start to set up the listener
|
||||
@@ -345,17 +287,3 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
|
||||
# When keyboard is not available, active should remain False
|
||||
self.assertFalse(ksm.active)
|
||||
|
||||
# Should still be able to register shortcuts, but they won't do anything
|
||||
callback = MagicMock()
|
||||
|
||||
# Create simulated keys
|
||||
ctrl_key = "Key.ctrl"
|
||||
s_key = MagicMock()
|
||||
s_key.char = "s"
|
||||
|
||||
# Register
|
||||
ksm.register_shortcut({ctrl_key}, s_key, callback)
|
||||
|
||||
# Verify it was added
|
||||
self.assertEqual(len(ksm.shortcuts), 1)
|
||||
|
||||
Reference in New Issue
Block a user