Add support for controlling the sound played with notifications

This commit is contained in:
Kovid Goyal 2024-08-03 13:46:38 +05:30
parent 5e809d2767
commit bd8162fa42
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
11 changed files with 104 additions and 24 deletions

View file

@ -333,6 +333,25 @@ The terminal *must not* send a response unless report is requested with
.. _notifications_query: .. _notifications_query:
Playing a sound with notifications
-----------------------------------------
.. versionadded:: 0.36.0
The ability to control the sound played with notifications
By default, notifications may or may not have a sound associated with them
depending on the policies of the OS notifications service. Sometimes it
might be useful to ensure a notification is not accompanied by a sound.
This can be done by using the ``s`` key which accepts :ref:`base64` encoded
UTF-8 text as its value. Using a value of ``silent`` means the notification
will not be accompanied with a sound. A value of ``system`` (the default)
means that the OS notifications default policies are followed. Any other name
is implementation dependent. For example, on Linux, one can use the `standard
sound names
<https://specifications.freedesktop.org/sound-naming-spec/latest/#names>`__.
Support for sounds can be queried as described below.
Querying for support Querying for support
------------------------- -------------------------
@ -372,6 +391,12 @@ Key Value
``p`` key that the terminal implements). These must contain at least ``p`` key that the terminal implements). These must contain at least
``title``. ``title``.
``s`` Comma separated list of keywords ``silent`` and ``xdg-names`` indicating
support for silent notifications and for passing of `Freedesktop
standard sound names
<https://specifications.freedesktop.org/sound-naming-spec/latest/#names>`__ to the
desktop notification service for custom sounds.
``u`` Comma separated list of urgency values that the terminal implements. ``u`` Comma separated list of urgency values that the terminal implements.
If urgency is not supported, the ``u`` key must be absent from the If urgency is not supported, the ``u`` key must be absent from the
query response. query response.
@ -441,6 +466,10 @@ Key Value Default Description
``?``, ``alive``, ``?``, ``alive``,
``buttons`` ``buttons``
``s`` :ref:`base64` ``system`` The sound name to play with the notification. ``silent`` means no sound.
encoded sound ``system`` means to play the default sound, if any, of the platform notification service.
name Other names are implementation dependent.
``t`` :ref:`base64` ``unset`` The type of the notification. Used to filter out notifications. Can be specified multiple times. ``t`` :ref:`base64` ``unset`` The type of the notification. Used to filter out notifications. Can be specified multiple times.
encoded UTF-8 encoded UTF-8
notification type notification type

View file

@ -34,6 +34,9 @@ def main(nc: NotificationCommand) -> bool:
if 'unwanted' in nc.title.lower(): if 'unwanted' in nc.title.lower():
return True return True
# force the notification to be silent
nc.sound_name = 'silent'
# filter out notifications from the application badapp # filter out notifications from the application badapp
if nc.application_name == 'badapp': if nc.application_name == 'badapp':
return True return True

2
glfw/glfw3.h vendored
View file

@ -1317,7 +1317,7 @@ typedef struct GLFWLayerShellConfig {
typedef struct GLFWDBUSNotificationData { typedef struct GLFWDBUSNotificationData {
const char *app_name, *icon, *summary, *body, *category, **actions; size_t num_actions; const char *app_name, *icon, *summary, *body, *category, **actions; size_t num_actions;
int32_t timeout; uint8_t urgency; uint32_t replaces; int32_t timeout; uint8_t urgency; uint32_t replaces; int muted;
} GLFWDBUSNotificationData; } GLFWDBUSNotificationData;
/*! @brief The function pointer type for error callbacks. /*! @brief The function pointer type for error callbacks.

1
glfw/linux_notify.c vendored
View file

@ -178,6 +178,7 @@ glfw_dbus_send_user_notification(const GLFWDBUSNotificationData *n, GLFWDBusnoti
} }
append_sv_dictionary_entry("urgency", DBUS_TYPE_BYTE, n->urgency); append_sv_dictionary_entry("urgency", DBUS_TYPE_BYTE, n->urgency);
if (n->category && n->category[0]) append_sv_dictionary_entry("category", DBUS_TYPE_STRING, n->category); if (n->category && n->category[0]) append_sv_dictionary_entry("category", DBUS_TYPE_STRING, n->category);
if (n->muted) append_sv_dictionary_entry("suppress-sound", DBUS_TYPE_BOOLEAN, n->muted);
check_call(dbus_message_iter_close_container, &args, &array); check_call(dbus_message_iter_close_container, &args, &array);
APPEND(args, DBUS_TYPE_INT32, n->timeout) APPEND(args, DBUS_TYPE_INT32, n->timeout)

View file

@ -68,6 +68,9 @@ func (p *parsed_data) create_metadata() string {
if p.opts.IconCacheId != "" { if p.opts.IconCacheId != "" {
ans = append(ans, "g="+p.opts.IconCacheId) ans = append(ans, "g="+p.opts.IconCacheId)
} }
if p.opts.SoundName != "system" {
ans = append(ans, "s="+b64encode(p.opts.SoundName))
}
m := strings.Join(ans, ":") m := strings.Join(ans, ":")
if m != "" { if m != "" {
m = ":" + m m = ":" + m

View file

@ -50,6 +50,14 @@ def OPTIONS() -> str:
action or OS policy. action or OS policy.
--sound-name -s
default=system
The name of the sound to play with the notification. :code:`system` means let the
notification system use whatever sound it wants. :code:`silent` means prevent
any sound from being played. Any other value is passed to the desktop's notification system
which may or may not honor it.
--type -t --type -t
The notification type. Can be any string, it is used by users to create filter rules The notification type. Can be any string, it is used by users to create filter rules
for notifications, so choose something descriptive of the notification's purpose. for notifications, so choose something descriptive of the notification's purpose.

View file

@ -464,7 +464,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
} }
static void static void
schedule_notification(const char *appname, const char *identifier, const char *title, const char *body, const char *image_path, int urgency, const char *category_id) {@autoreleasepool { schedule_notification(const char *appname, const char *identifier, const char *title, const char *body, const char *image_path, int urgency, const char *category_id, bool muted) {@autoreleasepool {
UNUserNotificationCenter *center = get_notification_center_safely(); UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) return; if (!center) return;
// Configure the notification's payload. // Configure the notification's payload.
@ -473,7 +473,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
if (body) content.body = @(body); if (body) content.body = @(body);
if (appname) content.threadIdentifier = @(appname); if (appname) content.threadIdentifier = @(appname);
if (category_id) content.categoryIdentifier = @(category_id); if (category_id) content.categoryIdentifier = @(category_id);
content.sound = [UNNotificationSound defaultSound]; if (!muted) content.sound = [UNNotificationSound defaultSound];
#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000
switch (urgency) { switch (urgency) {
case 0: case 0:
@ -521,7 +521,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
typedef struct { typedef struct {
char *identifier, *title, *body, *appname, *image_path, *category_id; char *identifier, *title, *body, *appname, *image_path, *category_id;
int urgency; int urgency; bool muted;
} QueuedNotification; } QueuedNotification;
typedef struct { typedef struct {
@ -531,13 +531,13 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
static NotificationQueue notification_queue = {0}; static NotificationQueue notification_queue = {0};
static void static void
queue_notification(const char *appname, const char *identifier, const char *title, const char* body, const char *image_path, int urgency, const char *category_id) { queue_notification(const char *appname, const char *identifier, const char *title, const char* body, const char *image_path, int urgency, const char *category_id, bool muted) {
ensure_space_for((&notification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true); ensure_space_for((&notification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
QueuedNotification *n = notification_queue.notifications + notification_queue.count++; QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
#define d(x) n->x = (x && x[0]) ? strdup(x) : NULL; #define d(x) n->x = (x && x[0]) ? strdup(x) : NULL;
d(appname); d(identifier); d(title); d(body); d(image_path); d(category_id); d(appname); d(identifier); d(title); d(body); d(image_path); d(category_id);
#undef d #undef d
n->urgency = urgency; n->urgency = urgency; n->muted = muted;
} }
static void static void
@ -545,7 +545,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
if (granted) { if (granted) {
for (size_t i = 0; i < notification_queue.count; i++) { for (size_t i = 0; i < notification_queue.count; i++) {
QueuedNotification *n = notification_queue.notifications + i; QueuedNotification *n = notification_queue.notifications + i;
schedule_notification(n->appname, n->identifier, n->title, n->body, n->image_path, n->urgency, n->category_id); schedule_notification(n->appname, n->identifier, n->title, n->body, n->image_path, n->urgency, n->category_id, n->muted);
} }
} }
while(notification_queue.count) { while(notification_queue.count) {
@ -598,17 +598,17 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
static PyObject* static PyObject*
cocoa_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) { cocoa_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
const char *identifier = "", *title = "", *body = "", *appname = "", *image_path = ""; int urgency = 1; const char *identifier = "", *title = "", *body = "", *appname = "", *image_path = ""; int urgency = 1;
PyObject *category, *categories; PyObject *category, *categories; int muted = 0;
static const char* kwlist[] = {"appname", "identifier", "title", "body", "category", "categories", "image_path", "urgency", NULL}; static const char* kwlist[] = {"appname", "identifier", "title", "body", "category", "categories", "image_path", "urgency", "muted", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssOO!|si", (char**)kwlist, if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssOO!|sip", (char**)kwlist,
&appname, &identifier, &title, &body, &category, &PyTuple_Type, &categories, &image_path, &urgency)) return NULL; &appname, &identifier, &title, &body, &category, &PyTuple_Type, &categories, &image_path, &urgency, &muted)) return NULL;
UNUserNotificationCenter *center = get_notification_center_safely(); UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) Py_RETURN_NONE; if (!center) Py_RETURN_NONE;
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init]; if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
if (PyObject_IsTrue(categories)) if (!set_notification_categories(center, categories)) return NULL; if (PyObject_IsTrue(categories)) if (!set_notification_categories(center, categories)) return NULL;
RAII_PyObject(category_id, PyObject_GetAttrString(category, "id")); RAII_PyObject(category_id, PyObject_GetAttrString(category, "id"));
queue_notification(appname, identifier, title, body, image_path, urgency, PyUnicode_AsUTF8(category_id)); queue_notification(appname, identifier, title, body, image_path, urgency, PyUnicode_AsUTF8(category_id), muted);
// The badge permission needs to be requested as well, even though it is not used, // The badge permission needs to be requested as well, even though it is not used,
// otherwise macOS refuses to show the preference checkbox for enable/disable notification sound. // otherwise macOS refuses to show the preference checkbox for enable/disable notification sound.

2
kitty/glfw-wrapper.h generated
View file

@ -1055,7 +1055,7 @@ typedef struct GLFWLayerShellConfig {
typedef struct GLFWDBUSNotificationData { typedef struct GLFWDBUSNotificationData {
const char *app_name, *icon, *summary, *body, *category, **actions; size_t num_actions; const char *app_name, *icon, *summary, *body, *category, **actions; size_t num_actions;
int32_t timeout; uint8_t urgency; uint32_t replaces; int32_t timeout; uint8_t urgency; uint32_t replaces; int muted;
} GLFWDBUSNotificationData; } GLFWDBUSNotificationData;
/*! @brief The function pointer type for error callbacks. /*! @brief The function pointer type for error callbacks.

View file

@ -2086,10 +2086,10 @@ static PyObject*
dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) { dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
int timeout = -1, urgency = 1; unsigned int replaces = 0; int timeout = -1, urgency = 1; unsigned int replaces = 0;
GLFWDBUSNotificationData d = {0}; GLFWDBUSNotificationData d = {0};
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "actions", "timeout", "urgency", "replaces", "category", NULL}; static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "actions", "timeout", "urgency", "replaces", "category", "muted", NULL};
PyObject *actions = NULL; PyObject *actions = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssO!|iiIs", (char**)kwlist, if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssO!|iiIsp", (char**)kwlist,
&d.app_name, &d.icon, &d.summary, &d.body, &PyDict_Type, &actions, &timeout, &urgency, &replaces, &d.category)) return NULL; &d.app_name, &d.icon, &d.summary, &d.body, &PyDict_Type, &actions, &timeout, &urgency, &replaces, &d.category, &d.muted)) return NULL;
if (!glfwDBusUserNotify) { if (!glfwDBusUserNotify) {
PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?"); PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?");
return NULL; return NULL;

View file

@ -219,6 +219,7 @@ class NotificationCommand:
notification_types: tuple[str, ...] = () notification_types: tuple[str, ...] = ()
timeout: int = -2 timeout: int = -2
buttons: tuple[str, ...] = () buttons: tuple[str, ...] = ()
sound_name: str = ''
# event callbacks # event callbacks
on_activation: Optional[Callable[['NotificationCommand', int], None]] = None on_activation: Optional[Callable[['NotificationCommand', int], None]] = None
@ -323,6 +324,11 @@ def parse_metadata(self, metadata: str, prev: 'NotificationCommand') -> tuple[Pa
self.timeout = max(-1, int(v)) self.timeout = max(-1, int(v))
except Exception: except Exception:
self.log('Ignoring invalid timeout in notification: {v!r}') self.log('Ignoring invalid timeout in notification: {v!r}')
elif k == 's':
try:
self.sound_name = base64_decode(v).decode('utf-8')
except Exception:
self.log('Ignoring invalid sound name in notification: {v!r}')
if not prev.done and prev.identifier == self.identifier: if not prev.done and prev.identifier == self.identifier:
self.merge_metadata(prev) self.merge_metadata(prev)
return payload_type, payload_is_encoded return payload_type, payload_is_encoded
@ -347,6 +353,8 @@ def merge_metadata(self, prev: 'NotificationCommand') -> None:
self.notification_types = prev.notification_types + self.notification_types self.notification_types = prev.notification_types + self.notification_types
if prev.buttons: if prev.buttons:
self.buttons += prev.buttons self.buttons += prev.buttons
if not self.sound_name:
self.sound_name = prev.sound_name
if self.timeout < -1: if self.timeout < -1:
self.timeout = prev.timeout self.timeout = prev.timeout
self.icon_path = prev.icon_path self.icon_path = prev.icon_path
@ -411,6 +419,7 @@ def finalise(self) -> None:
self.urgency = Urgency.Normal if self.urgency is None else self.urgency self.urgency = Urgency.Normal if self.urgency is None else self.urgency
self.close_response_requested = bool(self.close_response_requested) self.close_response_requested = bool(self.close_response_requested)
self.timeout = max(-1, self.timeout) self.timeout = max(-1, self.timeout)
self.sound_name = self.sound_name or 'system'
def matches_rule_item(self, location:str, query:str) -> bool: def matches_rule_item(self, location:str, query:str) -> bool:
import re import re
@ -440,6 +449,8 @@ class DesktopIntegration:
supports_close_events: bool = True supports_close_events: bool = True
supports_body: bool = True supports_body: bool = True
supports_buttons: bool = True supports_buttons: bool = True
supports_sound: bool = True
supports_sound_names: str = 'xdg-names'
def __init__(self, notification_manager: 'NotificationManager'): def __init__(self, notification_manager: 'NotificationManager'):
self.notification_manager = notification_manager self.notification_manager = notification_manager
@ -475,7 +486,8 @@ def query_response(self, identifier: str) -> str:
i = f'i={identifier or "0"}:' i = f'i={identifier or "0"}:'
p = ','.join(x.value for x in PayloadType if x.value and self.payload_type_supported(x)) p = ','.join(x.value for x in PayloadType if x.value and self.payload_type_supported(x))
c = ':c=1' if self.supports_close_events else '' c = ':c=1' if self.supports_close_events else ''
return f'99;{i}p=?;a={actions}:o={when}:u={urgency}:p={p}{c}:w=1' s = 'silent' + (f',{self.supports_sound_names}' if self.supports_sound_names else '')
return f'99;{i}p=?;a={actions}:o={when}:u={urgency}:p={p}{c}:w=1:s={s}'
class MacOSNotificationCategory(NamedTuple): class MacOSNotificationCategory(NamedTuple):
@ -487,6 +499,7 @@ class MacOSNotificationCategory(NamedTuple):
class MacOSIntegration(DesktopIntegration): class MacOSIntegration(DesktopIntegration):
supports_close_events: bool = False supports_close_events: bool = False
supports_sound_names: str = ''
def initialize(self) -> None: def initialize(self) -> None:
from .fast_data_types import cocoa_set_notification_activated_callback from .fast_data_types import cocoa_set_notification_activated_callback
@ -583,6 +596,7 @@ def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Opti
cocoa_send_notification( cocoa_send_notification(
nc.application_name or 'kitty', str(desktop_notification_id), nc.title, body, nc.application_name or 'kitty', str(desktop_notification_id), nc.title, body,
category=category, categories=categories, image_path=image_path, urgency=nc.urgency.value, category=category, categories=categories, image_path=image_path, urgency=nc.urgency.value,
muted=nc.sound_name == 'silent',
) )
return desktop_notification_id return desktop_notification_id
@ -671,7 +685,13 @@ def created(self, dbus_notification_id: int, desktop_notification_id: int) -> No
if len(self.dbus_to_desktop) > 128: if len(self.dbus_to_desktop) > 128:
k, v = self.dbus_to_desktop.popitem(False) k, v = self.dbus_to_desktop.popitem(False)
self.desktop_to_dbus.pop(v, None) self.desktop_to_dbus.pop(v, None)
self.notification_manager.notification_created(dbus_notification_id) if n := self.notification_manager.notification_created(dbus_notification_id):
# self.supports_sound does not tell us if the notification server
# supports named sounds or not so we play the named sound
# ourselves and tell the server to mute any sound it might play.
if n.sound_name not in ('system', 'silent'):
from .fast_data_types import play_desktop_sound_async
play_desktop_sound_async(n.sound_name, event_id='desktop notification')
def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None: def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None:
if event_type == 'capabilities': if event_type == 'capabilities':
@ -679,6 +699,7 @@ def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int
self.supports_body = 'body' in capabilities self.supports_body = 'body' in capabilities
self.supports_buttons = 'actions' in capabilities self.supports_buttons = 'actions' in capabilities
self.supports_body_markup = 'body-markup' in capabilities self.supports_body_markup = 'body-markup' in capabilities
self.supports_sound = 'sound' in capabilities
if debug_desktop_integration: if debug_desktop_integration:
log_error('Got notification server capabilities:', capabilities) log_error('Got notification server capabilities:', capabilities)
return return
@ -727,7 +748,9 @@ def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Opti
actions[str(i+1)] = b actions[str(i+1)] = b
desktop_notification_id = dbus_send_notification( desktop_notification_id = dbus_send_notification(
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, actions=actions, app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, actions=actions,
timeout=nc.timeout, urgency=nc.urgency.value, replaces=replaces_dbus_id, category=(nc.notification_types or ('',))[0]) timeout=nc.timeout, urgency=nc.urgency.value, replaces=replaces_dbus_id,
category=(nc.notification_types or ('',))[0], muted=nc.sound_name == 'silent' or nc.sound_name != 'system',
)
if debug_desktop_integration: if debug_desktop_integration:
log_error(f'Requested creation of notification with {desktop_notification_id=}') log_error(f'Requested creation of notification with {desktop_notification_id=}')
if existing_desktop_notification_id and replaces_dbus_id: if existing_desktop_notification_id and replaces_dbus_id:
@ -830,11 +853,13 @@ def reset(self) -> None:
self.in_progress_notification_commands_by_client_id: dict[str, NotificationCommand] = {} self.in_progress_notification_commands_by_client_id: dict[str, NotificationCommand] = {}
self.pending_commands: dict[int, NotificationCommand] = {} self.pending_commands: dict[int, NotificationCommand] = {}
def notification_created(self, desktop_notification_id: int) -> None: def notification_created(self, desktop_notification_id: int) -> NotificationCommand | None:
if n := self.in_progress_notification_commands.get(desktop_notification_id): if n := self.in_progress_notification_commands.get(desktop_notification_id):
n.created_by_desktop = True n.created_by_desktop = True
if n.timeout > 0: if n.timeout > 0:
add_timer(partial(self.expire_notification, desktop_notification_id, id(n)), n.timeout / 1000, False) add_timer(partial(self.expire_notification, desktop_notification_id, id(n)), n.timeout / 1000, False)
return n
return None
def notification_activation_token_received(self, desktop_notification_id: int, token: str) -> None: def notification_activation_token_received(self, desktop_notification_id: int, token: str) -> None:
if n := self.in_progress_notification_commands.get(desktop_notification_id): if n := self.in_progress_notification_commands.get(desktop_notification_id):

View file

@ -14,11 +14,11 @@
def n( def n(
title='title', body='', urgency=Urgency.Normal, desktop_notification_id=1, icon_names=(), icon_path='', title='title', body='', urgency=Urgency.Normal, desktop_notification_id=1, icon_names=(), icon_path='',
application_name='', notification_types=(), timeout=-1, application_name='', notification_types=(), timeout=-1, sound='system',
): ):
return { return {
'title': title, 'body': body, 'urgency': urgency, 'id': desktop_notification_id, 'icon_names': icon_names, 'icon_path': icon_path, 'title': title, 'body': body, 'urgency': urgency, 'id': desktop_notification_id, 'icon_names': icon_names, 'icon_path': icon_path,
'application_name': application_name, 'notification_types': notification_types, 'timeout': timeout 'application_name': application_name, 'notification_types': notification_types, 'timeout': timeout, 'sound': sound,
} }
@ -53,7 +53,8 @@ def notify(self, cmd, existing_desktop_notification_id) -> int:
self.counter += 1 self.counter += 1
did = self.counter did = self.counter
title, body, urgency = cmd.title, cmd.body, (Urgency.Normal if cmd.urgency is None else cmd.urgency) title, body, urgency = cmd.title, cmd.body, (Urgency.Normal if cmd.urgency is None else cmd.urgency)
ans = n(title, body, urgency, did, cmd.icon_names, os.path.basename(cmd.icon_path), cmd.application_name, cmd.notification_types, timeout=cmd.timeout) ans = n(title, body, urgency, did, cmd.icon_names, os.path.basename(cmd.icon_path), cmd.application_name,
cmd.notification_types, timeout=cmd.timeout, sound=cmd.sound_name)
self.notifications.append(ans) self.notifications.append(ans)
return self.counter return self.counter
@ -235,10 +236,20 @@ def assert_events(focus=True, close=0, report='', close_response='', live=''):
assert_events() assert_events()
reset() reset()
# test sounds
def enc(x):
return standard_b64encode(x.encode()).decode()
h(f's={enc("silent")};title')
self.ae(di.notifications, [n(sound='silent')])
h(f's={enc("custom")};title')
self.ae(di.notifications[-1], n(desktop_notification_id=2, sound='custom'))
reset()
# Test querying # Test querying
h('i=xyz:p=?') h('i=xyz:p=?')
self.assertFalse(di.notifications) self.assertFalse(di.notifications)
qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon,alive,buttons:c=1:w=1' qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon,alive,buttons:c=1:w=1:s=silent,xdg-names'
self.ae(ch.responses, [f'99;i=xyz:p=?;{qr}']) self.ae(ch.responses, [f'99;i=xyz:p=?;{qr}'])
reset() reset()
h('p=?') h('p=?')