Implement polling based closed notifications on macOS

This commit is contained in:
Kovid Goyal
2024-07-27 20:58:55 +05:30
parent 706cf1cd24
commit 54b328710b
5 changed files with 119 additions and 24 deletions

View File

@@ -116,11 +116,12 @@ If no notification id was specified ``i=0`` will be used.
If ``a=report`` is specified and the notification is activated/clicked on
then both the activation report and close notification are sent.
.. note::
Close events are best effort, some platforms such as macOS do not have
events when notifications are closed. Applications can use the
:ref:`notifications_query` to check if close events are supported
by the current terminal emulator.
.. note:: On macOS the OS does not supply notification
closed events to applications. As such close events must be implemented
via polling. It is up to the terminal emulator to decide a reasonable
time limit for how long to poll, before giving up. kitty polls for 60
seconds. Therefore, terminal applications should not rely on close events
being authoritative.
Closing an existing notification
@@ -205,7 +206,8 @@ Key Value Default Description
``-``
``d`` ``0`` or ``1`` ``1`` Indicates if the notification is
complete or not.
complete or not. A non-zero value
means it is complete.
``e`` ``0`` or ``1`` ``0`` If set to ``1`` means the payload is :rfc:`base64 <4648>` encoded UTF-8,
otherwise it is plain UTF-8 text with no C0 control codes in it

View File

@@ -1145,7 +1145,7 @@ static bool has_cocoa_pending_actions = false;
typedef struct cocoa_list { char **items; size_t count, capacity; } cocoa_list;
typedef struct {
char* wd;
cocoa_list open_urls, delivered_notifications, live_notifications;
cocoa_list open_urls, closed_notifications;
} CocoaPendingActionsData;
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
@@ -1165,18 +1165,21 @@ static void
cocoa_free_actions_data(void) {
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
cocoa_free_pending_list(&cocoa_pending_actions_data.open_urls);
cocoa_free_pending_list(&cocoa_pending_actions_data.delivered_notifications);
cocoa_free_pending_list(&cocoa_pending_actions_data.live_notifications);
cocoa_free_pending_list(&cocoa_pending_actions_data.closed_notifications);
}
void
set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
if (data) {
if (action == LAUNCH_URLS) {
cocoa_append_to_pending_list(&cocoa_pending_actions_data.open_urls, data);
} else {
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = strdup(data);
switch(action) {
case LAUNCH_URLS:
cocoa_append_to_pending_list(&cocoa_pending_actions_data.open_urls, data); break;
case COCOA_NOTIFICATION_CLOSED:
cocoa_append_to_pending_list(&cocoa_pending_actions_data.closed_notifications, data); break;
default:
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = strdup(data);
break;
}
}
cocoa_pending_actions[action] = true;
@@ -1223,6 +1226,14 @@ process_cocoa_pending_actions(void) {
}
}
cocoa_pending_actions_data.open_urls.count = 0;
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.closed_notifications.count; cpa++) {
if (cocoa_pending_actions_data.closed_notifications.items[cpa]) {
cocoa_report_closed_notification(cocoa_pending_actions_data.closed_notifications.items[cpa]);
free(cocoa_pending_actions_data.closed_notifications.items[cpa]);
cocoa_pending_actions_data.closed_notifications.items[cpa] = NULL;
}
}
cocoa_pending_actions_data.closed_notifications.count = 0;
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
has_cocoa_pending_actions = false;

View File

@@ -34,6 +34,7 @@ typedef enum {
MINIMIZE,
QUIT,
USER_MENU_ACTION,
COCOA_NOTIFICATION_CLOSED,
NUM_COCOA_PENDING_ACTIONS
} CocoaPendingAction;
@@ -58,3 +59,4 @@ bool cocoa_render_line_of_text(const char *text, const color_type fg, const colo
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
void get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*);
void set_cocoa_pending_action(CocoaPendingAction action, const char*);
void cocoa_report_closed_notification(const char* ident);

View File

@@ -354,10 +354,7 @@ static PyObject *notification_activated_callback = NULL;
static PyObject*
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
Py_CLEAR(notification_activated_callback);
if (callback != Py_None) {
notification_activated_callback = callback;
Py_INCREF(callback);
}
if (callback != Py_None) notification_activated_callback = Py_NewRef(callback);
Py_RETURN_NONE;
}
@@ -468,6 +465,89 @@ remove_delivered_notification(const char *identifier) {
return true;
}
static NSLock *notifications_polling_lock = NULL;
static bool polling_notifications = false;
typedef struct tn { char *ident; bool closed; monotonic_t creation_time; } tn;
static struct { tn *items; size_t count, capacity; monotonic_t creation_time; } tracked_notifications;
static void dispatch_closed_notifications(void);
#define CLOSE_POLL_TIME 60.
#define CLOSE_POLL_INTERVAL 750
static bool
ident_in_list_of_notifications(NSString *ident, NSArray<UNNotification*> *list) {
for (UNNotification *n in list) {
if ([[[n request] identifier] isEqualToString:ident]) return true;
}
return false;
}
static void
poll_for_closed_notifications(void) {
UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) return;
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * notifications) {
// NSLog(@"num of delivered but not closed nots: %lu", (unsigned long)[notifications count]);
[notifications_polling_lock lock];
for (size_t i = 0; i < tracked_notifications.count; i++) {
if (!ident_in_list_of_notifications(@(tracked_notifications.items[i].ident), notifications)) tracked_notifications.items[i].closed = true;
}
[notifications_polling_lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_closed_notifications();
});
}];
}
void
cocoa_report_closed_notification(const char* ident) {
do_notification_callback(@(ident), "closed");
}
static void
dispatch_closed_notifications(void) {
bool poll = false;
[notifications_polling_lock lock];
monotonic_t now = monotonic();
for (size_t i = tracked_notifications.count; i-- > 0; ) {
if (tracked_notifications.items[i].closed) {
set_cocoa_pending_action(COCOA_NOTIFICATION_CLOSED, tracked_notifications.items[i].ident);
free(tracked_notifications.items[i].ident);
remove_i_from_array(tracked_notifications.items, i, tracked_notifications.count);
} else if (now - tracked_notifications.items[i].creation_time < s_double_to_monotonic_t(CLOSE_POLL_TIME)) poll = true;
}
polling_notifications = poll;
[notifications_polling_lock unlock];
if (poll) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, CLOSE_POLL_INTERVAL * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
poll_for_closed_notifications();
});
}
}
static void
track_notification(char *ident) {
UNUserNotificationCenter *center = get_notification_center_safely();
if (center) {
[notifications_polling_lock lock];
bool has_existing = false;
for (size_t i = 0; i < tracked_notifications.count; i++) {
if (strcmp(tracked_notifications.items[i].ident, ident) == 0) {
tracked_notifications.items[i].creation_time = monotonic();
has_existing = true;
break;
}
}
if (!has_existing) {
ensure_space_for(&tracked_notifications, items, tn, tracked_notifications.count + 1, capacity, 8, false);
tracked_notifications.items[tracked_notifications.count++] = (tn){.ident=ident, .creation_time=monotonic()};
}
bool needs_poll = !polling_notifications;
[notifications_polling_lock unlock];
if (needs_poll) poll_for_closed_notifications();
}
}
static void
schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle, int urgency) {
UNUserNotificationCenter *center = get_notification_center_safely();
@@ -501,13 +581,12 @@ schedule_notification(const char *identifier, const char *title, const char *bod
content:content trigger:nil];
char *duped_ident = strdup(identifier ? identifier : "");
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]);
}
if (error != nil) log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]);
bool ok = error == nil;
dispatch_async(dispatch_get_main_queue(), ^{
do_notification_callback(@(duped_ident), ok ? "created" : "closed");
free(duped_ident);
if (ok) track_notification(duped_ident);
else free(duped_ident);
});
}];
[content release];
@@ -1037,6 +1116,8 @@ cleanup(void) {
dockMenu = nil;
if (beep_sound) [beep_sound release];
beep_sound = nil;
if (notifications_polling_lock) [notifications_polling_lock release];
notifications_polling_lock = nil;
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
drain_pending_notifications(NO);
@@ -1087,6 +1168,7 @@ bool
init_cocoa(PyObject *module) {
cocoa_clear_global_shortcuts();
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
notifications_polling_lock = [NSLock new];
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
return true;
}

View File

@@ -390,8 +390,6 @@ class DesktopIntegration:
class MacOSIntegration(DesktopIntegration):
supports_close_events: bool = False
def initialize(self) -> None:
from .fast_data_types import cocoa_set_notification_activated_callback
self.id_counter = count(start=1)