refactor: Windows-Hotkey-Record in eigenes Modul auslagern

This commit is contained in:
2026-04-11 21:13:10 +02:00
parent 64bd584181
commit f380828309
3 changed files with 112 additions and 70 deletions
+35 -63
View File
@@ -1,8 +1,8 @@
"""Einstellungs-Dialog für whisper-local (Windows)."""
"""Einstellungs-Dialog für whisper-local (cross-platform)."""
from __future__ import annotations
import ctypes
import sys
import threading
from typing import Callable
@@ -12,42 +12,6 @@ from whisper_local.config import Config, save_config
from whisper_local.tray._theme import apply_system_theme
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()
@@ -58,12 +22,22 @@ def list_microphones() -> list[tuple[str, int]]:
]
def _get_record_hotkey():
"""Wählt das plattformspezifische record_hotkey-Callable."""
if sys.platform == "win32":
from whisper_local.tray._hotkey_record_pynput import record_hotkey
return record_hotkey
from whisper_local.tray._hotkey_record_evdev import record_hotkey
return record_hotkey
class SettingsDialog:
"""Einstellungs-Dialog (läuft in eigenem Thread)."""
def __init__(self, config: Config, on_save: Callable[[Config], None]):
self._config = config
self._on_save = on_save
self._cancel_event = threading.Event()
def open(self) -> None:
"""Öffnet den Dialog in einem Daemon-Thread."""
@@ -94,36 +68,28 @@ class SettingsDialog:
row=1, column=0, columnspan=3, sticky=tk.W
)
def record_hotkey():
record_hotkey = _get_record_hotkey()
def do_record():
hotkey_var.set("...")
conflict_var.set("")
captured: list[str] = []
def on_result(evdev_key: str, has_conflict: bool):
root.after(0, lambda: hotkey_var.set(evdev_key))
if has_conflict:
root.after(
0,
lambda: conflict_var.set(
"⚠ Taste ist von einer anderen App belegt (Win32-Hotkeys)"
),
)
def on_press(key):
evdev = pynput_to_evdev_key(key)
if evdev:
captured.append(evdev)
return False # Listener stoppen
def worker():
record_hotkey(on_result, self._cancel_event)
def listen():
from pynput.keyboard import Listener
with Listener(on_press=on_press) as lst:
lst.join()
if captured:
evdev = captured[0]
root.after(0, lambda: hotkey_var.set(evdev))
if check_hotkey_conflict(evdev):
root.after(
0,
lambda: conflict_var.set(
"⚠ Taste ist von einer anderen App belegt (Win32-Hotkeys)"
),
)
threading.Thread(target=worker, daemon=True).start()
threading.Thread(target=listen, daemon=True).start()
ttk.Button(frame, text="Aufzeichnen", command=record_hotkey).grid(
ttk.Button(frame, text="Aufzeichnen", command=do_record).grid(
row=0, column=2, padx=4
)
@@ -154,9 +120,15 @@ class SettingsDialog:
)
save_config(new_config)
self._on_save(new_config)
self._cancel_event.set()
root.destroy()
def cancel():
self._cancel_event.set()
root.destroy()
ttk.Button(btn_frame, text="Speichern", command=save).pack(side=tk.RIGHT, padx=4)
ttk.Button(btn_frame, text="Abbrechen", command=root.destroy).pack(side=tk.RIGHT)
ttk.Button(btn_frame, text="Abbrechen", command=cancel).pack(side=tk.RIGHT)
root.protocol("WM_DELETE_WINDOW", cancel)
root.mainloop()