diff --git a/src/main.py b/src/main.py index 37981ef..7a95e32 100644 --- a/src/main.py +++ b/src/main.py @@ -281,6 +281,7 @@ def _display_live_stats( displayed if newer data exists on Nostr. """ + stats_mgr = getattr(password_manager, "stats_manager", None) display_fn = getattr(password_manager, "display_stats", None) sync_fn = getattr(password_manager, "start_background_sync", None) if not callable(display_fn): @@ -294,12 +295,17 @@ def _display_live_stats( if not sys.stdin or not sys.stdin.isatty(): clear_screen() - display_fn() + if stats_mgr is not None: + stats_mgr.display_stats_once(password_manager) + else: + display_fn() note = get_notification_text(password_manager) if note: print(note) print(colored("Press Enter to continue.", "cyan")) pause() + if stats_mgr is not None: + stats_mgr.reset() return while True: @@ -309,7 +315,10 @@ def _display_live_stats( except Exception: # pragma: no cover - sync best effort logging.debug("Background sync failed during stats display") clear_screen() - display_fn() + if stats_mgr is not None: + stats_mgr.display_stats_once(password_manager) + else: + display_fn() note = get_notification_text(password_manager) if note: print(note) @@ -324,6 +333,8 @@ def _display_live_stats( except KeyboardInterrupt: print() break + if stats_mgr is not None: + stats_mgr.reset() def handle_display_stats(password_manager: PasswordManager) -> None: diff --git a/src/seedpass/core/__init__.py b/src/seedpass/core/__init__.py index 4610c5e..1d23614 100644 --- a/src/seedpass/core/__init__.py +++ b/src/seedpass/core/__init__.py @@ -4,7 +4,14 @@ from importlib import import_module -__all__ = ["PasswordManager", "ConfigManager", "Vault", "EntryType", "StateManager"] +__all__ = [ + "PasswordManager", + "ConfigManager", + "Vault", + "EntryType", + "StateManager", + "StatsManager", +] def __getattr__(name: str): @@ -18,4 +25,6 @@ def __getattr__(name: str): return import_module(".entry_types", __name__).EntryType if name == "StateManager": return import_module(".state_manager", __name__).StateManager + if name == "StatsManager": + return import_module(".stats_manager", __name__).StatsManager raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 19e8767..bf381a5 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -99,6 +99,7 @@ from utils.fingerprint_manager import FingerprintManager from nostr.client import NostrClient, DEFAULT_RELAYS, MANIFEST_ID_PREFIX from .config_manager import ConfigManager from .state_manager import StateManager +from .stats_manager import StatsManager # Instantiate the logger logger = logging.getLogger(__name__) @@ -163,6 +164,7 @@ class PasswordManager: self.nostr_client: Optional[NostrClient] = None self.config_manager: Optional[ConfigManager] = None self.state_manager: Optional[StateManager] = None + self.stats_manager: StatsManager = StatsManager() self.notifications: queue.Queue[Notification] = queue.Queue() self._current_notification: Optional[Notification] = None self._notification_expiry: float = 0.0 diff --git a/src/seedpass/core/stats_manager.py b/src/seedpass/core/stats_manager.py new file mode 100644 index 0000000..f1008f8 --- /dev/null +++ b/src/seedpass/core/stats_manager.py @@ -0,0 +1,20 @@ +"""Manage display of stats screens.""" + +from __future__ import annotations + + +class StatsManager: + """Track whether stats have been displayed.""" + + def __init__(self) -> None: + self._displayed = False + + def display_stats_once(self, manager) -> None: + """Display stats using ``manager`` once per reset.""" + if not self._displayed: + manager.display_stats() + self._displayed = True + + def reset(self) -> None: + """Reset the displayed flag.""" + self._displayed = False diff --git a/src/tests/test_stats_screen.py b/src/tests/test_stats_screen.py index 182c7b9..6a76872 100644 --- a/src/tests/test_stats_screen.py +++ b/src/tests/test_stats_screen.py @@ -2,6 +2,7 @@ import sys from types import SimpleNamespace from pathlib import Path import pytest +from seedpass.core.stats_manager import StatsManager sys.path.append(str(Path(__file__).resolve().parents[1])) @@ -12,6 +13,7 @@ def _make_pm(): return SimpleNamespace( display_stats=lambda: print("stats"), start_background_sync=lambda: None, + stats_manager=StatsManager(), ) @@ -57,3 +59,32 @@ def test_live_stats_triggers_background_sync(monkeypatch): main._display_live_stats(pm) assert called["sync"] >= 1 + + +def test_stats_display_only_once(monkeypatch, capsys): + pm = _make_pm() + monkeypatch.setattr(main, "get_notification_text", lambda *_: "") + + events = [TimeoutError(), KeyboardInterrupt()] + + def fake_input(*_args, **_kwargs): + raise events.pop(0) + + monkeypatch.setattr(main, "timed_input", fake_input) + main._display_live_stats(pm, interval=0.01) + out = capsys.readouterr().out + assert out.count("stats") == 1 + + +def test_stats_display_resets_after_exit(monkeypatch, capsys): + pm = _make_pm() + monkeypatch.setattr(main, "get_notification_text", lambda *_: "") + monkeypatch.setattr( + main, + "timed_input", + lambda *_args, **_kwargs: (_ for _ in ()).throw(KeyboardInterrupt()), + ) + main._display_live_stats(pm) + main._display_live_stats(pm) + out = capsys.readouterr().out + assert out.count("stats") == 2