diff --git a/kitty/boss.py b/kitty/boss.py index e43134bc3..767c07132 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -376,7 +376,7 @@ def __init__( self.mouse_handler: Optional[Callable[[WindowSystemMouseEvent], None]] = None set_boss(self) self.mappings: Mappings = Mappings(global_shortcuts, self.refresh_active_tab_bar) - self.notification_manager: NotificationManager = NotificationManager() + self.notification_manager: NotificationManager = NotificationManager(debug=self.args.debug_keyboard or self.args.debug_rendering) def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None: si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session) diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index d28e42555..cc67849b2 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -429,11 +429,15 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center withCompletionHandler:(void (^)(void))completionHandler { (void)(center); if (notification_activated_callback) { - NSString *identifier = [[[response notification] request] identifier]; - PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z", - identifier ? [identifier UTF8String] : NULL); - if (ret == NULL) PyErr_Print(); - else Py_DECREF(ret); + if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) { + NSString *identifier = [[[response notification] request] identifier]; + PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z", + identifier ? [identifier UTF8String] : NULL); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); + } else if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) { + NSLog(@"Notification was dismissed: %@", response.notification); + } } completionHandler(); } @@ -455,6 +459,14 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center return center; } +static bool +remove_delivered_notification(const char *identifier) { + UNUserNotificationCenter *center = get_notification_center_safely(); + if (!center) return false; + [center removeDeliveredNotificationsWithIdentifiers:@[ @(identifier) ]]; + return true; +} + static void schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle, int urgency) { UNUserNotificationCenter *center = get_notification_center_safely(); @@ -532,10 +544,17 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center } } +static PyObject* +cocoa_remove_delivered_notification(PyObject *self UNUSED, PyObject *x) { + if (!PyUnicode_Check(x)) { PyErr_SetString(PyExc_TypeError, "identifier must be a string"); return NULL; } + if (remove_delivered_notification(PyUnicode_AsUTF8(x))) { Py_RETURN_TRUE; } + Py_RETURN_FALSE; +} + static PyObject* cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { char *identifier = NULL, *title = NULL, *body = NULL, *subtitle = NULL; int urgency = 1; - if (!PyArg_ParseTuple(args, "zsz|zi", &identifier, &title, &body, &subtitle, &urgency)) return NULL; + if (!PyArg_ParseTuple(args, "ssz|zi", &identifier, &title, &body, &subtitle, &urgency)) return NULL; UNUserNotificationCenter *center = get_notification_center_safely(); if (!center) Py_RETURN_NONE; @@ -546,7 +565,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center // otherwise macOS refuses to show the preference checkbox for enable/disable notification sound. [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (error != nil) { + if (!granted && error != nil) { log_error("Failed to request permission for showing notification: %s", [[error localizedDescription] UTF8String]); } dispatch_async(dispatch_get_main_queue(), ^{ @@ -1048,6 +1067,7 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, {"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""}, {"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""}, + {"cocoa_remove_delivered_notification", (PyCFunction)cocoa_remove_delivered_notification, METH_O, ""}, {"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""}, {"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""}, {"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""}, diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index bb10d7884..afd7496a5 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -543,7 +543,7 @@ 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_set_notification_callback(c: Optional[Callable[[str, int, Union[str, int]], None]]) -> None: ... def dbus_send_notification( app_name: str, @@ -561,7 +561,7 @@ def dbus_close_notification(dbus_notification_id: int) -> bool: ... def cocoa_send_notification( - identifier: Optional[str], + identifier: str, title: str, body: Optional[str], subtitle: Optional[str], @@ -569,6 +569,7 @@ def cocoa_send_notification( ) -> None: pass +def cocoa_remove_delivered_notification(identifier: str) -> bool: ... def create_os_window( get_window_size: Callable[[int, int, int, int, float, float], Tuple[int, diff --git a/kitty/glfw.c b/kitty/glfw.c index a4c2c0b05..bcd4ce6fb 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1423,7 +1423,8 @@ 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_CLEAR(dbus_notification_callback); + if (callback && callback != Py_None) { dbus_notification_callback = callback; Py_INCREF(callback); } Py_RETURN_NONE; } diff --git a/kitty/notifications.py b/kitty/notifications.py index 4aa710ae1..726469de6 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -279,9 +279,16 @@ class MacOSIntegration(DesktopIntegration): def initialize(self) -> None: from .fast_data_types import cocoa_set_notification_activated_callback - self.id_counter = count() + self.id_counter = count(start=1) cocoa_set_notification_activated_callback(self.notification_activated) + def close_notification(self, desktop_notification_id: int) -> bool: + from .fast_data_types import cocoa_remove_delivered_notification + close_succeeded = cocoa_remove_delivered_notification(str(desktop_notification_id)) + if debug_desktop_integration: + log_error(f'Close request for {desktop_notification_id=} {"succeeded" if close_succeeded else "failed"}') + return close_succeeded + def notify(self, title: str, body: str, @@ -293,10 +300,15 @@ def notify(self, ) -> int: desktop_notification_id = next(self.id_counter) from .fast_data_types import cocoa_send_notification - cocoa_send_notification(str(desktop_notification_id), title, body, subtitle, urgency.value) + # If the body is not set macos makes the title the body and uses + # "kitty" as the title. So use a single space for the body in this + # case. + cocoa_send_notification(str(desktop_notification_id), title, body or ' ', subtitle, urgency.value) return desktop_notification_id def notification_activated(self, ident: str) -> None: + if debug_desktop_integration: + log_error(f'Notification {ident} activated') try: desktop_notification_id = int(ident) except Exception: @@ -434,7 +446,10 @@ def __init__( desktop_integration: Optional[DesktopIntegration] = None, channel: Channel = Channel(), log: Log = Log(), + debug: bool = False, ): + global debug_desktop_integration + debug_desktop_integration = debug if desktop_integration is None: self.desktop_integration = MacOSIntegration(self) if is_macos else FreeDesktopIntegration(self) else: