From 4e3c6e52aad41b9deb98d052c806f4a84846b782 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Aug 2020 20:09:48 +0530 Subject: [PATCH] macOS: Switch to the User Notifications framework 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. And notifications can only be displayed once the user grants permission. A completely brain-dead design. Complain to Apple. --- docs/changelog.rst | 6 ++ kitty/boss.py | 7 +- kitty/cocoa_window.m | 153 +++++++++++++++++++++++--------------- kitty/fast_data_types.pyi | 4 +- kitty/notify.py | 2 +- setup.py | 18 ++--- 6 files changed, 116 insertions(+), 74 deletions(-) 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)