Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 06:25:41 -06:00
commit 80bef6f524
63 changed files with 5674 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
"""Desktop notification bridge (Linux/D-Bus).
Lets Bolt react to things that happen without you: a build finishing, a
calendar alert, a message arriving. Notifications are tailed from
`dbus-monitor`, filtered, rate-limited, and handed to the controller, which
forwards them through the normal converse() path — so the pet can say
"your deploy just went green" instead of only ever answering questions.
Off by default (NOTIFICATION_BRIDGE): every forwarded notification is a
round trip to the server, and an unfiltered desktop can be very chatty.
NOTIFICATION_FILTER (a regex) is the main knob for keeping it useful.
The dbus-monitor *parsing* is pure and unit tested; only the subprocess
plumbing needs a real session bus.
"""
from __future__ import annotations
import platform
import re
import shutil
import subprocess
import threading
from dataclasses import dataclass
from typing import Callable, Iterable, Iterator, Optional
_STRING_LINE = re.compile(r'^\s*string\s+"(.*)"\s*$')
_BLOCK_START = re.compile(r"^(method call|signal|method return|error)\b")
@dataclass(frozen=True)
class Notification:
app: str
summary: str
body: str
def as_text(self) -> str:
parts = [p for p in (self.summary, self.body) if p]
joined = "".join(parts)
return f"{self.app}: {joined}" if self.app else joined
def iter_notifications(lines: Iterable[str]) -> Iterator[Notification]:
"""Pull Notification records out of a `dbus-monitor` line stream.
A Notify call prints its arguments one per line after the header; the
string arguments arrive in the order app_name, app_icon, summary, body
(replaces_id is a uint32, so it isn't in the string list). Anything
that doesn't look like that is skipped rather than guessed at.
"""
collecting = False
strings: list[str] = []
def _emit() -> Optional[Notification]:
if len(strings) < 3:
return None
return Notification(app=strings[0].strip(), summary=strings[2].strip(),
body=(strings[3].strip() if len(strings) > 3 else ""))
for line in lines:
if _BLOCK_START.match(line):
if collecting:
notification = _emit()
if notification is not None:
yield notification
collecting = "member=Notify" in line
strings = []
continue
if not collecting:
continue
match = _STRING_LINE.match(line)
if match:
strings.append(match.group(1))
if collecting:
notification = _emit()
if notification is not None:
yield notification
class NotificationGate:
"""Filter + rate limit. Clock is passed in (monotonic seconds) so the
rate limiting is testable without sleeping."""
def __init__(self, pattern: str = "", min_interval: float = 60.0):
self._min_interval = max(0.0, min_interval)
self._last_forwarded = None
self._pattern = None
if (pattern or "").strip():
try:
self._pattern = re.compile(pattern, re.IGNORECASE)
except re.error:
self._pattern = None # a broken regex shouldn't mute everything
def matches(self, notification: Notification) -> bool:
if self._pattern is None:
return True
return bool(self._pattern.search(notification.as_text()))
def should_forward(self, notification: Notification, now: float) -> bool:
if not notification.as_text().strip():
return False
if not self.matches(notification):
return False
if self._last_forwarded is not None and now - self._last_forwarded < self._min_interval:
return False
self._last_forwarded = now
return True
def available() -> bool:
return platform.system() == "Linux" and shutil.which("dbus-monitor") is not None
class NotificationWatcher:
"""Tails dbus-monitor on a daemon thread, calling *callback* per
notification. Best-effort: if the session bus isn't reachable it reports
why via start() and stays off."""
_ARGS = [
"dbus-monitor", "--session",
"interface='org.freedesktop.Notifications',member='Notify'",
]
def __init__(self, callback: Callable[[Notification], None]):
self._callback = callback
self._process: Optional[subprocess.Popen] = None
self._thread: Optional[threading.Thread] = None
self._running = False
def start(self) -> Optional[str]:
"""None on success, otherwise the reason the bridge is off."""
if not available():
return "notification bridge needs Linux + dbus-monitor — skipping"
try:
self._process = subprocess.Popen(
self._ARGS, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, bufsize=1,
)
except Exception as exc:
return f"couldn't start dbus-monitor ({exc}) — notification bridge off"
self._running = True
self._thread = threading.Thread(target=self._pump, name="notification-bridge", daemon=True)
self._thread.start()
return None
def _pump(self) -> None:
assert self._process is not None and self._process.stdout is not None
try:
for notification in iter_notifications(self._process.stdout):
if not self._running:
return
try:
self._callback(notification)
except Exception:
pass # one bad notification shouldn't end the bridge
except Exception:
pass
def stop(self) -> None:
self._running = False
if self._process is not None:
try:
self._process.terminate()
except Exception:
pass
self._process = None