Add support for controlling the sound played with notifications
This commit is contained in:
parent
5e809d2767
commit
bd8162fa42
11 changed files with 104 additions and 24 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
2
glfw/glfw3.h
vendored
|
|
@ -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
1
glfw/linux_notify.c
vendored
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
ensure_space_for((¬ification_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
2
kitty/glfw-wrapper.h
generated
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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=?')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue