feat: add Win32TrayApp and NoOpTray with state management
This commit is contained in:
@@ -22,3 +22,38 @@ class TestCreateIcon:
|
||||
waiting = create_icon(AppState.WAITING)
|
||||
recording = create_icon(AppState.RECORDING)
|
||||
assert waiting.tobytes() != recording.tobytes()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows")
|
||||
class TestWin32TrayApp:
|
||||
def test_set_state_updates_icon(self):
|
||||
from unittest.mock import MagicMock, patch
|
||||
from whisper_local.tray._tray import AppState, Win32TrayApp
|
||||
|
||||
app = Win32TrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
|
||||
mock_icon = MagicMock()
|
||||
app._icon = mock_icon
|
||||
|
||||
app.set_state(AppState.RECORDING)
|
||||
|
||||
assert mock_icon.icon is not None
|
||||
|
||||
def test_set_state_before_start_is_safe(self):
|
||||
from unittest.mock import MagicMock
|
||||
from whisper_local.tray._tray import AppState, Win32TrayApp
|
||||
|
||||
app = Win32TrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
app.set_state(AppState.WAITING) # kein Fehler, _icon ist None
|
||||
|
||||
|
||||
class TestNoOpTray:
|
||||
def test_start_does_nothing(self):
|
||||
from whisper_local.tray._tray import AppState, NoOpTray
|
||||
tray = NoOpTray()
|
||||
tray.start() # kein Fehler
|
||||
|
||||
def test_set_state_does_nothing(self):
|
||||
from whisper_local.tray._tray import AppState, NoOpTray
|
||||
tray = NoOpTray()
|
||||
tray.set_state(AppState.RECORDING) # kein Fehler
|
||||
|
||||
@@ -1,9 +1,62 @@
|
||||
"""Tray-App und App-Zustände für whisper-local (Windows)."""
|
||||
|
||||
import enum
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class AppState(enum.Enum):
|
||||
WAITING = "waiting"
|
||||
RECORDING = "recording"
|
||||
TRANSCRIBING = "transcribing"
|
||||
|
||||
|
||||
class Win32TrayApp:
|
||||
"""Tray-Icon via pystray für Windows."""
|
||||
|
||||
def __init__(self, on_settings: Callable[[], None], on_quit: Callable[[], None]):
|
||||
self._on_settings = on_settings
|
||||
self._on_quit = on_quit
|
||||
self._icon = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""Startet pystray in einem Daemon-Thread."""
|
||||
import pystray
|
||||
from whisper_local.tray._icon import create_icon
|
||||
|
||||
menu = pystray.Menu(
|
||||
pystray.MenuItem("Einstellungen", self._menu_settings),
|
||||
pystray.MenuItem("Beenden", self._menu_quit),
|
||||
)
|
||||
self._icon = pystray.Icon(
|
||||
"whisper-local",
|
||||
create_icon(AppState.WAITING),
|
||||
"whisper-local",
|
||||
menu,
|
||||
)
|
||||
thread = threading.Thread(target=self._icon.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def set_state(self, state: AppState) -> None:
|
||||
"""Tauscht das Icon aus (thread-sicher)."""
|
||||
if self._icon is not None:
|
||||
from whisper_local.tray._icon import create_icon
|
||||
self._icon.icon = create_icon(state)
|
||||
|
||||
def _menu_settings(self, icon, item) -> None:
|
||||
self._on_settings()
|
||||
|
||||
def _menu_quit(self, icon, item) -> None:
|
||||
if self._icon is not None:
|
||||
self._icon.stop()
|
||||
self._on_quit()
|
||||
|
||||
|
||||
class NoOpTray:
|
||||
"""Platzhalter-Implementierung für nicht-Windows-Plattformen."""
|
||||
|
||||
def start(self) -> None:
|
||||
pass
|
||||
|
||||
def set_state(self, state: AppState) -> None:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user