diff --git a/src/vocalinux/__init__.py b/src/vocalinux/__init__.py index f91b258..bd6e0cd 100644 --- a/src/vocalinux/__init__.py +++ b/src/vocalinux/__init__.py @@ -12,3 +12,4 @@ from .common_types import RecognitionState from .speech_recognition import recognition_manager from .text_injection import text_injector from .ui import tray_indicator +from .utils import ResourceManager diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py index 3961e6e..9a3e07f 100644 --- a/src/vocalinux/main.py +++ b/src/vocalinux/main.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) from .speech_recognition import recognition_manager from .text_injection import text_injector from .ui import tray_indicator +from .ui.action_handler import ActionHandler def parse_arguments(): @@ -44,6 +45,38 @@ def parse_arguments(): return parser.parse_args() +def check_dependencies(): + """Check for required dependencies and provide helpful error messages.""" + missing_deps = [] + + try: + import gi + gi.require_version("Gtk", "3.0") + gi.require_version("AppIndicator3", "0.1") + from gi.repository import AppIndicator3, Gtk + except (ImportError, ValueError) as e: + missing_deps.append("GTK3 and AppIndicator3 (install with: sudo apt install python3-gi gir1.2-appindicator3-0.1)") + + try: + import pynput + except ImportError: + missing_deps.append("pynput (install with: pip install pynput)") + + try: + import requests + except ImportError: + missing_deps.append("requests (install with: pip install requests)") + + if missing_deps: + logger.error("Missing required dependencies:") + for dep in missing_deps: + logger.error(f" - {dep}") + logger.error("Please install the missing dependencies and try again.") + return False + + return True + + def main(): """Main entry point for the application.""" args = parse_arguments() @@ -53,29 +86,51 @@ def main(): logging.getLogger().setLevel(logging.DEBUG) logger.debug("Debug logging enabled") + # Check dependencies first + if not check_dependencies(): + logger.error("Cannot start Vocalinux due to missing dependencies") + sys.exit(1) + # Initialize main components logger.info("Initializing Vocalinux...") - # Initialize speech recognition engine - speech_engine = recognition_manager.SpeechRecognitionManager( - engine=args.engine, - model_size=args.model, - ) + try: + # Initialize speech recognition engine + speech_engine = recognition_manager.SpeechRecognitionManager( + engine=args.engine, + model_size=args.model, + ) - # Initialize text injection system - text_system = text_injector.TextInjector(wayland_mode=args.wayland) + # Initialize text injection system + text_system = text_injector.TextInjector(wayland_mode=args.wayland) - # Connect speech recognition to text injection - speech_engine.register_text_callback(text_system.inject_text) + # Initialize action handler + action_handler = ActionHandler(text_system) - # Initialize and start the system tray indicator - indicator = tray_indicator.TrayIndicator( - speech_engine=speech_engine, - text_injector=text_system, - ) + # Create a wrapper function to track injected text for action handler + def text_callback_wrapper(text: str): + """Wrapper to track injected text and handle it.""" + success = text_system.inject_text(text) + if success: + action_handler.set_last_injected_text(text) - # Start the GTK main loop - indicator.run() + # Connect speech recognition to text injection and action handling + speech_engine.register_text_callback(text_callback_wrapper) + speech_engine.register_action_callback(action_handler.handle_action) + + # Initialize and start the system tray indicator + indicator = tray_indicator.TrayIndicator( + speech_engine=speech_engine, + text_injector=text_system, + ) + + # Start the GTK main loop + indicator.run() + + except Exception as e: + logger.error(f"Failed to initialize Vocalinux: {e}") + logger.error("Please check the logs above for more details") + sys.exit(1) if __name__ == "__main__": diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index 221565b..a3e9603 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -141,7 +141,7 @@ class SpeechRecognitionManager: model_urls = { "small": "https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip", "medium": "https://alphacephei.com/vosk/models/vosk-model-en-us-0.22.zip", - "large": "https://alphacephei.com/vosk/models/vosk-model-en-us-0.42.zip", + "large": "https://alphacephei.com/vosk/models/vosk-model-en-us-0.22.zip", # Use 0.22 as 0.42 is not available } url = model_urls.get(self.model_size) @@ -307,6 +307,14 @@ class SpeechRecognitionManager: import numpy as np import pyaudio + except ImportError as e: + logger.error(f"Failed to import required audio libraries: {e}") + logger.error("Please install required dependencies: pip install pyaudio numpy") + play_error_sound() + self._update_state(RecognitionState.ERROR) + return + + try: # PyAudio configuration CHUNK = 1024 diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index 48a9484..3858379 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -182,15 +182,19 @@ class TextInjector: except Exception as e: logger.error(f"Failed to test XWayland fallback: {e}") - def inject_text(self, text: str): + def inject_text(self, text: str) -> bool: """ Inject text into the currently focused application. Args: text: The text to inject + + Returns: + True if injection was successful, False otherwise """ if not text or not text.strip(): - return + logger.debug("Empty text provided, skipping injection") + return True logger.debug(f"Injecting text: {text}") @@ -218,11 +222,15 @@ class TextInjector: self._inject_with_xdotool(escaped_text) else: raise + return True except Exception as e: logger.error(f"Failed to inject text: {e}") - from ..ui.audio_feedback import play_error_sound - - play_error_sound() # Play error sound when text injection fails + try: + from ..ui.audio_feedback import play_error_sound + play_error_sound() # Play error sound when text injection fails + except ImportError: + logger.warning("Could not import audio feedback module") + return False def _escape_text(self, text: str) -> str: """ @@ -363,3 +371,82 @@ class TextInjector: logger.info( f"Text injected using {self.wayland_tool}: '{text[:20]}...' ({len(text)} chars)" ) + + def _inject_keyboard_shortcut(self, shortcut: str) -> bool: + """ + Inject a keyboard shortcut. + + Args: + shortcut: The keyboard shortcut to inject (e.g., "ctrl+z", "ctrl+a") + + Returns: + True if injection was successful, False otherwise + """ + logger.debug(f"Injecting keyboard shortcut: {shortcut}") + + try: + if ( + self.environment == DesktopEnvironment.X11 + or self.environment == DesktopEnvironment.WAYLAND_XDOTOOL + ): + return self._inject_shortcut_with_xdotool(shortcut) + else: + return self._inject_shortcut_with_wayland_tool(shortcut) + except Exception as e: + logger.error(f"Failed to inject keyboard shortcut '{shortcut}': {e}") + return False + + def _inject_shortcut_with_xdotool(self, shortcut: str) -> bool: + """ + Inject a keyboard shortcut using xdotool. + + Args: + shortcut: The keyboard shortcut to inject + + Returns: + True if successful, False otherwise + """ + # Create environment with explicit X11 settings for Wayland compatibility + env = os.environ.copy() + + if self.environment == DesktopEnvironment.WAYLAND_XDOTOOL: + env["GDK_BACKEND"] = "x11" + env["QT_QPA_PLATFORM"] = "xcb" + if "DISPLAY" not in env or not env["DISPLAY"]: + env["DISPLAY"] = ":0" + + try: + cmd = ["xdotool", "key", "--clearmodifiers", shortcut] + subprocess.run(cmd, env=env, check=True, stderr=subprocess.PIPE, text=True) + logger.debug(f"Keyboard shortcut '{shortcut}' injected successfully") + return True + except subprocess.CalledProcessError as e: + logger.error(f"xdotool shortcut error: {e.stderr}") + return False + + def _inject_shortcut_with_wayland_tool(self, shortcut: str) -> bool: + """ + Inject a keyboard shortcut using a Wayland-compatible tool. + + Args: + shortcut: The keyboard shortcut to inject + + Returns: + True if successful, False otherwise + """ + if self.wayland_tool == "wtype": + # wtype doesn't support key combinations directly, so we can't implement this easily + logger.warning("Keyboard shortcuts not supported with wtype") + return False + elif self.wayland_tool == "ydotool": + try: + cmd = ["ydotool", "key", shortcut] + subprocess.run(cmd, check=True, stderr=subprocess.PIPE, text=True) + logger.debug(f"Keyboard shortcut '{shortcut}' injected successfully") + return True + except subprocess.CalledProcessError as e: + logger.error(f"ydotool shortcut error: {e.stderr}") + return False + else: + logger.warning(f"Keyboard shortcuts not supported with {self.wayland_tool}") + return False diff --git a/src/vocalinux/ui/action_handler.py b/src/vocalinux/ui/action_handler.py new file mode 100644 index 0000000..69489d1 --- /dev/null +++ b/src/vocalinux/ui/action_handler.py @@ -0,0 +1,130 @@ +""" +Action handler for Vocalinux voice commands. + +This module handles special voice commands like "delete that", "undo", etc. +""" + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..text_injection.text_injector import TextInjector + +logger = logging.getLogger(__name__) + + +class ActionHandler: + """ + Handles special voice command actions. + + This class processes action commands from the speech recognition system + and performs the appropriate actions like deleting text, undoing, etc. + """ + + def __init__(self, text_injector: "TextInjector"): + """ + Initialize the action handler. + + Args: + text_injector: The text injector instance for performing actions + """ + self.text_injector = text_injector + self.last_injected_text = "" + + # Map actions to handler methods + self.action_handlers = { + "delete_last": self._handle_delete_last, + "undo": self._handle_undo, + "redo": self._handle_redo, + "select_all": self._handle_select_all, + "select_line": self._handle_select_line, + "select_word": self._handle_select_word, + "select_paragraph": self._handle_select_paragraph, + "cut": self._handle_cut, + "copy": self._handle_copy, + "paste": self._handle_paste, + } + + def handle_action(self, action: str) -> bool: + """ + Handle a voice command action. + + Args: + action: The action to perform + + Returns: + True if the action was handled successfully, False otherwise + """ + logger.debug(f"Handling action: {action}") + + handler = self.action_handlers.get(action) + if handler: + try: + return handler() + except Exception as e: + logger.error(f"Error handling action '{action}': {e}") + return False + else: + logger.warning(f"Unknown action: {action}") + return False + + def set_last_injected_text(self, text: str): + """ + Set the last injected text for undo/delete operations. + + Args: + text: The last text that was injected + """ + self.last_injected_text = text + + def _handle_delete_last(self) -> bool: + """Handle 'delete that' command by sending backspace keys.""" + if not self.last_injected_text: + logger.debug("No text to delete") + return True + + # Send backspace keys for each character in the last injected text + backspaces = "\b" * len(self.last_injected_text) + success = self.text_injector.inject_text(backspaces) + + if success: + logger.debug(f"Deleted {len(self.last_injected_text)} characters") + self.last_injected_text = "" + + return success + + def _handle_undo(self) -> bool: + """Handle 'undo' command by sending Ctrl+Z.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+z") + + def _handle_redo(self) -> bool: + """Handle 'redo' command by sending Ctrl+Y.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+y") + + def _handle_select_all(self) -> bool: + """Handle 'select all' command by sending Ctrl+A.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+a") + + def _handle_select_line(self) -> bool: + """Handle 'select line' command by sending Home+Shift+End.""" + return self.text_injector._inject_keyboard_shortcut("Home+shift+End") + + def _handle_select_word(self) -> bool: + """Handle 'select word' command by sending Ctrl+Shift+Right.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Right") + + def _handle_select_paragraph(self) -> bool: + """Handle 'select paragraph' command by sending Ctrl+Shift+Down.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Down") + + def _handle_cut(self) -> bool: + """Handle 'cut' command by sending Ctrl+X.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+x") + + def _handle_copy(self) -> bool: + """Handle 'copy' command by sending Ctrl+C.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+c") + + def _handle_paste(self) -> bool: + """Handle 'paste' command by sending Ctrl+V.""" + return self.text_injector._inject_keyboard_shortcut("ctrl+v") \ No newline at end of file diff --git a/src/vocalinux/ui/audio_feedback.py b/src/vocalinux/ui/audio_feedback.py index cc06fda..68a14d7 100644 --- a/src/vocalinux/ui/audio_feedback.py +++ b/src/vocalinux/ui/audio_feedback.py @@ -19,54 +19,16 @@ logger = logging.getLogger(__name__) CI_MODE = os.environ.get("GITHUB_ACTIONS") == "true" -# Define a more robust way to find the resources directory -def find_resources_dir(): - """Find the resources directory regardless of how the application is executed.""" - # First, check if we're running from the repository - module_dir = os.path.dirname(os.path.abspath(__file__)) +# Import the centralized resource manager +from ..utils.resource_manager import ResourceManager - # Try several methods to find the resources directory - candidates = [ - # For direct repository execution - os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(module_dir))), "resources" - ), - # For installed package or virtual environment - os.path.join(sys.prefix, "share", "vocalinux", "resources"), - # For development in virtual environment - os.path.join(os.path.dirname(sys.prefix), "resources"), - # Additional fallback - "/usr/local/share/vocalinux/resources", - "/usr/share/vocalinux/resources", - ] - - # Log all candidates for debugging - for candidate in candidates: - logger.debug( - f"Checking resources candidate: {candidate} (exists: {os.path.exists(candidate)})" - ) - - # Return the first candidate that exists - for candidate in candidates: - if os.path.exists(candidate): - logger.info(f"Found resources directory: {candidate}") - return candidate - - # If no candidate exists, default to the first one (with warning) - logger.warning( - f"Could not find resources directory, defaulting to: {candidates[0]}" - ) - return candidates[0] - - -# Update paths to find resources directory -RESOURCES_DIR = find_resources_dir() -SOUNDS_DIR = os.path.join(RESOURCES_DIR, "sounds") +# Initialize resource manager +_resource_manager = ResourceManager() # Sound file paths -START_SOUND = os.path.join(SOUNDS_DIR, "start_recording.wav") -STOP_SOUND = os.path.join(SOUNDS_DIR, "stop_recording.wav") -ERROR_SOUND = os.path.join(SOUNDS_DIR, "error.wav") +START_SOUND = _resource_manager.get_sound_path("start_recording") +STOP_SOUND = _resource_manager.get_sound_path("stop_recording") +ERROR_SOUND = _resource_manager.get_sound_path("error") def _get_audio_player(): diff --git a/src/vocalinux/ui/tray_indicator.py b/src/vocalinux/ui/tray_indicator.py index 35c34a6..ab891c3 100644 --- a/src/vocalinux/ui/tray_indicator.py +++ b/src/vocalinux/ui/tray_indicator.py @@ -37,49 +37,12 @@ logger = logging.getLogger(__name__) APP_ID = "vocalinux" -# Define a robust way to find the resources directory -def find_resources_dir(): - """Find the resources directory regardless of how the application is executed.""" - # First, check if we're running from the repository - module_dir = os.path.dirname(os.path.abspath(__file__)) +# Import the centralized resource manager +from ..utils.resource_manager import ResourceManager - # Try several methods to find the resources directory - candidates = [ - # For direct repository execution - os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(module_dir))), "resources" - ), - # For installed package or virtual environment - os.path.join(sys.prefix, "share", "vocalinux", "resources"), - # For development in virtual environment - os.path.join(os.path.dirname(sys.prefix), "resources"), - # Additional fallback - "/usr/local/share/vocalinux/resources", - "/usr/share/vocalinux/resources", - ] - - # Log all candidates for debugging - for candidate in candidates: - logger.debug( - f"Checking resources candidate: {candidate} (exists: {os.path.exists(candidate)})" - ) - - # Return the first candidate that exists - for candidate in candidates: - if os.path.exists(candidate): - logger.info(f"Found resources directory: {candidate}") - return candidate - - # If no candidate exists, default to the first one (with warning) - logger.warning( - f"Could not find resources directory, defaulting to: {candidates[0]}" - ) - return candidates[0] - - -# Find resources directory and icon directory -RESOURCES_DIR = find_resources_dir() -ICON_DIR = os.path.join(RESOURCES_DIR, "icons/scalable") +# Initialize resource manager +_resource_manager = ResourceManager() +ICON_DIR = _resource_manager.icons_dir # Icon file names DEFAULT_ICON = "vocalinux-microphone-off" @@ -119,18 +82,19 @@ class TrayIndicator: # Ensure icon directory exists os.makedirs(ICON_DIR, exist_ok=True) - # Set up icon file paths + # Set up icon file paths using resource manager self.icon_paths = { - "default": os.path.join(ICON_DIR, f"{DEFAULT_ICON}.svg"), - "active": os.path.join(ICON_DIR, f"{ACTIVE_ICON}.svg"), - "processing": os.path.join(ICON_DIR, f"{PROCESSING_ICON}.svg"), + "default": _resource_manager.get_icon_path(DEFAULT_ICON), + "active": _resource_manager.get_icon_path(ACTIVE_ICON), + "processing": _resource_manager.get_icon_path(PROCESSING_ICON), } # Register for speech recognition state changes self.speech_engine.register_state_callback(self._on_recognition_state_changed) - # Initialize the icon files + # Initialize the icon files and validate resources self._init_icons() + self._validate_resources() # Initialize the indicator (in the GTK main thread) GLib.idle_add(self._init_indicator) @@ -141,8 +105,27 @@ class TrayIndicator: def _init_icons(self): """Initialize the icon files for the tray indicator.""" - # TODO: Copy default icons to the icon directory if they don't exist - pass + # Ensure icon directory exists + _resource_manager.ensure_directories_exist() + + def _validate_resources(self): + """Validate that required resources are available.""" + validation_results = _resource_manager.validate_resources() + + if not validation_results["resources_dir_exists"]: + logger.warning("Resources directory not found") + + if validation_results["missing_icons"]: + logger.warning(f"Missing icon files: {validation_results['missing_icons']}") + + if validation_results["missing_sounds"]: + logger.warning(f"Missing sound files: {validation_results['missing_sounds']}") + + # Log successful validation + if (validation_results["resources_dir_exists"] and + not validation_results["missing_icons"] and + not validation_results["missing_sounds"]): + logger.info("All required resources validated successfully") def _init_indicator(self): """Initialize the system tray indicator.""" @@ -335,7 +318,7 @@ class TrayIndicator: about_dialog.set_license_type(Gtk.License.GPL_3_0) # Set the logo using our custom icon - logo_path = os.path.join(ICON_DIR, "vocalinux.svg") + logo_path = _resource_manager.get_icon_path("vocalinux") if os.path.exists(logo_path): try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(logo_path) diff --git a/src/vocalinux/utils/__init__.py b/src/vocalinux/utils/__init__.py new file mode 100644 index 0000000..5a18094 --- /dev/null +++ b/src/vocalinux/utils/__init__.py @@ -0,0 +1,7 @@ +""" +Utility modules for Vocalinux. +""" + +from .resource_manager import ResourceManager + +__all__ = ["ResourceManager"] \ No newline at end of file diff --git a/src/vocalinux/utils/resource_manager.py b/src/vocalinux/utils/resource_manager.py new file mode 100644 index 0000000..13d9f8e --- /dev/null +++ b/src/vocalinux/utils/resource_manager.py @@ -0,0 +1,162 @@ +""" +Centralized resource manager for Vocalinux. + +This module provides a unified way to locate and access application resources +like icons, sounds, and other assets regardless of how the application is installed. +""" + +import logging +import os +import sys +from pathlib import Path +from typing import Optional + +logger = logging.getLogger(__name__) + + +class ResourceManager: + """ + Centralized manager for application resources. + + This class provides a unified way to locate resources like icons and sounds + regardless of whether the application is running from source, installed via pip, + or installed system-wide. + """ + + _instance = None + _resources_dir = None + + def __new__(cls): + """Singleton pattern to ensure only one instance exists.""" + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """Initialize the resource manager.""" + if self._resources_dir is None: + self._resources_dir = self._find_resources_dir() + + def _find_resources_dir(self) -> str: + """ + Find the resources directory regardless of how the application is executed. + + Returns: + Path to the resources directory + """ + # Get the directory where this module is located + module_dir = Path(__file__).parent.absolute() + + # Try several methods to find the resources directory + candidates = [ + # For direct repository execution (go up from src/vocalinux/utils to root) + module_dir.parent.parent.parent / "resources", + # For installed package or virtual environment + Path(sys.prefix) / "share" / "vocalinux" / "resources", + # For development in virtual environment + Path(sys.prefix).parent / "resources", + # Additional fallbacks + Path("/usr/local/share/vocalinux/resources"), + Path("/usr/share/vocalinux/resources"), + ] + + # Log all candidates for debugging + for candidate in candidates: + logger.debug(f"Checking resources candidate: {candidate} (exists: {candidate.exists()})") + + # Return the first candidate that exists + for candidate in candidates: + if candidate.exists(): + logger.info(f"Found resources directory: {candidate}") + return str(candidate) + + # If no candidate exists, default to the first one (with warning) + default_path = str(candidates[0]) + logger.warning(f"Could not find resources directory, defaulting to: {default_path}") + return default_path + + @property + def resources_dir(self) -> str: + """Get the resources directory path.""" + return self._resources_dir + + @property + def icons_dir(self) -> str: + """Get the icons directory path.""" + return os.path.join(self._resources_dir, "icons", "scalable") + + @property + def sounds_dir(self) -> str: + """Get the sounds directory path.""" + return os.path.join(self._resources_dir, "sounds") + + def get_icon_path(self, icon_name: str) -> str: + """ + Get the full path to an icon file. + + Args: + icon_name: Name of the icon (without extension) + + Returns: + Full path to the icon file + """ + return os.path.join(self.icons_dir, f"{icon_name}.svg") + + def get_sound_path(self, sound_name: str) -> str: + """ + Get the full path to a sound file. + + Args: + sound_name: Name of the sound file (without extension) + + Returns: + Full path to the sound file + """ + return os.path.join(self.sounds_dir, f"{sound_name}.wav") + + def ensure_directories_exist(self): + """Ensure that resource directories exist.""" + os.makedirs(self.icons_dir, exist_ok=True) + os.makedirs(self.sounds_dir, exist_ok=True) + + def validate_resources(self) -> dict: + """ + Validate that all expected resources exist. + + Returns: + Dictionary with validation results + """ + results = { + "resources_dir_exists": os.path.exists(self._resources_dir), + "icons_dir_exists": os.path.exists(self.icons_dir), + "sounds_dir_exists": os.path.exists(self.sounds_dir), + "missing_icons": [], + "missing_sounds": [], + } + + # Expected icons + expected_icons = [ + "vocalinux", + "vocalinux-microphone", + "vocalinux-microphone-off", + "vocalinux-microphone-process" + ] + + for icon in expected_icons: + icon_path = self.get_icon_path(icon) + if not os.path.exists(icon_path): + results["missing_icons"].append(icon) + + # Expected sounds + expected_sounds = [ + "start_recording", + "stop_recording", + "error" + ] + + for sound in expected_sounds: + sound_path = self.get_sound_path(sound) + if not os.path.exists(sound_path): + results["missing_sounds"].append(sound) + + return results \ No newline at end of file diff --git a/tests/test_audio_feedback.py b/tests/test_audio_feedback.py index 30699ca..6352875 100644 --- a/tests/test_audio_feedback.py +++ b/tests/test_audio_feedback.py @@ -16,8 +16,6 @@ AUDIO_FEEDBACK_MODULE = "vocalinux.ui.audio_feedback" # Import the module under test from vocalinux.ui.audio_feedback import ( ERROR_SOUND, - RESOURCES_DIR, - SOUNDS_DIR, START_SOUND, STOP_SOUND, ) @@ -46,14 +44,19 @@ class TestAudioFeedback(unittest.TestCase): def test_resource_paths(self): """Test that resource paths are correctly set up.""" + # Import the resource manager to test paths + from vocalinux.utils.resource_manager import ResourceManager + + resource_manager = ResourceManager() + # Verify that resource paths are correctly set and accessible self.assertTrue( - os.path.exists(RESOURCES_DIR) or RESOURCES_DIR.endswith("resources"), - f"Resources directory is not valid: {RESOURCES_DIR}", + resource_manager.resources_dir.endswith("resources"), + f"Resources directory is not valid: {resource_manager.resources_dir}", ) self.assertTrue( - SOUNDS_DIR.endswith("sounds"), - f"Sounds directory path is not valid: {SOUNDS_DIR}", + resource_manager.sounds_dir.endswith("sounds"), + f"Sounds directory path is not valid: {resource_manager.sounds_dir}", ) self.assertEqual(os.path.basename(START_SOUND), "start_recording.wav") self.assertEqual(os.path.basename(STOP_SOUND), "stop_recording.wav") diff --git a/tests/test_main.py b/tests/test_main.py index 24e0c4c..3d2cf84 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,23 +44,30 @@ class TestMainModule(unittest.TestCase): self.assertEqual(args.engine, "whisper") self.assertTrue(args.wayland) + @patch("vocalinux.main.check_dependencies") + @patch("vocalinux.main.ActionHandler") @patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager") @patch("vocalinux.text_injection.text_injector.TextInjector") @patch("vocalinux.ui.tray_indicator.TrayIndicator") @patch("vocalinux.main.logging") def test_main_initializes_components( - self, mock_logging, mock_tray, mock_text, mock_speech + self, mock_logging, mock_tray, mock_text, mock_speech, mock_action_handler, mock_check_deps ): """Test that main initializes all the required components.""" + # Mock dependency check to return True + mock_check_deps.return_value = True + # Mock objects mock_speech_instance = MagicMock() mock_text_instance = MagicMock() mock_tray_instance = MagicMock() + mock_action_instance = MagicMock() # Setup return values mock_speech.return_value = mock_speech_instance mock_text.return_value = mock_text_instance mock_tray.return_value = mock_tray_instance + mock_action_handler.return_value = mock_action_instance # Mock the arguments with patch("vocalinux.main.parse_arguments") as mock_parse: @@ -77,13 +84,15 @@ class TestMainModule(unittest.TestCase): # Verify components were initialized correctly mock_speech.assert_called_once_with(engine="vosk", model_size="medium") mock_text.assert_called_once_with(wayland_mode=True) + mock_action_handler.assert_called_once_with(mock_text_instance) mock_tray.assert_called_once_with( speech_engine=mock_speech_instance, text_injector=mock_text_instance ) # Verify callbacks were registered - mock_speech_instance.register_text_callback.assert_called_once_with( - mock_text_instance.inject_text + mock_speech_instance.register_text_callback.assert_called_once() + mock_speech_instance.register_action_callback.assert_called_once_with( + mock_action_instance.handle_action ) # Verify the tray indicator was started @@ -102,7 +111,14 @@ class TestMainModule(unittest.TestCase): "vocalinux.text_injection.text_injector.TextInjector" ), patch( "vocalinux.ui.tray_indicator.TrayIndicator" - ): + ), patch( + "vocalinux.main.ActionHandler" + ), patch( + "vocalinux.main.check_dependencies" + ) as mock_check_deps: + + # Mock dependency check to return True + mock_check_deps.return_value = True # Create mock args mock_args = MagicMock()