mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
Implement polling based closed notifications on macOS
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user