diff --git a/docs/changelog.rst b/docs/changelog.rst index 47d2cc3c8..2dcf1a9c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,12 @@ To update |kitty|, :doc:`follow the instructions `. - Add support for displaying correct colors with non-sRGB PNG files (Adds a dependency on liblcms2) +- macOS: Switch to using the User Notifications framework for notifications. + The current notifications framework has been deprecated in Big Sur. The new + framework only allows notifications from signed and notarized applications, + so people using kitty from homebrew/source are out of luck. Complain to + Apple. + 0.18.3 [2020-08-11] ------------------- diff --git a/kitty/boss.py b/kitty/boss.py index a23ce8765..f47e942c2 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -67,6 +67,8 @@ def notification_activated(identifier: str) -> None: if identifier == 'new-version': from .update_check import notification_activated as do do() + elif identifier.startswith('test-notify-'): + log_error(f'Test notification {identifier} activated') def listen_on(spec: str) -> int: @@ -1539,4 +1541,7 @@ class Boss: def send_test_notification(self) -> None: from .notify import notify - notify('Test notification', 'Hello world') + from time import monotonic + now = monotonic() + ident = f'test-notify-{now}' + notify(f'Test {now}', f'At: {now}', identifier=ident) diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 4b2d7418a..e1566ee0c 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -9,6 +9,7 @@ #include "state.h" #include "monotonic.h" #include +#include #include // Needed for _NSGetProgname @@ -133,7 +134,79 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) { } static PyObject *notification_activated_callback = NULL; -static PyObject* + +@interface NotificationDelegate : NSObject +@end + +@implementation NotificationDelegate + - (void)userNotificationCenter:(UNUserNotificationCenter *)center + didReceiveNotificationResponse:(UNNotificationResponse *)response + 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); + } + completionHandler(); + } +@end + + +static void +schedule_notification(const char *identifier, const char *title, const char *body) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + if (!center) return; + // Configure the notification's payload. + UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; + if (title) content.title = @(title); + if (body) content.body = @(body); + // Deliver the notification + static unsigned long counter = 1; + UNNotificationRequest* request = [ + UNNotificationRequest requestWithIdentifier:(identifier ? @(identifier) : [NSString stringWithFormat:@"Id_%lu", counter++]) + content:content trigger:nil]; + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]); + } + }]; + [content release]; +} + + +typedef struct { + char *identifier, *title, *body; +} QueuedNotification; + +typedef struct { + QueuedNotification *notifications; + size_t count, capacity; +} NotificationQueue; +static NotificationQueue notification_queue = {0}; + +static void +queue_notification(const char *identifier, const char *title, const char* body) { + ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true); + QueuedNotification *n = notification_queue.notifications + notification_queue.count++; + n->identifier = identifier ? strdup(identifier) : NULL; + n->title = title ? strdup(title) : NULL; + n->body = body ? strdup(body) : NULL; +} + +static void +drain_pending_notifications(BOOL granted) { + while(notification_queue.count) { + QueuedNotification *n = notification_queue.notifications + --notification_queue.count; + if (granted) schedule_notification(n->identifier, n->title, n->body); + free(n->identifier); free(n->title); free(n->body); + n->identifier = NULL; n->title = NULL; n->body = NULL; + } +} + +PyObject* set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) { if (notification_activated_callback) Py_DECREF(notification_activated_callback); notification_activated_callback = callback; @@ -141,65 +214,26 @@ set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) { Py_RETURN_NONE; } -@interface NotificationDelegate : NSObject -@end - -@implementation NotificationDelegate - - (void)userNotificationCenter:(NSUserNotificationCenter *)center - didDeliverNotification:(NSUserNotification *)notification { - (void)(center); (void)(notification); - } - - - (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center - shouldPresentNotification:(NSUserNotification *)notification { - (void)(center); (void)(notification); - return YES; - } - - - (void) userNotificationCenter:(NSUserNotificationCenter *)center - didActivateNotification:(NSUserNotification *)notification { - (void)(center); (void)(notification); - if (notification_activated_callback) { - PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z", - notification.userInfo[@"user_id"] ? [notification.userInfo[@"user_id"] UTF8String] : NULL); - if (ret == NULL) PyErr_Print(); - else Py_DECREF(ret); - } - } -@end - static PyObject* cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { - char *identifier = NULL, *title = NULL, *subtitle = NULL, *informativeText = NULL, *path_to_image = NULL; - if (!PyArg_ParseTuple(args, "zssz|z", &identifier, &title, &informativeText, &path_to_image, &subtitle)) return NULL; - NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; - if (!center) {PyErr_SetString(PyExc_RuntimeError, "Failed to get the user notification center"); return NULL; } + char *identifier = NULL, *title = NULL, *body = NULL; + if (!PyArg_ParseTuple(args, "zsz", &identifier, &title, &body)) return NULL; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + if (!center) Py_RETURN_NONE; if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init]; - NSUserNotification *n = [NSUserNotification new]; - NSImage *img = nil; - if (path_to_image) { - NSString *p = @(path_to_image); - NSURL *url = [NSURL fileURLWithPath:p]; - img = [[NSImage alloc] initWithContentsOfURL:url]; - [url release]; [p release]; - if (img) { - [n setValue:img forKey:@"_identityImage"]; - [n setValue:@(false) forKey:@"_identityImageHasBorder"]; + queue_notification(identifier, title, body); + + [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert) + completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (error != nil) { + log_error("Failed to request permission for showing notification: %s", [[error localizedDescription] UTF8String]); + } + dispatch_async(dispatch_get_main_queue(), ^{ + drain_pending_notifications(granted); + }); } - [img release]; - } -#define SET(x) { \ - if (x) { \ - NSString *t = @(x); \ - n.x = t; \ - [t release]; \ - }} - SET(title); SET(subtitle); SET(informativeText); -#undef SET - if (identifier) { - n.userInfo = @{@"user_id": @(identifier)}; - } - [center deliverNotification:n]; + ]; Py_RETURN_NONE; } @@ -497,8 +531,11 @@ cleanup() { if (dockMenu) [dockMenu release]; dockMenu = nil; - if (notification_activated_callback) Py_DECREF(notification_activated_callback); - notification_activated_callback = NULL; + if (notification_activated_callback) Py_CLEAR(notification_activated_callback); + drain_pending_notifications(NO); + free(notification_queue.notifications); + notification_queue.notifications = NULL; + notification_queue.capacity = 0; } // autoreleasepool } diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index e69d7fa8a..783f8801e 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -519,9 +519,7 @@ def dbus_send_notification( def cocoa_send_notification( identifier: Optional[str], title: str, - informative_text: str, - path_to_img: Optional[str], - subtitle: Optional[str] = None + body: Optional[str], ) -> None: pass diff --git a/kitty/notify.py b/kitty/notify.py index ea4e3bcdd..704def436 100644 --- a/kitty/notify.py +++ b/kitty/notify.py @@ -17,7 +17,7 @@ if is_macos: icon: bool = True, identifier: Optional[str] = None ) -> None: - cocoa_send_notification(identifier, title, body, None) + cocoa_send_notification(identifier, title, body) else: diff --git a/setup.py b/setup.py index 2fbc4784e..667bcf0b0 100755 --- a/setup.py +++ b/setup.py @@ -331,20 +331,23 @@ def kitty_env() -> Env: cflags.extend(pkg_config('libpng', '--cflags-only-I')) cflags.extend(pkg_config('lcms2', '--cflags-only-I')) if is_macos: - font_libs = ['-framework', 'CoreText', '-framework', 'CoreGraphics'] + platform_libs = [ + '-framework', 'CoreText', '-framework', 'CoreGraphics', + '-framework', 'UserNotifications' + ] # Apple deprecated OpenGL in Mojave (10.14) silence the endless # warnings about it cppflags.append('-DGL_SILENCE_DEPRECATION') else: cflags.extend(pkg_config('fontconfig', '--cflags-only-I')) - font_libs = pkg_config('fontconfig', '--libs') + platform_libs = pkg_config('fontconfig', '--libs') cflags.extend(pkg_config('harfbuzz', '--cflags-only-I')) - font_libs.extend(pkg_config('harfbuzz', '--libs')) + platform_libs.extend(pkg_config('harfbuzz', '--libs')) pylib = get_python_flags(cflags) gl_libs = ['-framework', 'OpenGL'] if is_macos else pkg_config('gl', '--libs') libpng = pkg_config('libpng', '--libs') lcms2 = pkg_config('lcms2', '--libs') - ans.ldpaths += pylib + font_libs + gl_libs + libpng + lcms2 + ans.ldpaths += pylib + platform_libs + gl_libs + libpng + lcms2 if is_macos: ans.ldpaths.extend('-framework Cocoa'.split()) elif not is_openbsd: @@ -409,10 +412,6 @@ SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[Env, str], Unio 'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']), 'kitty/data-types.c': ('kitty/data-types.c', get_vcs_rev_defines), } -NO_WERROR_SOURCES = { - # because of deprecation of Notifications API, see https://github.com/kovidgoyal/kitty/pull/2876 - 'kitty/cocoa_window.m', -} def newer(dest: str, *sources: str) -> bool: @@ -603,9 +602,6 @@ def compile_c_extension( cmd = [kenv.cc, '-MMD'] + cppflags + kenv.cflags cmd += ['-c', src] + ['-o', dest] - if src in NO_WERROR_SOURCES: - if '-Werror' in cmd: - cmd.remove('-Werror') key = CompileKey(original_src, os.path.basename(dest)) desc = 'Compiling {} ...'.format(emphasis(desc_prefix + src)) compilation_database.add_command(desc, cmd, partial(newer, dest, *dependecies_for(src, dest, headers)), key=key, keyfile=src)