diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index ae9b13ecc..8aaf8eb73 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -453,7 +453,7 @@ live_delivered_notifications(void) { } static void -schedule_notification(const char *appname, const char *identifier, const char *title, const char *body, int urgency) { +schedule_notification(const char *appname, const char *identifier, const char *title, const char *body, const char *image_path, int urgency) { UNUserNotificationCenter *center = get_notification_center_safely(); if (!center) return; // Configure the notification's payload. @@ -478,6 +478,18 @@ schedule_notification(const char *appname, const char *identifier, const char *t [content setValue:@(level) forKey:@"interruptionLevel"]; } #endif + if (image_path) { + @try { + NSError *error; + NSURL *image_url = [NSURL fileURLWithFileSystemRepresentation:image_path isDirectory:NO relativeToURL:nil]; // autoreleased + UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:image_url options:nil error:&error]; // autoreleased + if (attachment) { content.attachments = @[ attachment ]; } + else NSLog(@"Error attaching image %@ to notification: %@", @(image_path), error.localizedDescription); + } @catch(NSException *exc) { + NSLog(@"Creating image attachment %@ for notification failed with error: %@", @(image_path), exc.reason); + } + } + // Deliver the notification static unsigned long counter = 1; UNNotificationRequest* request = [ @@ -497,7 +509,7 @@ schedule_notification(const char *appname, const char *identifier, const char *t typedef struct { - char *identifier, *title, *body, *appname; + char *identifier, *title, *body, *appname, *image_path; int urgency; } QueuedNotification; @@ -508,13 +520,12 @@ typedef struct { static NotificationQueue notification_queue = {0}; static void -queue_notification(const char *appname, const char *identifier, const char *title, const char* body, int urgency) { +queue_notification(const char *appname, const char *identifier, const char *title, const char* body, const char *image_path, int urgency) { ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true); QueuedNotification *n = notification_queue.notifications + notification_queue.count++; - n->appname = appname ? strdup(appname) : NULL; - n->identifier = identifier ? strdup(identifier) : NULL; - n->title = title ? strdup(title) : NULL; - n->body = body ? strdup(body) : NULL; +#define d(x) n->x = (x && x[0]) ? strdup(x) : NULL; + d(appname); d(identifier); d(title); d(body); d(image_path); +#undef d n->urgency = urgency; } @@ -523,13 +534,13 @@ drain_pending_notifications(BOOL granted) { if (granted) { for (size_t i = 0; i < notification_queue.count; i++) { QueuedNotification *n = notification_queue.notifications + i; - schedule_notification(n->appname, n->identifier, n->title, n->body, n->urgency); + schedule_notification(n->appname, n->identifier, n->title, n->body, n->image_path, n->urgency); } } while(notification_queue.count) { QueuedNotification *n = notification_queue.notifications + --notification_queue.count; if (!granted) do_notification_callback(@(n->identifier), "creation_failed"); - free(n->identifier); free(n->title); free(n->body); free(n->appname); + free(n->identifier); free(n->title); free(n->body); free(n->appname); free(n->image_path); memset(n, 0, sizeof(QueuedNotification)); } } @@ -551,14 +562,14 @@ cocoa_live_delivered_notifications(PyObject *self UNUSED, PyObject *x UNUSED) { static PyObject* cocoa_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) { - const char *identifier = "", *title = "", *body = "", *appname = ""; int urgency = 1; - static const char* kwlist[] = {"appname", "identifier", "title", "body", "urgency", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|i", (char**)kwlist, &appname, &identifier, &title, &body, &urgency)) return NULL; + const char *identifier = "", *title = "", *body = "", *appname = "", *image_path = ""; int urgency = 1; + static const char* kwlist[] = {"appname", "identifier", "title", "body", "image_path", "urgency", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|si", (char**)kwlist, &appname, &identifier, &title, &body, &image_path, &urgency)) return NULL; UNUserNotificationCenter *center = get_notification_center_safely(); if (!center) Py_RETURN_NONE; if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init]; - queue_notification(appname, identifier, title, body, urgency); + queue_notification(appname, identifier, title, body, image_path, urgency); // 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. diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 66d9ce529..bdb990b6f 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -566,6 +566,7 @@ def cocoa_send_notification( identifier: str, title: str, body: str, + image_path: str = "", urgency: int = 1, ) -> None: pass diff --git a/kitty/notifications.py b/kitty/notifications.py index 4c3371798..4c9f9f3d8 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -20,6 +20,16 @@ from .utils import get_custom_window_icon, log_error, sanitize_control_codes debug_desktop_integration = False +def image_type(data: bytes) -> str: + if data[:8] == b"\211PNG\r\n\032\n": + return 'png' + if data[:6] in (b'GIF87a', b'GIF89a'): + return 'gif' + if data[:2] == b'\xff\xd8': + return 'jpeg' + return 'unknown' + + class IconDataCache: @@ -52,7 +62,7 @@ class IconDataCache: def hash(self, data: bytes) -> str: from kittens.transfer.rsync import xxh128_hash_with_seed d = xxh128_hash_with_seed(data, self.seed) - return d.hex() + return d.hex() + '.' + image_type(data) def add_icon(self, key: str, data: bytes) -> str: self._ensure_state() @@ -482,12 +492,14 @@ class MacOSIntegration(DesktopIntegration): # 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. Although https://developer.apple.com/documentation/usernotifications/unnotificationcontent/body?language=objc - # says printf style strings are stripped this does not actually happen, - # so dont double % - # for %% escaping. + # says printf style strings are stripped this does not actually happen, so dont double % for %% escaping. body = (nc.body or ' ') assert nc.urgency is not None - cocoa_send_notification(nc.application_name or 'kitty', str(desktop_notification_id), nc.title, body, nc.urgency.value) + image_path = nc.icon_path + cocoa_send_notification( + nc.application_name or 'kitty', str(desktop_notification_id), nc.title, body, + image_path=image_path, urgency=nc.urgency.value, + ) return desktop_notification_id def notification_activated(self, event: str, ident: str) -> None: