From 7e610eb64559d2f8ec970cf1d592302e94d9513c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 25 Jul 2024 12:22:58 +0530 Subject: [PATCH] Get closing of notifications working on Linux Also implement support for the ActivationToken signal so that OS Windows are focused on Wayland when clicking on a notification. --- glfw/linux_notify.c | 9 +++--- kitty/boss.py | 3 -- kitty/fast_data_types.pyi | 2 ++ kitty/glfw.c | 24 +++++++++++++-- kitty/notifications.py | 62 ++++++++++++++++++++++++++------------- 5 files changed, 71 insertions(+), 29 deletions(-) diff --git a/glfw/linux_notify.c b/glfw/linux_notify.c index 80319aeaa..d599f3683 100644 --- a/glfw/linux_notify.c +++ b/glfw/linux_notify.c @@ -16,8 +16,6 @@ static inline void cleanup_free(void *p) { free(*(void**)p); } #define RAII_ALLOC(type, name, initializer) __attribute__((cleanup(cleanup_free))) type *name = initializer -static notification_id_type notification_id = 0; - typedef struct { notification_id_type next_id; GLFWDBusnotificationcreatedfun callback; @@ -41,8 +39,10 @@ notification_created(DBusMessage *msg, const char* errmsg, void *data) { uint32_t id; if (!glfw_dbus_get_args(msg, "Failed to get Notification uid", DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID)) return; NotificationCreatedData *ncd = (NotificationCreatedData*)data; - if (ncd->callback) ncd->callback(ncd->next_id, id, ncd->data); - if (data) free(data); + if (ncd) { + if (ncd->callback) ncd->callback(ncd->next_id, id, ncd->data); + free(ncd); + } } static DBusHandlerResult @@ -107,6 +107,7 @@ glfw_dbus_send_user_notification(const char *app_name, const char* icon, const c } RAII_ALLOC(NotificationCreatedData, data, malloc(sizeof(NotificationCreatedData))); if (!data) return 0; + static notification_id_type notification_id = 0; data->next_id = ++notification_id; data->callback = callback; data->data = user_data; if (!data->next_id) data->next_id = ++notification_id; diff --git a/kitty/boss.py b/kitty/boss.py index acf6b04a3..e43134bc3 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2750,9 +2750,6 @@ def on_monitored_pid_death(self, pid: int, exit_status: int) -> None: except Exception as e: log_error(f'Failed to process update check data {raw!r}, with error: {e}') - def dbus_notification_callback(self, event_type: str, a: int, b: Union[int, str]) -> None: - self.notification_manager.desktop_integration.dispatch_event_from_desktop(event_type, a, b) - def show_bad_config_lines(self, bad_lines: Iterable[BadLine], misc_errors: Iterable[str] = ()) -> None: def format_bad_line(bad_line: BadLine) -> str: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 8c1df5f2d..bb10d7884 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -543,6 +543,8 @@ def os_window_has_background_image(os_window_id: int) -> bool: pass +def dbus_set_notification_callback(c: Callable[[str, int, Union[str, int]], None]) -> None: ... + def dbus_send_notification( app_name: str, icon: str, diff --git a/kitty/glfw.c b/kitty/glfw.c index f9b5f702a..a4c2c0b05 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1419,6 +1419,24 @@ error_callback(int error, const char* description) { #ifndef __APPLE__ +static PyObject *dbus_notification_callback = NULL; + +static PyObject* +dbus_set_notification_callback(PyObject *self UNUSED, PyObject *callback) { + Py_CLEAR(dbus_notification_callback); dbus_notification_callback = callback; Py_INCREF(callback); + Py_RETURN_NONE; +} + +#define send_dbus_notification_event_to_python(event_type, a, b) { \ + if (dbus_notification_callback) { \ + const char call_args_fmt[] = {'s', \ + _Generic((a), unsigned long : 'k', unsigned long long : 'K'), _Generic((b), unsigned long : 'k', const char* : 's') }; \ + RAII_PyObject(ret, PyObject_CallFunction(dbus_notification_callback, call_args_fmt, event_type, a, b)); \ + if (!ret) PyErr_Print(); \ + } \ +} + + static void dbus_user_notification_activated(uint32_t notification_id, int type, const char* action) { unsigned long nid = notification_id; @@ -1427,7 +1445,7 @@ dbus_user_notification_activated(uint32_t notification_id, int type, const char* case 0: stype = "closed"; break; case 1: stype = "activation_token"; break; } - call_boss(dbus_notification_callback, "sks", stype, nid, action); + send_dbus_notification_event_to_python(stype, nid, action); } #endif @@ -2069,7 +2087,7 @@ request_frame_render(OSWindow *w) { void dbus_notification_created_callback(unsigned long long notification_id, uint32_t new_notification_id, void* data UNUSED) { unsigned long new_id = new_notification_id; - call_boss(dbus_notification_callback, "sKk", "created", notification_id, new_id); + send_dbus_notification_event_to_python("created", notification_id, new_id); } static PyObject* @@ -2263,6 +2281,7 @@ static PyMethodDef module_methods[] = { #ifndef __APPLE__ METHODB(dbus_send_notification, METH_VARARGS), METHODB(dbus_close_notification, METH_VARARGS), + METHODB(dbus_set_notification_callback, METH_O), #else {"cocoa_recreate_global_menu", (PyCFunction)py_recreate_global_menu, METH_NOARGS, ""}, {"cocoa_clear_global_shortcuts", (PyCFunction)py_clear_global_shortcuts, METH_NOARGS, ""}, @@ -2286,6 +2305,7 @@ void cleanup_glfw(void) { logo.pixels = NULL; Py_CLEAR(edge_spacing_func); #ifndef __APPLE__ + Py_CLEAR(dbus_notification_callback); release_freetype_render_context(csd_title_render_ctx); #endif } diff --git a/kitty/notifications.py b/kitty/notifications.py index d23b4c210..4aa710ae1 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -256,9 +256,6 @@ def __init__(self, notification_manager: 'NotificationManager'): def initialize(self) -> None: pass - def dispatch_event_from_desktop(self, *a: Any) -> None: - raise NotImplementedError('Implement me in subclass') - def close_notification(self, desktop_notification_id: int) -> bool: raise NotImplementedError('Implement me in subclass') @@ -310,27 +307,53 @@ def notification_activated(self, ident: str) -> None: class FreeDesktopIntegration(DesktopIntegration): + def initialize(self) -> None: + from .fast_data_types import dbus_set_notification_callback + dbus_set_notification_callback(self.dispatch_event_from_desktop) + # map the id returned by the notification daemon to the + # desktop_notification_id we use for the notification + self.creation_id_map: 'OrderedDict[int, int]' = OrderedDict() + def close_notification(self, desktop_notification_id: int) -> bool: from .fast_data_types import dbus_close_notification - close_succeeded = dbus_close_notification(desktop_notification_id) - if debug_desktop_integration: - log_error(f'Close request for {desktop_notification_id=} {"succeeded" if close_succeeded else "failed"}') + close_succeeded = False + if dbus_id := self.get_dbus_notification_id(desktop_notification_id, 'close_request'): + close_succeeded = dbus_close_notification(dbus_id) + if debug_desktop_integration: + log_error(f'Close request for {desktop_notification_id=} {"succeeded" if close_succeeded else "failed"}') return close_succeeded - def dispatch_event_from_desktop(self, *args: Any) -> None: - event_type: str = args[0] - dbus_notification_id: int = args[1] + def get_desktop_notification_id(self, dbus_notification_id: int, event: str) -> Optional[int]: + q = self.creation_id_map.get(dbus_notification_id) + if q is None: + if debug_desktop_integration: + log_error(f'Could not find desktop_notification_id for {dbus_notification_id=} for event {event}') + return q + + def get_dbus_notification_id(self, desktop_notification_id: int, event: str) ->Optional[int]: + for dbus_id, q in self.creation_id_map.items(): + if q == desktop_notification_id: + return dbus_id if debug_desktop_integration: - log_error(f'Got notification event from desktop: {args=}') + log_error(f'Could not find dbus_notification_id for {desktop_notification_id=} for event {event}') + return None + + def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None: + if debug_desktop_integration: + log_error(f'Got notification event from desktop: {event_type=} {dbus_notification_id=} {extra=}') if event_type == 'created': + self.creation_id_map[int(extra)] = dbus_notification_id + if len(self.creation_id_map) > 128: + self.creation_id_map.popitem(False) self.notification_manager.notification_created(dbus_notification_id) - elif event_type == 'activation_token': - token: str = args[2] - self.notification_manager.notification_activation_token_received(dbus_notification_id, token) - elif event_type == 'activated': - self.notification_manager.notification_activated(dbus_notification_id) - elif event_type == 'closed': - self.notification_manager.notification_closed(dbus_notification_id) + return + if desktop_notification_id := self.get_desktop_notification_id(dbus_notification_id, event_type): + if event_type == 'activation_token': + self.notification_manager.notification_activation_token_received(desktop_notification_id, str(extra)) + elif event_type == 'activated': + self.notification_manager.notification_activated(desktop_notification_id) + elif event_type == 'closed': + self.notification_manager.notification_closed(desktop_notification_id) def notify(self, title: str, @@ -381,6 +404,8 @@ def send(self, channel_id: int, osc_escape_code: str) -> bool: return False def focus(self, channel_id: int, activation_token: str) -> None: + if debug_desktop_integration: + log_error(f'Focusing window: {channel_id} with activation_token: {activation_token}') boss = get_boss() if w := self.window_for_id(channel_id): boss.set_active_window(w, switch_os_window_if_needed=True, activation_token=activation_token) @@ -423,9 +448,6 @@ def reset(self) -> None: self.in_progress_notification_commands_by_client_id: Dict[str, NotificationCommand] = {} self.pending_commands: Dict[int, NotificationCommand] = {} - def dispatch_event_from_desktop(self, *args: Any) -> None: - self.desktop_integration.dispatch_event_from_desktop(*args) - def notification_created(self, desktop_notification_id: int) -> None: if n := self.in_progress_notification_commands.get(desktop_notification_id): n.created_by_desktop = True