diff --git a/enodia_sentinel/tray/notify.py b/enodia_sentinel/tray/notify.py new file mode 100644 index 0000000..45706df --- /dev/null +++ b/enodia_sentinel/tray/notify.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Desktop notification helper. Prefers notify-send; no GUI imports.""" +from __future__ import annotations + +import shutil +import subprocess +from typing import Callable, Optional, Sequence + +Runner = Callable[[Sequence[str]], "subprocess.CompletedProcess"] + + +def _default_runner(cmd: Sequence[str]) -> "subprocess.CompletedProcess": + try: + return subprocess.run(list(cmd), capture_output=True, text=True, + timeout=10, check=False) + except (OSError, subprocess.TimeoutExpired) as exc: + return subprocess.CompletedProcess(list(cmd), 127, "", str(exc)) + + +def notify(title: str, body: str, *, runner: Optional[Runner] = None, + which: Callable[[str], object] = shutil.which) -> bool: + """Show a desktop notification. Returns True if notify-send was invoked.""" + if which("notify-send") is None: + return False + run = runner or _default_runner + run(["notify-send", "-a", "Enodia Sentinel", title, body]) + return True diff --git a/tests/test_tray_notify.py b/tests/test_tray_notify.py new file mode 100644 index 0000000..f55caf3 --- /dev/null +++ b/tests/test_tray_notify.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Desktop notification helper: notify-send when available, else no-op.""" +import subprocess +import unittest + +from enodia_sentinel.tray import notify + + +class TestNotify(unittest.TestCase): + def test_no_notify_send_is_noop(self): + calls = [] + + def runner(cmd): + calls.append(cmd) + return subprocess.CompletedProcess(cmd, 0) + + used = notify.notify("t", "b", runner=runner, which=lambda _n: None) + self.assertFalse(used) + self.assertEqual(calls, []) + + def test_uses_notify_send_with_app_name(self): + calls = [] + + def runner(cmd): + calls.append(cmd) + return subprocess.CompletedProcess(cmd, 0) + + used = notify.notify("Title", "Body", runner=runner, + which=lambda _n: "/usr/bin/notify-send") + self.assertTrue(used) + self.assertEqual(len(calls), 1) + self.assertIn("notify-send", calls[0][0]) + self.assertIn("Title", calls[0]) + self.assertIn("Body", calls[0])