feat: add hotkey conflict detection and microphone listing helpers
Implementiere drei Hilfsfunktionen in _settings.py: - check_hotkey_conflict(): Prüft mit Win32 RegisterHotKey ob eine Taste belegt ist - list_microphones(): Gibt alle Eingabegeräte als (name, index) Tupel zurück - pynput_to_evdev_key(): Konvertiert pynput Keys zu evdev Key-Namen Alle Tests (9 neue + 9 existierende) bestehen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,3 +105,80 @@ class TestApplySystemTheme:
|
|||||||
mock_set.assert_called_once_with("light")
|
mock_set.assert_called_once_with("light")
|
||||||
finally:
|
finally:
|
||||||
root.destroy()
|
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) == ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user