Skip to content

Commit ed731b5

Browse files
committed
feat(snackbar): drive overlay from tray state machine
Build one RecordingSnackbar in _TrayApp and show/hide it from the single _apply_state seam: a red "Recording" pill while capturing, an amber "Processing" pill while the pipeline runs, hidden on idle. Hide it on quit. Always-on for v1; no config/settings surface.
1 parent dccfa66 commit ed731b5

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

src/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from src.injector import type_text
4040
from src.rewrite import rewrite
4141
from src.settings_dialog import SettingsDialog
42+
from src.snackbar import RecordingSnackbar, snackbar_content_for
4243
from src.stt import transcribe
4344
from src.utils import AppError, PipelineResult, ScreamerError, SignalBridge
4445

@@ -125,6 +126,8 @@ def __init__(self, startup_mode: bool = False) -> None:
125126
self._recording = False
126127
self._enabled = True
127128

129+
self._snackbar = RecordingSnackbar()
130+
128131
self._build_tray()
129132
self._build_hotkey()
130133
self._apply_state(TrayState.IDLE)
@@ -275,6 +278,12 @@ def _apply_state(self, state: TrayState) -> None:
275278
}
276279
self._tray.setToolTip(f"Screamer — {labels[state]}")
277280

281+
content = snackbar_content_for(state.value)
282+
if content is None:
283+
self._snackbar.hide_state()
284+
else:
285+
self._snackbar.show_state(*content)
286+
278287
# ------------------------------------------------------------------
279288
# Recording lifecycle
280289
# ------------------------------------------------------------------
@@ -470,6 +479,7 @@ def _exit(self) -> None:
470479

471480
# 5. Quit Qt.
472481
self._tray.hide()
482+
self._snackbar.hide_state()
473483
QApplication.instance().quit()
474484

475485

tests/test_snackbar_wiring.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os
2+
import unittest
3+
from unittest.mock import MagicMock
4+
5+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
6+
7+
from PySide6.QtCore import QObject
8+
from PySide6.QtWidgets import QApplication
9+
10+
from src.icons import TrayState
11+
from src.main import _TrayApp
12+
13+
14+
def _bare_app():
15+
"""Construct a _TrayApp without running its real __init__/Qt build steps."""
16+
QApplication.instance() or QApplication([])
17+
app = _TrayApp.__new__(_TrayApp)
18+
QObject.__init__(app)
19+
app._tray = MagicMock() # _apply_state calls setIcon/setToolTip
20+
app._snackbar = MagicMock() # what we assert on
21+
return app
22+
23+
24+
class SnackbarWiringTests(unittest.TestCase):
25+
def test_recording_state_shows_snackbar_with_content(self):
26+
app = _bare_app()
27+
app._apply_state(TrayState.RECORDING)
28+
app._snackbar.show_state.assert_called_once_with("Recording", (229, 57, 53))
29+
app._snackbar.hide_state.assert_not_called()
30+
31+
def test_processing_state_shows_amber(self):
32+
app = _bare_app()
33+
app._apply_state(TrayState.PROCESSING)
34+
app._snackbar.show_state.assert_called_once_with("Processing", (255, 179, 0))
35+
36+
def test_idle_state_hides_snackbar(self):
37+
app = _bare_app()
38+
app._apply_state(TrayState.IDLE)
39+
app._snackbar.hide_state.assert_called_once_with()
40+
app._snackbar.show_state.assert_not_called()

0 commit comments

Comments
 (0)