diff --git a/src/main.py b/src/main.py index b4f796d..1c9d473 100644 --- a/src/main.py +++ b/src/main.py @@ -101,20 +101,23 @@ def confirm_action(prompt: str) -> bool: print(colored("Please enter 'Y' or 'N'.", "red")) -def drain_notifications(pm: PasswordManager) -> None: - """Display all queued notifications.""" +def drain_notifications(pm: PasswordManager) -> str | None: + """Return the most recent queued notification message, clearing the queue.""" queue_obj = getattr(pm, "notifications", None) if queue_obj is None: - return + return None + last_note = None while True: try: - note = queue_obj.get_nowait() + last_note = queue_obj.get_nowait() except queue.Empty: break - category = note.level.lower() - if category not in ("info", "warning", "error"): - category = "info" - print(color_text(note.message, category)) + if not last_note: + return None + category = getattr(last_note, "level", "info").lower() + if category not in ("info", "warning", "error"): + category = "info" + return color_text(getattr(last_note, "message", ""), category) def handle_switch_fingerprint(password_manager: PasswordManager): @@ -949,7 +952,13 @@ def display_menu( for handler in logging.getLogger().handlers: handler.flush() print(color_text(menu, "menu")) - drain_notifications(password_manager) + print() + last_note = drain_notifications(password_manager) + sys.stdout.write("\033[F\033[2K") + if last_note: + print(last_note) + else: + print() try: choice = timed_input( "Enter your choice (1-8) or press Enter to exit: ", diff --git a/src/tests/test_menu_notifications.py b/src/tests/test_menu_notifications.py index aabf1e5..123f4b4 100644 --- a/src/tests/test_menu_notifications.py +++ b/src/tests/test_menu_notifications.py @@ -12,7 +12,8 @@ import main def _make_pm(msg): q = queue.Queue() - q.put(SimpleNamespace(message=msg, level="INFO")) + if msg is not None: + q.put(SimpleNamespace(message=msg, level="INFO")) return SimpleNamespace( notifications=q, is_dirty=False, @@ -43,4 +44,21 @@ def test_display_menu_prints_notifications(monkeypatch, capsys): with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) out = capsys.readouterr().out - assert "hello" in out + assert "\x1b[F\x1b[2K" in out + assert out.count("hello") == 1 + + +def test_display_menu_reuses_notification_line(monkeypatch, capsys): + pm = _make_pm(None) + msgs = iter(["first", "second"]) + monkeypatch.setattr(main, "_display_live_stats", lambda *_: None) + monkeypatch.setattr(main, "clear_and_print_fingerprint", lambda *a, **k: None) + inputs = iter(["9", ""]) + monkeypatch.setattr(main, "timed_input", lambda *a, **k: next(inputs)) + monkeypatch.setattr(main, "drain_notifications", lambda _pm: next(msgs, None)) + with pytest.raises(SystemExit): + main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) + out = capsys.readouterr().out + assert out.count("first") == 1 + assert out.count("second") == 1 + assert out.count("Select an option:") == 2 diff --git a/src/utils/color_scheme.py b/src/utils/color_scheme.py index b0212c2..96dccfd 100644 --- a/src/utils/color_scheme.py +++ b/src/utils/color_scheme.py @@ -20,7 +20,7 @@ _COLOR_MAP = { "index": "yellow", "menu": "cyan", "stats": "green", - "info": "cyan", + "info": "white", "warning": "yellow", "error": "red", "default": "white", @@ -32,4 +32,5 @@ def color_text(text: str, category: str = "default") -> str: color = _COLOR_MAP.get(category, "white") if color == "orange": return _apply_orange(text) - return colored(text, color) + attrs = ["bold"] if category in {"info", "warning", "error"} else None + return colored(text, color, attrs=attrs)