diff --git a/docs/desktop-notifications.rst b/docs/desktop-notifications.rst index 70e939270..5da9f77f6 100644 --- a/docs/desktop-notifications.rst +++ b/docs/desktop-notifications.rst @@ -370,7 +370,7 @@ Key Value ``p`` Comma spearated list of supported payload types (i.e. values of the ``p`` key that the terminal implements). These must contain at least - ``title`` and ``body``. + ``title``. ``u`` Comma separated list of urgency values that the terminal implements. If urgency is not supported, the ``u`` key must be absent from the diff --git a/glfw/linux_notify.c b/glfw/linux_notify.c index 88db41ed3..583fba8e5 100644 --- a/glfw/linux_notify.c +++ b/glfw/linux_notify.c @@ -92,11 +92,43 @@ cancel_user_notification(DBusConnection *session_bus, uint32_t *id) { return glfw_dbus_call_method_no_reply(session_bus, NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "CloseNotification", DBUS_TYPE_UINT32, id, DBUS_TYPE_INVALID); } +static void +got_capabilities(DBusMessage *msg, const char* err, void* data UNUSED) { + if (err) { + _glfwInputError(GLFW_PLATFORM_ERROR, "Notify: Failed to get server capabilities error: %s", err); + return; + } +#define check_call(func, err, ...) if (!func(__VA_ARGS__)) { _glfwInputError(GLFW_PLATFORM_ERROR, "Notify: GetCapabilities: %s", err); return; } + DBusMessageIter iter, array_iter; + check_call(dbus_message_iter_init, "message has no parameters", msg, &iter); + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY || dbus_message_iter_get_element_type(&iter) != DBUS_TYPE_STRING) { + _glfwInputError(GLFW_PLATFORM_ERROR, "Notify: GetCapabilities: %s", "reply is not an array of strings"); + return; + } + dbus_message_iter_recurse(&iter, &array_iter); + char buf[2048] = {0}, *p = buf; + while (dbus_message_iter_get_arg_type(&array_iter) == DBUS_TYPE_STRING) { + const char *str; + dbus_message_iter_get_basic(&array_iter, &str); + if (sizeof(buf) > (size_t)(p-buf) + 1u) p += snprintf(p, sizeof(buf) - (p-buf) - 1, "%s\n", str); + dbus_message_iter_next(&array_iter); + } + if (activated_handler) activated_handler(0, -1, buf); +#undef check_call + +} + +static bool +get_capabilities(DBusConnection *session_bus) { + return glfw_dbus_call_method_with_reply(session_bus, NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "GetCapabilities", 60, got_capabilities, NULL, DBUS_TYPE_INVALID); +} + notification_id_type glfw_dbus_send_user_notification(const GLFWDBUSNotificationData *n, GLFWDBusnotificationcreatedfun callback, void *user_data) { DBusConnection *session_bus = glfw_dbus_session_bus(); if (!session_bus) return 0; if (n->timeout == -9999 && n->urgency == 255) return cancel_user_notification(session_bus, user_data) ? 1 : 0; + if (n->timeout == -99999 && n->urgency == 255) return get_capabilities(session_bus) ? 1 : 0; static DBusConnection *added_signal_match = NULL; if (added_signal_match != session_bus) { dbus_bus_add_match(session_bus, "type='signal',interface='" NOTIFICATIONS_IFACE "',member='ActionInvoked'", NULL); diff --git a/kitty/glfw.c b/kitty/glfw.c index ca14ddc7c..f44f7fc86 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1407,7 +1407,15 @@ static PyObject *dbus_notification_callback = NULL; static PyObject* dbus_set_notification_callback(PyObject *self UNUSED, PyObject *callback) { Py_CLEAR(dbus_notification_callback); - if (callback && callback != Py_None) { dbus_notification_callback = callback; Py_INCREF(callback); } + if (callback && callback != Py_None) { + dbus_notification_callback = callback; Py_INCREF(callback); + GLFWDBUSNotificationData d = {.timeout=-99999, .urgency=255}; + if (!glfwDBusUserNotify) { + PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?"); + return NULL; + } + glfwDBusUserNotify(&d, NULL, NULL); + } Py_RETURN_NONE; } @@ -1428,6 +1436,7 @@ dbus_user_notification_activated(uint32_t notification_id, int type, const char* switch (type) { case 0: stype = "closed"; break; case 1: stype = "activation_token"; break; + case -1: stype = "capabilities"; break; } send_dbus_notification_event_to_python(stype, nid, action); } diff --git a/kitty/notifications.py b/kitty/notifications.py index f338e7075..613ce9728 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -438,6 +438,8 @@ def get_matches(location: str, query: str, candidates: set['NotificationCommand' class DesktopIntegration: supports_close_events: bool = True + supports_body: bool = True + supports_buttons: bool = True def __init__(self, notification_manager: 'NotificationManager'): self.notification_manager = notification_manager @@ -459,12 +461,19 @@ def on_new_version_notification_activation(self, cmd: NotificationCommand, which from .update_check import notification_activated notification_activated() + def payload_type_supported(self, x: PayloadType) -> bool: + if x is PayloadType.body and not self.supports_body: + return False + if x is PayloadType.buttons and not self.supports_buttons: + return False + return True + def query_response(self, identifier: str) -> str: actions = ','.join(x.value for x in Action) when = ','.join(x.value for x in OnlyWhen if x.value) urgency = ','.join(str(x.value) for x in Urgency) i = f'i={identifier or "0"}:' - p = ','.join(x.value for x in PayloadType if x.value) + 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 '' return f'99;{i}p=?;a={actions}:o={when}:u={urgency}:p={p}{c}:w=1' @@ -620,6 +629,8 @@ def notification_activated(self, event: str, ident: str, button_id: str) -> None class FreeDesktopIntegration(DesktopIntegration): + supports_body_markup: bool = True + def initialize(self) -> None: from .fast_data_types import dbus_set_notification_callback dbus_set_notification_callback(self.dispatch_event_from_desktop) @@ -663,6 +674,14 @@ def created(self, dbus_notification_id: int, desktop_notification_id: int) -> No self.notification_manager.notification_created(dbus_notification_id) def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None: + if event_type == 'capabilities': + capabilities = frozenset(str(extra).splitlines()) + self.supports_body = 'body' in capabilities + self.supports_buttons = 'actions' in capabilities + self.supports_body_markup = 'body-markup' in capabilities + if debug_desktop_integration: + log_error('Got notification server capabilities:', capabilities) + return if debug_desktop_integration: log_error(f'Got notification event from desktop: {event_type=} {dbus_notification_id=} {extra=}') if event_type == 'created': @@ -696,7 +715,9 @@ def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Opti if not app_icon: app_icon = get_custom_window_icon()[1] or logo_png_file - body = nc.body.replace('<', '<\u200c').replace('&', '&\u200c') # prevent HTML markup from being recognized + body = nc.body + if self.supports_body_markup: + body = body.replace('<', '<\u200c').replace('&', '&\u200c') # prevent HTML markup from being recognized assert nc.urgency is not None replaces_dbus_id = 0 if existing_desktop_notification_id: