Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies = [
"bashlex==0.18",
"pathspec==1.1.1",
"notify-py==0.3.43",
"jeepney==0.9.0; sys_platform == 'linux'",
"pyperclip==1.11.0",
"watchdog==6.0.0",
"setproctitle==1.3.7",
Expand Down
76 changes: 72 additions & 4 deletions src/toad/setting_choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from dataclasses import dataclass
from typing import ClassVar
from random import choice
from threading import Lock
from textual.content import Content
from toad.widgets.future_text import FutureText

import toad
from agent_comms.declared_family import DeclaredFamily
from toad.setting_declarations import Choice

Expand Down Expand Up @@ -96,24 +98,90 @@ def shown(cls, count: int) -> bool:
return False


class DesktopService:
"""The platform's notification service, as notifypy detects it."""

def notification(self):
from notifypy import Notify

return Notify()

def send(self, app, notification) -> DesktopService:
notification.send()
return self


class DBusDesktop(DesktopService):
"""notifypy's notify-send path inherits Toad's terminal as stderr; its D-Bus
notifier starts no child process that could write over the TUI."""

def notification(self):
from notifypy import Notify
from notifypy.os_notifiers.linux import LinuxNotifier

return Notify(use_custom_notifier=LinuxNotifier)


class UncheckedDBusDesktop(DBusDesktop):
"""Asks the session bus once whether anything serves notifications, because
notifypy's D-Bus notifier reports an error reply as a delivery."""

name: ClassVar[str] = "org.freedesktop.Notifications"

def send(self, app, notification) -> DesktopService:
return self.served(app).send(app, notification)

def served(self, app) -> DesktopService:
from jeepney import DBusAddress, new_method_call
from jeepney.io.blocking import open_dbus_connection

bus = DBusAddress("/org/freedesktop/DBus", bus_name="org.freedesktop.DBus",
interface="org.freedesktop.DBus")
try:
with open_dbus_connection(bus="SESSION") as connection:
owned = connection.send_and_get_reply(
new_method_call(bus, "NameHasOwner", "s", (self.name,)), timeout=2).body[0]
activatable = self.name in connection.send_and_get_reply(
new_method_call(bus, "ListActivatableNames"), timeout=2).body[0]
except OSError:
owned = activatable = False
if owned or activatable:
return DBusDesktop()
app.call_from_thread(
app.notify,
"Desktop notifications are off for this session: no notification daemon is running.",
severity="warning",
)
return AbsentDesktop()


class AbsentDesktop(DesktopService):
"""Nothing serves notifications; asking again per message only fails again."""

def send(self, app, notification) -> DesktopService:
return self


class NotificationPolicy(Choice, DeclaredFamily, affix="Notification"):
desktop: ClassVar[DesktopService] = UncheckedDBusDesktop() if toad.os == "linux" else DesktopService()
sending: ClassVar[Lock] = Lock()

@classmethod
def deliver(cls, app, message: str, *, title: str = "", sound: str | None = None) -> None:
"""Shared delivery belongs to the policy that admits it."""
if not cls.enabled(app.app_focus):
return
from importlib.resources import files
from notifypy import Notify
import toad

notification = Notify()
notification = cls.desktop.notification()
notification.message = message
notification.title = title
notification.application_name = "🐸 Toad" if toad.os == "macos" else "Toad"
if sound and app.settings.notifications.enable_sounds:
notification.audio = str(files("toad.data").joinpath(f"sounds/{sound}.wav"))
notification.icon = str(files("toad.data").joinpath("images/frog.png"))
notification.send()
with cls.sending:
NotificationPolicy.desktop = NotificationPolicy.desktop.send(app, notification)

@classmethod
@abstractmethod
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading