diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0d0f6..cec55ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ SPDX-License-Identifier: LGPL-2.1-or-later SPDX-FileCopyrightText: 2024 igo95862 --> +## 1.0.3 + +* Added new `activation_token` signal. +* Fixed `action_invoked` signal having incorrect type for the second argument. (reported by @C0rn3j) +* Fixed missing D-Bus type signatures and result argument names. +* Fixed missing D-Bus method flags. + ## 1.0.2 * Fix *create_hints* `urgency` parameter not working. (found and fixed by @dhjw) diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..0a834fa --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,172 @@ +.. SPDX-License-Identifier: LGPL-2.1-or-later +.. SPDX-FileCopyrightText: 2025 igo95862 + +Examples +======== + +Examples source code can be found in ``examples`` directory. + +Sending notification using blocking API +--------------------------------------- + +Send a simple notification without waiting for user interaction. +Blocking API version. + +.. code-block:: python + + from __future__ import annotations + + from sdbus_block.notifications import FreedesktopNotifications + + + def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + + notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="FooBar", + ) + + + if __name__ == "__main__": + main() + +Sending notification using async API +------------------------------------ + +Send a simple notification without waiting for user interaction. +Async API version. + +.. code-block:: python + + from __future__ import annotations + + from asyncio import run as asyncio_run + + from sdbus_async.notifications import FreedesktopNotifications + + # FreedesktopNotifications is a class that automatically proxies to + # the notifications daemon's service name and path. + # NotificationsInterface is the raw interface that can be used to + # implement your own daemon or proxied to non-standard path. + + + async def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + + await notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="FooBar", + ) + + + if __name__ == "__main__": + asyncio_run(main()) + +Sending notification and waiting for user interaction +----------------------------------------------------- + +Because ``python-sdbus`` only supports signals in async API there is only +async version of this. + +Send notifications with 2 buttons displaying ``Foo`` and ``Bar`` and wait for +user to press one of them or dismiss the notification. + +Remmember to bind signal reading task to a variable or otherwise it might get garbage +collected. + +Monitor ``action_invoked`` and ``notification_closed`` signals to track user interaction. + + +.. code-block:: python + + from __future__ import annotations + + from asyncio import FIRST_COMPLETED, get_running_loop + from asyncio import run as asyncio_run + from asyncio import wait + + from sdbus_async.notifications import FreedesktopNotifications + + # FreedesktopNotifications is a class that automatically proxies to + # the notifications daemon's service name and path. + # NotificationsInterface is the raw interface that can be used to + # implement your own daemon or proxied to non-standard path. + + + async def wait_action_invoked( + notifications: FreedesktopNotifications, + notifications_waiting: set[int], + ) -> None: + async for ( + notification_id, + action_key, + ) in notifications.action_invoked.catch(): + if notification_id in notifications_waiting: + print("Action invoked:", action_key) + return + + + async def wait_notification_closed( + notifications: FreedesktopNotifications, + notifications_waiting: set[int], + ) -> None: + async for ( + notification_id, + reason, + ) in notifications.notification_closed.catch(): + if notification_id in notifications_waiting: + print("Notification closed!") + return + + + async def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + notifications_waiting: set[int] = set() + + loop = get_running_loop() + + # Always bind tasks to variables or they will be garbage collected + action_invoked_task = loop.create_task( + wait_action_invoked( + notifications, + notifications_waiting, + ) + ) + notification_closed_task = loop.create_task( + wait_notification_closed( + notifications, + notifications_waiting, + ) + ) + + notification_id = await notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="Foo or Bar?", + body="Select either Foo or Bar.", + # Actions are defined in pairs of action_key to displayed string. + actions=["foo", "Foo", "bar", "Bar"], + ) + notifications_waiting.add(notification_id) + + await wait( + (action_invoked_task, notification_closed_task), + return_when=FIRST_COMPLETED, + ) + + + if __name__ == "__main__": + asyncio_run(main()) + diff --git a/docs/index.rst b/docs/index.rst index 0d06387..96b4f68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,11 @@ Freedesktop Notifications binds for python-sdbus ================================================ This package contains python-sdbus binds for -`Freedesktop notifications standard `_. +`Freedesktop notifications standard `_. + +.. toctree:: + + examples .. py:currentmodule:: sdbus_async.notifications diff --git a/examples/async/await_action.py b/examples/async/await_action.py new file mode 100644 index 0000000..6a402c1 --- /dev/null +++ b/examples/async/await_action.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2025 igo95862 +from __future__ import annotations + +from asyncio import FIRST_COMPLETED, get_running_loop +from asyncio import run as asyncio_run +from asyncio import wait + +from sdbus_async.notifications import FreedesktopNotifications + +# FreedesktopNotifications is a class that automatically proxies to +# the notifications daemon's service name and path. +# NotificationsInterface is the raw interface that can be used to +# implement your own daemon or proxied to non-standard path. + + +async def wait_action_invoked( + notifications: FreedesktopNotifications, + notifications_waiting: set[int], +) -> None: + async for ( + notification_id, + action_key, + ) in notifications.action_invoked.catch(): + if notification_id in notifications_waiting: + print("Action invoked:", action_key) + return + + +async def wait_notification_closed( + notifications: FreedesktopNotifications, + notifications_waiting: set[int], +) -> None: + async for ( + notification_id, + reason, + ) in notifications.notification_closed.catch(): + if notification_id in notifications_waiting: + print("Notification closed!") + return + + +async def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + notifications_waiting: set[int] = set() + + loop = get_running_loop() + + # Always bind tasks to variables or they will be garbage collected + action_invoked_task = loop.create_task( + wait_action_invoked( + notifications, + notifications_waiting, + ) + ) + notification_closed_task = loop.create_task( + wait_notification_closed( + notifications, + notifications_waiting, + ) + ) + + notification_id = await notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="Foo or Bar?", + body="Select either Foo or Bar.", + # Actions are defined in pairs of action_key to displayed string. + actions=["foo", "Foo", "bar", "Bar"], + ) + notifications_waiting.add(notification_id) + + await wait( + (action_invoked_task, notification_closed_task), + return_when=FIRST_COMPLETED, + ) + + +if __name__ == "__main__": + asyncio_run(main()) diff --git a/examples/async/simple.py b/examples/async/simple.py new file mode 100644 index 0000000..1415e69 --- /dev/null +++ b/examples/async/simple.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2025 igo95862 +from __future__ import annotations + +from asyncio import run as asyncio_run + +from sdbus_async.notifications import FreedesktopNotifications + +# FreedesktopNotifications is a class that automatically proxies to +# the notifications daemon's service name and path. +# NotificationsInterface is the raw interface that can be used to +# implement your own daemon or proxied to non-standard path. + + +async def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + + await notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="FooBar", + ) + + +if __name__ == "__main__": + asyncio_run(main()) diff --git a/examples/block/simple.py b/examples/block/simple.py new file mode 100644 index 0000000..40b15e8 --- /dev/null +++ b/examples/block/simple.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2025 igo95862 +from __future__ import annotations + +from sdbus_block.notifications import FreedesktopNotifications + + +def main() -> None: + # Default bus will be used if no bus is explicitly passed. + # When running as user the default bus will be session bus where + # Notifications daemon usually runs. + notifications = FreedesktopNotifications() + + notifications.notify( + # summary is the only required argument. + # For other arguments default values will be used. + summary="FooBar", + ) + + +if __name__ == "__main__": + main() diff --git a/sdbus_async/notifications/__init__.py b/sdbus_async/notifications/__init__.py index eedbbfb..dcb1f71 100644 --- a/sdbus_async/notifications/__init__.py +++ b/sdbus_async/notifications/__init__.py @@ -22,8 +22,12 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -from sdbus import (DbusInterfaceCommonAsync, dbus_method_async, - dbus_signal_async) +from sdbus import ( + DbusInterfaceCommonAsync, + DbusUnprivilegedFlag, + dbus_method_async, + dbus_signal_async, +) from sdbus.sd_bus_internals import SdBus @@ -31,7 +35,11 @@ class NotificationsInterface( DbusInterfaceCommonAsync, interface_name='org.freedesktop.Notifications'): - @dbus_method_async('u') + @dbus_method_async( + input_signature="u", + result_args_names=(), + flags=DbusUnprivilegedFlag, + ) async def close_notification(self, notif_id: int) -> None: """Close notification by id. @@ -39,7 +47,11 @@ async def close_notification(self, notif_id: int) -> None: """ raise NotImplementedError - @dbus_method_async() + @dbus_method_async( + result_signature="as", + result_args_names=('capabilities',), + flags=DbusUnprivilegedFlag, + ) async def get_capabilities(self) -> List[str]: """Returns notification daemon capabilities. @@ -66,7 +78,16 @@ async def get_capabilities(self) -> List[str]: """ raise NotImplementedError - @dbus_method_async() + @dbus_method_async( + result_signature="ssss", + result_args_names=( + 'server_name', + 'server_vendor', + 'version', + 'notifications_version', + ), + flags=DbusUnprivilegedFlag, + ) async def get_server_information(self) -> Tuple[str, str, str, str]: """Returns notification server information. @@ -76,7 +97,12 @@ async def get_server_information(self) -> Tuple[str, str, str, str]: """ raise NotImplementedError - @dbus_method_async("susssasa{sv}i") + @dbus_method_async( + input_signature="susssasa{sv}i", + result_signature="u", + result_args_names=('notif_id',), + flags=DbusUnprivilegedFlag, + ) async def notify( self, app_name: str = '', @@ -97,7 +123,8 @@ async def notify( :param str summary: Summary of notification. :param str body: Optional body of notification. :param List[str] actions: Optional list of actions presented to user. \ - List index becomes action id. + Should be sent in pairs of strings that represent action \ + key identifier and a localized string to be displayed to user. :param Dict[str,Tuple[str,Any]] hints: Extra options such as sounds \ that can be passed. See :py:meth:`create_hints`. :param int expire_timeout: Optional notification expiration timeout \ @@ -109,18 +136,23 @@ async def notify( raise NotImplementedError - @dbus_signal_async() - def action_invoked(self) -> Tuple[int, int]: + @dbus_signal_async( + signal_signature="us", + signal_args_names=('notif_id', 'action_key'), + ) + def action_invoked(self) -> Tuple[int, str]: """Signal when user invokes one of the actions specified. First element of tuple is notification id. - Second element is the index of the action invoked. \ - Matches the index of passed list of actions. + Second element is the key identifier of the action invoked. """ raise NotImplementedError - @dbus_signal_async() + @dbus_signal_async( + signal_signature="uu", + signal_args_names=('notif_id', 'reason'), + ) def notification_closed(self) -> Tuple[int, int]: """Signal when notification is closed. @@ -135,6 +167,24 @@ def notification_closed(self) -> Tuple[int, int]: """ raise NotImplementedError + @dbus_signal_async( + signal_signature="us", + signal_args_names=('notif_id', 'action_key'), + ) + def activation_token(self) -> Tuple[int, str]: + """Signal carrying window system token. + + Emitted before :py:attr:`action_invoked`. + + Carries windowing system token like X11 startup id or + Wayland adctivation token. + + First element of tuple is notification id. + + Second element is the key identifier of the action invoked. + """ + raise NotImplementedError + def create_hints( self, use_action_icons: Optional[bool] = None, diff --git a/sdbus_block/notifications/__init__.py b/sdbus_block/notifications/__init__.py index 7ebc7d8..f1e7f25 100644 --- a/sdbus_block/notifications/__init__.py +++ b/sdbus_block/notifications/__init__.py @@ -22,7 +22,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -from sdbus import DbusInterfaceCommon, dbus_method +from sdbus import DbusInterfaceCommon, DbusUnprivilegedFlag, dbus_method from sdbus.sd_bus_internals import SdBus @@ -47,7 +47,10 @@ def __init__(self, bus: Optional[SdBus] = None) -> None: bus, ) - @dbus_method('u') + @dbus_method( + input_signature="u", + flags=DbusUnprivilegedFlag, + ) def close_notification(self, notif_id: int) -> None: """Close notification by id. @@ -55,7 +58,10 @@ def close_notification(self, notif_id: int) -> None: """ raise NotImplementedError - @dbus_method() + @dbus_method( + result_signature="as", + flags=DbusUnprivilegedFlag, + ) def get_capabilities(self) -> List[str]: """Returns notification daemon capabilities. @@ -82,7 +88,10 @@ def get_capabilities(self) -> List[str]: """ raise NotImplementedError - @dbus_method() + @dbus_method( + result_signature="ssss", + flags=DbusUnprivilegedFlag, + ) def get_server_information(self) -> Tuple[str, str, str, str]: """Returns notification server information. @@ -92,7 +101,11 @@ def get_server_information(self) -> Tuple[str, str, str, str]: """ raise NotImplementedError - @dbus_method('susssasa{sv}i') + @dbus_method( + input_signature="susssasa{sv}i", + result_signature="u", + flags=DbusUnprivilegedFlag, + ) def notify( self, app_name: str = '', @@ -113,7 +126,8 @@ def notify( :param str summary: Summary of notification. :param str body: Optional body of notification. :param List[str] actions: Optional list of actions presented to user. \ - List index becomes action id. + Should be sent in pairs of strings that represent action \ + key identifier and a localized string to be displayed to user. :param Dict[str,Tuple[str,Any]] hints: Extra options such as sounds \ that can be passed. See :py:meth:`create_hints`. :param int expire_timeout: Optional notification expiration timeout \ diff --git a/setup.py b/setup.py index a985ce9..2896206 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ description=('Freedesktop notifications binds for sdbus.'), long_description=long_description, long_description_content_type='text/markdown', - version='1.0.2', + version='1.0.3', url='https://github.com/python-sdbus/python-sdbus-notifications', author='igo95862', author_email='igo95862@yandex.ru',