diff --git a/tests/test_tray.py b/tests/test_tray.py index c29410c..df06495 100644 --- a/tests/test_tray.py +++ b/tests/test_tray.py @@ -105,3 +105,80 @@ class TestApplySystemTheme: mock_set.assert_called_once_with("light") finally: root.destroy() + + +@pytest.mark.skipif(sys.platform != "win32", reason="Settings nur auf Windows") +class TestCheckHotkeyConflict: + def test_returns_false_when_key_is_free(self): + from unittest.mock import patch, MagicMock + from whisper_local.tray._settings import check_hotkey_conflict + + mock_user32 = MagicMock() + mock_user32.RegisterHotKey.return_value = 1 # Erfolg + with patch("ctypes.windll") as mock_windll: + mock_windll.user32 = mock_user32 + result = check_hotkey_conflict("KEY_F12") + assert result is False + mock_user32.UnregisterHotKey.assert_called_once() + + def test_returns_true_when_key_is_taken(self): + from unittest.mock import patch, MagicMock + from whisper_local.tray._settings import check_hotkey_conflict + + mock_user32 = MagicMock() + mock_user32.RegisterHotKey.return_value = 0 # Belegt + with patch("ctypes.windll") as mock_windll: + mock_windll.user32 = mock_user32 + result = check_hotkey_conflict("KEY_F12") + assert result is True + mock_user32.UnregisterHotKey.assert_not_called() + + def test_returns_false_for_unknown_key(self): + from whisper_local.tray._settings import check_hotkey_conflict + result = check_hotkey_conflict("KEY_NONEXISTENT_999") + assert result is False + + +class TestListMicrophones: + def test_returns_only_input_devices(self): + from unittest.mock import patch + from whisper_local.tray._settings import list_microphones + + fake_devices = [ + {"name": "Speakers", "max_input_channels": 0}, + {"name": "Headset Mic", "max_input_channels": 1}, + {"name": "USB Mic", "max_input_channels": 2}, + ] + with patch("sounddevice.query_devices", return_value=fake_devices): + result = list_microphones() + assert result == [("Headset Mic", 1), ("USB Mic", 2)] + + def test_returns_empty_list_when_no_input(self): + from unittest.mock import patch + from whisper_local.tray._settings import list_microphones + + with patch("sounddevice.query_devices", return_value=[]): + result = list_microphones() + assert result == [] + + +class TestPynputToEvdevKey: + def test_function_key(self): + from pynput.keyboard import Key + from whisper_local.tray._settings import pynput_to_evdev_key + assert pynput_to_evdev_key(Key.f12) == "KEY_F12" + + def test_space_key(self): + from pynput.keyboard import Key + from whisper_local.tray._settings import pynput_to_evdev_key + assert pynput_to_evdev_key(Key.space) == "KEY_SPACE" + + def test_char_key(self): + from pynput.keyboard import KeyCode + from whisper_local.tray._settings import pynput_to_evdev_key + key = KeyCode.from_char("a") + assert pynput_to_evdev_key(key) == "KEY_A" + + def test_unknown_returns_empty(self): + from whisper_local.tray._settings import pynput_to_evdev_key + assert pynput_to_evdev_key(None) == "" diff --git a/whisper_local/tray/_settings.py b/whisper_local/tray/_settings.py new file mode 100644 index 0000000..7c54168 --- /dev/null +++ b/whisper_local/tray/_settings.py @@ -0,0 +1,57 @@ +"""Einstellungs-Dialog für whisper-local (Windows).""" + +from __future__ import annotations + +import ctypes +import threading +from typing import Callable + +import sounddevice as sd + +from whisper_local.config import Config, save_config + + +def pynput_to_evdev_key(key) -> str: + """Konvertiert pynput-Key zu evdev-Key-Namen (z.B. Key.f12 → 'KEY_F12').""" + from pynput.keyboard import Key, KeyCode + + if isinstance(key, Key): + return f"KEY_{key.name.upper()}" + if isinstance(key, KeyCode) and key.char: + return f"KEY_{key.char.upper()}" + return "" + + +def check_hotkey_conflict(evdev_name: str) -> bool: + """Gibt True zurück wenn die Taste per Win32-RegisterHotKey belegt ist.""" + from whisper_local.hotkey._pynput import _evdev_to_pynput_key + from pynput.keyboard import Key + + try: + pynput_key = _evdev_to_pynput_key(evdev_name) + except ValueError: + return False + + if not isinstance(pynput_key, Key): + return False + + vk = getattr(pynput_key.value, "vk", None) + if vk is None: + return False + + HOTKEY_ID = 0x7FFF + user32 = ctypes.windll.user32 + if user32.RegisterHotKey(None, HOTKEY_ID, 0, vk): + user32.UnregisterHotKey(None, HOTKEY_ID) + return False + return True + + +def list_microphones() -> list[tuple[str, int]]: + """Gibt Liste aller Eingabegeräte als (name, index) zurück.""" + devices = sd.query_devices() + return [ + (dev["name"], idx) + for idx, dev in enumerate(devices) + if dev["max_input_channels"] > 0 + ]