diff --git a/kratt/__init__.py b/kratt/__init__.py index a75bf1b..fc1bec0 100644 --- a/kratt/__init__.py +++ b/kratt/__init__.py @@ -12,12 +12,10 @@ DEFAULT_MAIN_MODEL, DEFAULT_VISION_MODEL, DEFAULT_SYSTEM_PROMPT, - HOTKEY, ) __all__ = [ "DEFAULT_MAIN_MODEL", "DEFAULT_VISION_MODEL", "DEFAULT_SYSTEM_PROMPT", - "HOTKEY", -] +] \ No newline at end of file diff --git a/kratt/config.py b/kratt/config.py index cfc0811..6192be3 100644 --- a/kratt/config.py +++ b/kratt/config.py @@ -1,15 +1,12 @@ """ Configuration constants, default settings, and settings persistence for Kratt. -This module defines default models, system prompts, hotkey bindings, -RAG (Retrieval-Augmented Generation) parameters, and handles loading/saving -user settings to a JSON file. +This module defines default models, system prompts, RAG (Retrieval-Augmented +Generation) parameters, and handles loading/saving user settings to a JSON file. """ -from pynput import keyboard import json from pathlib import Path -from pynput import keyboard # LLM Models DEFAULT_MAIN_MODEL = "qwen2.5:7b" @@ -34,9 +31,6 @@ - Use code blocks with language identifiers. """.strip() -# Global Hotkey -HOTKEY = {keyboard.Key.ctrl_l, keyboard.Key.shift_l} - # RAG Settings RAG_CHUNK_SIZE = 500 RAG_CHUNK_OVERLAP = 50 @@ -90,4 +84,4 @@ def save_settings(settings: dict) -> None: with open(SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(settings, f, indent=4) except IOError as e: - print(f"Error saving settings: {e}") + print(f"Error saving settings: {e}") \ No newline at end of file diff --git a/kratt/core/__init__.py b/kratt/core/__init__.py index 910cd9a..e4f5418 100644 --- a/kratt/core/__init__.py +++ b/kratt/core/__init__.py @@ -1,8 +1,7 @@ """ -Core backend logic (workers, search, hotkeys). +Core backend logic (workers, search). """ -from kratt.core.hotkey_manager import HotkeyManager from kratt.core.worker import OllamaWorker -__all__ = ["HotkeyManager", "OllamaWorker"] +__all__ = ["OllamaWorker"] \ No newline at end of file diff --git a/kratt/core/hotkey_manager.py b/kratt/core/hotkey_manager.py deleted file mode 100644 index e16b2b5..0000000 --- a/kratt/core/hotkey_manager.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -System-wide hotkey detection using keyboard library. - -Provides global hotkey detection across all applications on Linux/Fedora -without requiring window focus or elevated privileges. -""" -from pynput import keyboard as pynput_keyboard -from typing import Callable, Set, Optional -import keyboard - - -class HotkeyManager: - """ - Manages system-wide hotkey detection and callback invocation. - - Uses the 'keyboard' library for global hotkey detection on Linux. - - Attributes: - hotkey_set: Set of pynput Key objects representing the hotkey. - callback: Function to invoke when the hotkey is pressed. - """ - - def __init__( - self, hotkey_set: Set, callback: Callable[[], None] - ) -> None: - """ - Initialize the hotkey manager. - - Args: - hotkey_set: Set of pynput Key objects (e.g., {Key.ctrl_l, Key.alt_r}). - callback: Function to call when hotkey is triggered. - """ - self.hotkey_set = hotkey_set - self.callback = callback - self._hotkey_id: Optional[int] = None - self._setup() - - def _setup(self) -> None: - """Register the global hotkey.""" - try: - key_names = self._convert_keys_to_hotkey_string() - self._hotkey_id = keyboard.add_hotkey( - key_names, - self._on_hotkey_pressed, - suppress=False - ) - except Exception as e: - print(f"Hotkey registration failed: {e}") - - def _convert_keys_to_hotkey_string(self) -> str: - """ - Convert pynput Key objects to keyboard library hotkey format. - - The keyboard library supports modifier keys: ctrl, alt, shift. - - Returns: - A hotkey string in format "ctrl+alt+..." compatible with - the keyboard library's add_hotkey() method. - - Raises: - ValueError: If no valid keys can be mapped. - """ - key_map = { - pynput_keyboard.Key.ctrl_l: "ctrl", - pynput_keyboard.Key.ctrl_r: "ctrl", - pynput_keyboard.Key.alt_l: "alt", - pynput_keyboard.Key.alt_r: "alt", - pynput_keyboard.Key.shift_l: "shift", - pynput_keyboard.Key.shift_r: "shift", - } - - key_names = [] - for key in self.hotkey_set: - name = key_map.get(key) - if name and name not in key_names: - key_names.append(name) - - if not key_names: - raise ValueError("No valid keys found in hotkey set") - - return "+".join(sorted(key_names)) - - def _on_hotkey_pressed(self) -> None: - """Invoke the callback when hotkey is detected.""" - try: - self.callback() - except Exception: - pass - - def stop(self) -> None: - """Stop the hotkey listener and clean up resources.""" - try: - if self._hotkey_id is not None: - keyboard.remove_hotkey(self._hotkey_id) - except Exception: - pass diff --git a/kratt/main.py b/kratt/main.py index 283d383..a8894a2 100644 --- a/kratt/main.py +++ b/kratt/main.py @@ -2,7 +2,7 @@ Application entry point. Initializes the Qt Application, styling fixes for Linux, -and the global hotkey listener. +and the main window with system tray integration. """ import sys @@ -10,8 +10,6 @@ from PySide6.QtGui import QFontDatabase, QFont from PySide6.QtWidgets import QApplication from kratt.ui.main_window import MainWindow -from kratt.core.hotkey_manager import HotkeyManager -from kratt.config import HOTKEY def load_stylesheet(app: QApplication) -> None: @@ -47,13 +45,8 @@ def main() -> None: app.setStyleSheet(current_style + f"\nQToolTip {{ font-family: '{font_family}'; }}") window = MainWindow() - - # Initialize global hotkey listener - hotkey_mgr = HotkeyManager(HOTKEY, window.toggle_visibility) - window.show() exit_code = app.exec() - hotkey_mgr.stop() sys.exit(exit_code) diff --git a/kratt/ui/main_window.py b/kratt/ui/main_window.py index e032716..b8c1b00 100644 --- a/kratt/ui/main_window.py +++ b/kratt/ui/main_window.py @@ -43,8 +43,6 @@ class MainWindow(QWidget): integration for minimized state. """ - toggle_signal = Signal() - def __init__(self) -> None: """Initialize the main window and set up all components.""" super().__init__() @@ -62,7 +60,6 @@ def __init__(self) -> None: self._setup_ui() self._setup_tray() - self._setup_hotkey() self.new_chat() def _setup_ui(self) -> None: @@ -185,7 +182,7 @@ def _setup_header(self) -> None: btn_close.setObjectName("CloseBtn") btn_close.setFixedSize(28, 28) btn_close.setCursor(Qt.CursorShape.PointingHandCursor) - btn_close.clicked.connect(self.toggle_visibility) + btn_close.clicked.connect(self.hide) self.header_layout.addWidget(title) self.header_layout.addStretch() @@ -548,15 +545,4 @@ def _scroll_to_bottom(self) -> None: lambda: self.scroll_area.verticalScrollBar().setValue( self.scroll_area.verticalScrollBar().maximum() ) - ) - - def _setup_hotkey(self) -> None: - """Connect the global hotkey signal to toggle visibility.""" - self.toggle_signal.connect(self.toggle_visibility) - - def toggle_visibility(self) -> None: - """Show or hide the window, ensuring focus when shown.""" - if self.isVisible(): - self.hide() - else: - self.show_window() + ) \ No newline at end of file diff --git a/setup/setup.sh b/setup/setup.sh index 179bf02..ed61ffc 100755 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -21,14 +21,7 @@ pip install -r requirements.txt echo "Installing Playwright browsers..." playwright install -# 4. Add user to input group for global hotkey support -echo "Adding $USER to input group for global hotkey detection..." -sudo usermod -a -G input "$USER" -echo "" -echo "⚠️ Important: Log out and back in for global hotkeys to work (Ctrl+Alt_R)." -echo "" - -# 5. Systemd Auto-startup +# 4. Systemd Auto-startup read -p "Do you want to create a systemd user service for automatic startup? (y/N): " confirm if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then echo "Creating systemd user service..." diff --git a/tests/conftest.py b/tests/conftest.py index 0978e72..f023816 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,35 +1,9 @@ """ Pytest configuration and fixtures. -Handles mocking of system-level modules (pynput, keyboard) that require -input device access or display servers. +Handles mocking of system-level modules that require external services. """ -import sys -from unittest.mock import MagicMock, patch - -# Mock pynput -mock_pynput = MagicMock() -mock_pynput_keyboard = MagicMock() - -# Create a mock Key object -mock_key = MagicMock() -mock_key.ctrl_l = "ctrl_l" -mock_key.ctrl_r = "ctrl_r" -mock_key.alt_l = "alt_l" -mock_key.alt_r = "alt_r" -mock_key.shift_l = "shift_l" -mock_key.shift_r = "shift_r" - -mock_pynput_keyboard.Key = mock_key -mock_pynput.keyboard = mock_pynput_keyboard - -sys.modules['pynput'] = mock_pynput -sys.modules['pynput.keyboard'] = mock_pynput_keyboard - -# Mock keyboard -mock_keyboard = MagicMock() -sys.modules['keyboard'] = mock_keyboard - +from unittest.mock import patch import pytest diff --git a/tests/test_backend.py b/tests/test_backend.py index 43a2d43..0cf8806 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -4,18 +4,15 @@ Tests for: - Configuration loading and persistence - File system tools (search and find) -- Hotkey detection - Web search functionality (DuckDuckGo, URL normalization, scraping) """ import pytest from pathlib import Path from unittest.mock import MagicMock, patch -from pynput import keyboard from kratt import config from kratt.core.tools import find_files, search_files, execute_tool, get_tool_definitions -from kratt.core.hotkey_manager import HotkeyManager from kratt.core.web_search import ( normalize_url, improve_search_query, @@ -215,112 +212,6 @@ def test_get_tool_definitions_returns_valid_schema(self): assert all("name" in d["function"] for d in definitions) -class TestHotkeyManager: - """Test cases for global hotkey detection.""" - - def test_hotkey_manager_registers_hotkey(self, mocker): - """ - Test that hotkey manager correctly registers the global hotkey. - - Verifies hotkey registration with keyboard library. - """ - mock_add_hotkey = mocker.patch('keyboard.add_hotkey', return_value=1) - - callback = MagicMock() - manager = HotkeyManager({keyboard.Key.ctrl_l, keyboard.Key.alt_r}, callback) - - # Verify add_hotkey was called with correct key combination - mock_add_hotkey.assert_called_once() - call_args = mock_add_hotkey.call_args - assert 'alt' in call_args[0][0] - assert 'ctrl' in call_args[0][0] - - manager.stop() - - def test_hotkey_manager_triggers_callback(self, mocker): - """ - Test that hotkey manager callback is invoked when hotkey fires. - - Verifies callback registration and invocation. - """ - callback = MagicMock() - mock_add_hotkey = mocker.patch('keyboard.add_hotkey') - - manager = HotkeyManager({keyboard.Key.ctrl_l}, callback) - - # Extract and invoke the callback function - registered_callback = mock_add_hotkey.call_args[0][1] - registered_callback() - - callback.assert_called_once() - manager.stop() - - def test_hotkey_manager_stops_cleanly(self, mocker): - """ - Test that hotkey manager can be stopped cleanly. - - Verifies proper resource cleanup. - """ - callback = MagicMock() - mock_add_hotkey = mocker.patch('keyboard.add_hotkey', return_value=42) - mock_remove_hotkey = mocker.patch('keyboard.remove_hotkey') - - manager = HotkeyManager({keyboard.Key.alt_l}, callback) - manager.stop() - - # Verify remove_hotkey was called with correct hotkey ID - mock_remove_hotkey.assert_called_once_with(42) - - def test_hotkey_manager_handles_registration_error(self, mocker): - """ - Test that hotkey manager handles registration errors gracefully. - - Verifies error handling during hotkey setup. - """ - callback = MagicMock() - mocker.patch('keyboard.add_hotkey', side_effect=Exception("Permission denied")) - - # Should not raise, just print error - manager = HotkeyManager({keyboard.Key.ctrl_l}, callback) - manager.stop() - - def test_hotkey_manager_converts_keys_correctly(self, mocker): - """ - Test that pynput keys are correctly converted to keyboard library format. - - Verifies key mapping for ctrl, alt, and shift modifiers. - """ - callback = MagicMock() - mocker.patch('keyboard.add_hotkey') - - manager = HotkeyManager( - {keyboard.Key.ctrl_l, keyboard.Key.alt_r, keyboard.Key.shift_l}, - callback - ) - - # Verify conversion happened - hotkey_str = manager._convert_keys_to_hotkey_string() - assert 'ctrl' in hotkey_str - assert 'alt' in hotkey_str - assert 'shift' in hotkey_str - - def test_hotkey_manager_skips_invalid_keys(self, mocker): - """ - Test that invalid keys are skipped during conversion. - - Verifies graceful handling of unsupported keys. - """ - callback = MagicMock() - mocker.patch('keyboard.add_hotkey') - - # Use only supported keys - manager = HotkeyManager({keyboard.Key.ctrl_l, keyboard.Key.alt_l}, callback) - - hotkey_str = manager._convert_keys_to_hotkey_string() - assert hotkey_str is not None - assert len(hotkey_str) > 0 - - class TestWebSearchNormalization: """Test cases for URL normalization.""" @@ -493,4 +384,4 @@ def test_scraper_site_extraction(self): scraper = WebScraper() result = scraper.scrape_site("http://test.com", mock_page) - assert isinstance(result, dict) + assert isinstance(result, dict) \ No newline at end of file diff --git a/tests/test_ui.py b/tests/test_ui.py index 6941ba8..c39c678 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -245,6 +245,33 @@ def test_main_window_force_stop_cancels_generation(self, main_window, mocker): mock_worker.request_stop.assert_called_once() + def test_main_window_show_window_displays_and_focuses(self, main_window): + """ + Test that show_window properly displays the window and sets focus. + + Verifies window visibility and focus management. + """ + main_window.hide() + assert not main_window.isVisible() + + main_window.show_window() + + assert main_window.isVisible() + + def test_main_window_close_button_hides_window(self, main_window): + """ + Test that close button (✕) hides the window instead of closing it. + + Verifies tray integration behavior. + """ + main_window.show() + assert main_window.isVisible() + + # Simulate close button click + main_window.hide() + + assert not main_window.isVisible() + class TestSettingsDialog: """Test cases for settings configuration dialog.""" @@ -362,4 +389,4 @@ def test_workflow_new_chat_resets_state(self, main_window, mocker): main_window.new_chat() assert len(main_window.history) == 1 - assert main_window.full_response_buffer == "" + assert main_window.full_response_buffer == "" \ No newline at end of file