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.
This commit is contained in:
Kovid Goyal 2020-08-19 20:09:48 +05:30
parent ae5ceedfe9
commit 4e3c6e52aa
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 116 additions and 74 deletions

View File

@ -10,6 +10,12 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- 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]
-------------------

View File

@ -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)

View File

@ -9,6 +9,7 @@
#include "state.h"
#include "monotonic.h"
#include <Cocoa/Cocoa.h>
#include <UserNotifications/UserNotifications.h>
#include <AvailabilityMacros.h>
// 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 <UNUserNotificationCenterDelegate>
@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((&notification_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 <NSUserNotificationCenterDelegate>
@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
}

View File

@ -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

View File

@ -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:

View File

@ -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)