From eb74e4787fa3465515a5a798beeb51e8d2aa72df Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Fri, 10 Apr 2026 21:07:48 +0200 Subject: [PATCH] feat: add AppState enum and programmatic tray icon generation Implements Task 5 of the TDD plan: creates the tray package with AppState enum (WAITING, RECORDING, TRANSCRIBING) and a create_icon() function that generates colorized microphone icons via Pillow. - whisper_local/tray/__init__.py: empty package marker - whisper_local/tray/_tray.py: AppState enum - whisper_local/tray/_icon.py: icon generation with state-specific colors - tests/test_tray.py: comprehensive test coverage for icon creation Co-Authored-By: Claude Sonnet 4.6 --- tests/test_tray.py | 24 ++++++++++++++++ whisper_local/tray/__init__.py | 1 + whisper_local/tray/_icon.py | 50 ++++++++++++++++++++++++++++++++++ whisper_local/tray/_tray.py | 9 ++++++ 4 files changed, 84 insertions(+) create mode 100644 tests/test_tray.py create mode 100644 whisper_local/tray/__init__.py create mode 100644 whisper_local/tray/_icon.py create mode 100644 whisper_local/tray/_tray.py diff --git a/tests/test_tray.py b/tests/test_tray.py new file mode 100644 index 0000000..eaa50ee --- /dev/null +++ b/tests/test_tray.py @@ -0,0 +1,24 @@ +import sys +import pytest + + +@pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows") +class TestCreateIcon: + def test_returns_image_for_each_state(self): + from PIL import Image + from whisper_local.tray._tray import AppState + from whisper_local.tray._icon import create_icon + + for state in AppState: + img = create_icon(state) + assert isinstance(img, Image.Image) + assert img.size == (64, 64) + assert img.mode == "RGBA" + + def test_different_states_have_different_colors(self): + from whisper_local.tray._tray import AppState + from whisper_local.tray._icon import create_icon + + waiting = create_icon(AppState.WAITING) + recording = create_icon(AppState.RECORDING) + assert waiting.tobytes() != recording.tobytes() diff --git a/whisper_local/tray/__init__.py b/whisper_local/tray/__init__.py new file mode 100644 index 0000000..55a8d9f --- /dev/null +++ b/whisper_local/tray/__init__.py @@ -0,0 +1 @@ +"""Tray-Package — wird in späteren Tasks befüllt.""" diff --git a/whisper_local/tray/_icon.py b/whisper_local/tray/_icon.py new file mode 100644 index 0000000..0657a8b --- /dev/null +++ b/whisper_local/tray/_icon.py @@ -0,0 +1,50 @@ +"""Programmatische Icon-Generierung via Pillow.""" + +from PIL import Image, ImageDraw + +from whisper_local.tray._tray import AppState + +_STATE_COLORS: dict[AppState, tuple[int, int, int]] = { + AppState.WAITING: (150, 150, 150), + AppState.RECORDING: (220, 50, 50), + AppState.TRANSCRIBING: (220, 180, 0), +} + + +def create_icon(state: AppState, size: int = 64) -> Image.Image: + """Erzeugt ein Mikrofon-Icon in der Farbe des übergebenen Zustands.""" + color = _STATE_COLORS[state] + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + cx = size // 2 + bw = max(4, size // 6) # Hälfte der Körperbreite + top = size // 8 + mid = size // 2 + + # Mikrofon-Körper (abgerundetes Rechteck) + draw.rounded_rectangle( + [cx - bw, top, cx + bw, mid + bw], + radius=bw, + fill=color, + ) + + # Bogen (Stativ-Bogen) + arc_r = size // 4 + arc_top = mid + lw = max(2, size // 20) + draw.arc( + [cx - arc_r, arc_top, cx + arc_r, arc_top + arc_r], + start=0, end=180, fill=color, width=lw, + ) + + # Stiel + pole_top = arc_top + arc_r // 2 + pole_bot = size - size // 8 + draw.line([cx, pole_top, cx, pole_bot], fill=color, width=lw) + + # Sockel + base = size // 5 + draw.line([cx - base, pole_bot, cx + base, pole_bot], fill=color, width=lw) + + return img diff --git a/whisper_local/tray/_tray.py b/whisper_local/tray/_tray.py new file mode 100644 index 0000000..1959ebb --- /dev/null +++ b/whisper_local/tray/_tray.py @@ -0,0 +1,9 @@ +"""Tray-App und App-Zustände für whisper-local (Windows).""" + +import enum + + +class AppState(enum.Enum): + WAITING = "waiting" + RECORDING = "recording" + TRANSCRIBING = "transcribing"