Skip to content

Commit

Permalink
Attempt to use the User Notifications API
Browse files Browse the repository at this point in the history
Apple are deprecating the usual Notifications API in favor of this
travesty, for Big Turd. It requires users to approve a local
notification from software they have explicitly installed!!

Apparently the macOS ecosystem is full of

a) developers that are stupid enough to spam their users with notifications
b) users that are stupid enough to not be able to block notifications
from applications they dont like, instead needing to grant permission
for a notification popup!!

Note that this commit is not functional, since as far as I can tell
on Catalina the permission request always fails with the very helpful
error message: "Notifications are not allowed for this application"
Presumably, the plumbing for allowing notifications will only be present
on Big Turd. And yes, I have tried it with a signed and notarized
(more Apple branded security theatre) application.

At this point I give up. I will simply implement #2391 someday and use
that to show notifications. So much for all the little control freaks
running around in the toilet bowl that looks like a spaceship.

I just wasted eight hours of my life on these fools.
  • Loading branch information
kovidgoyal committed Aug 19, 2020
1 parent 022e2eb commit 2fafdae
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 60 deletions.
148 changes: 96 additions & 52 deletions kitty/cocoa_window.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "state.h"
#include "monotonic.h"
#include <Cocoa/Cocoa.h>
#include <UserNotifications/UserNotifications.h>

#include <AvailabilityMacros.h>
// Needed for _NSGetProgname
Expand Down Expand Up @@ -133,73 +134,113 @@ + (GlobalMenuTarget *) shared_instance
}

static PyObject *notification_activated_callback = NULL;
static PyObject*
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
notification_activated_callback = callback;
Py_INCREF(callback);
Py_RETURN_NONE;
}

@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
@interface NotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@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);
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler {
(void)(center);
if (notification_activated_callback) {
UNNotificationContent *notification = [[[response notification] request] content];
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);
}
completionHandler();
}
@end


static UNMutableNotificationContent*
create_notification(const char *identifier, const char *title, const char *body) {
// Configure the notification's payload.
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
if (title) content.title = @(title);
if (body) content.body = @(body);
if (identifier) content.userInfo = @{@"user_id": @(identifier)};
content.sound = [UNNotificationSound defaultSound];
return content;
}

static void
schedule_notification(const char *identifier, const char *title, const char *body) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
if (!center) return;
UNMutableNotificationContent *content = create_notification(identifier, title, body);
// Deliver the notification in five seconds.
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
triggerWithTimeInterval:5 repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"Standard"
content:content trigger:trigger];

[center addNotificationRequest:request withCompletionHandler:NULL];
[trigger release];
[request release];
[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;
Py_INCREF(callback);
Py_RETURN_NONE;
}

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 + UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error) {
log_error("Failed to request permission for showing notification: %s: %s", [[error localizedDescription] UTF8String], [[error localizedFailureReason] 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;
}

Expand Down Expand Up @@ -497,8 +538,11 @@ - (void)openFilesFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {

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
}

Expand Down
4 changes: 1 addition & 3 deletions kitty/fast_data_types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion kitty/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def notify(
icon: bool = True,
identifier: Optional[str] = None
) -> None:
cocoa_send_notification(identifier, title, body, None)
cocoa_send_notification(identifier, title, body)

else:

Expand Down
11 changes: 7 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 2fafdae

Please sign in to comment.