mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-19 23:14:55 +02:00
Implement a polling mode for close events
This commit is contained in:
@@ -132,14 +132,22 @@ is updated then the close event is not sent unless the updated notification
|
|||||||
also requests a close notification.
|
also requests a close notification.
|
||||||
|
|
||||||
Note that on some platforms, such as macOS, the OS does not inform applications
|
Note that on some platforms, such as macOS, the OS does not inform applications
|
||||||
when notifications are closed, on such platforms, terminals may reply with::
|
when notifications are closed, on such platforms, terminals reply with::
|
||||||
|
|
||||||
<OSC> 99 ; i=mynotification : p=close ; untracked <terminator>
|
<OSC> 99 ; i=mynotification : p=close ; untracked <terminator>
|
||||||
|
|
||||||
This means that the terminal has no way of knowing when the notification is
|
This means that the terminal has no way of knowing when the notification is
|
||||||
closed. |kitty|, on macOS, manually tracks notifications by polling the OS
|
closed. Instead, applications can poll the terminal to determine which
|
||||||
for a short period to see if they are closed, after which it gives up
|
notifications are still alive (not closed), with::
|
||||||
and replies with an ``untracked`` response.
|
|
||||||
|
<OSC> 99 ; i=myid : p=alive ; <terminator>
|
||||||
|
|
||||||
|
The terminal will reply with::
|
||||||
|
<OSC> 99 ; i=myid : p=alive ; id1,id2,id3 <terminator>
|
||||||
|
|
||||||
|
Here, ``myid`` is present for multiplxer support. The reponse from the terminal
|
||||||
|
contains a comma separated list of ids that are still alive.
|
||||||
|
|
||||||
|
|
||||||
Updating or closing an existing notification
|
Updating or closing an existing notification
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
@@ -244,7 +252,7 @@ Key Value Default Description
|
|||||||
``p`` One of ``title``, ``title`` Whether the payload is the notification title or body or query. If a
|
``p`` One of ``title``, ``title`` Whether the payload is the notification title or body or query. If a
|
||||||
``body``, notification has no title, the body will be used as title. Terminal
|
``body``, notification has no title, the body will be used as title. Terminal
|
||||||
``close``, emulators should ignore payloads of unknown type to allow for future
|
``close``, emulators should ignore payloads of unknown type to allow for future
|
||||||
``?`` expansion of this protocol.
|
``?``, ``alive`` expansion of this protocol.
|
||||||
|
|
||||||
|
|
||||||
``o`` One of ``always``, ``always`` When to honor the notification request. ``unfocused`` means when the window
|
``o`` One of ``always``, ``always`` When to honor the notification request. ``unfocused`` means when the window
|
||||||
|
|||||||
@@ -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 cocoa_list { char **items; size_t count, capacity; } cocoa_list;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* wd;
|
char* wd;
|
||||||
cocoa_list open_urls, closed_notifications, untracked_notifications;
|
cocoa_list open_urls, untracked_notifications;
|
||||||
} CocoaPendingActionsData;
|
} CocoaPendingActionsData;
|
||||||
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
||||||
|
|
||||||
@@ -1165,7 +1165,6 @@ static void
|
|||||||
cocoa_free_actions_data(void) {
|
cocoa_free_actions_data(void) {
|
||||||
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
|
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.open_urls);
|
||||||
cocoa_free_pending_list(&cocoa_pending_actions_data.closed_notifications);
|
|
||||||
cocoa_free_pending_list(&cocoa_pending_actions_data.untracked_notifications);
|
cocoa_free_pending_list(&cocoa_pending_actions_data.untracked_notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,8 +1174,6 @@ set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
|
|||||||
switch(action) {
|
switch(action) {
|
||||||
case LAUNCH_URLS:
|
case LAUNCH_URLS:
|
||||||
cocoa_append_to_pending_list(&cocoa_pending_actions_data.open_urls, data); break;
|
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;
|
|
||||||
case COCOA_NOTIFICATION_UNTRACKED:
|
case COCOA_NOTIFICATION_UNTRACKED:
|
||||||
cocoa_append_to_pending_list(&cocoa_pending_actions_data.untracked_notifications, data); break;
|
cocoa_append_to_pending_list(&cocoa_pending_actions_data.untracked_notifications, data); break;
|
||||||
default:
|
default:
|
||||||
@@ -1230,18 +1227,9 @@ process_cocoa_pending_actions(void) {
|
|||||||
}
|
}
|
||||||
cocoa_pending_actions_data.open_urls.count = 0;
|
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], false);
|
|
||||||
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;
|
|
||||||
|
|
||||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.untracked_notifications.count; cpa++) {
|
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.untracked_notifications.count; cpa++) {
|
||||||
if (cocoa_pending_actions_data.untracked_notifications.items[cpa]) {
|
if (cocoa_pending_actions_data.untracked_notifications.items[cpa]) {
|
||||||
cocoa_report_closed_notification(cocoa_pending_actions_data.untracked_notifications.items[cpa], true);
|
cocoa_report_live_notifications(cocoa_pending_actions_data.untracked_notifications.items[cpa]);
|
||||||
free(cocoa_pending_actions_data.untracked_notifications.items[cpa]);
|
free(cocoa_pending_actions_data.untracked_notifications.items[cpa]);
|
||||||
cocoa_pending_actions_data.untracked_notifications.items[cpa] = NULL;
|
cocoa_pending_actions_data.untracked_notifications.items[cpa] = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ typedef enum {
|
|||||||
MINIMIZE,
|
MINIMIZE,
|
||||||
QUIT,
|
QUIT,
|
||||||
USER_MENU_ACTION,
|
USER_MENU_ACTION,
|
||||||
COCOA_NOTIFICATION_CLOSED,
|
|
||||||
COCOA_NOTIFICATION_UNTRACKED,
|
COCOA_NOTIFICATION_UNTRACKED,
|
||||||
|
|
||||||
NUM_COCOA_PENDING_ACTIONS
|
NUM_COCOA_PENDING_ACTIONS
|
||||||
@@ -60,4 +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);
|
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 get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*);
|
||||||
void set_cocoa_pending_action(CocoaPendingAction action, const char*);
|
void set_cocoa_pending_action(CocoaPendingAction action, const char*);
|
||||||
void cocoa_report_closed_notification(const char* ident, bool untracked);
|
void cocoa_report_live_notifications(const char* ident);
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
|||||||
do_notification_callback([[[response notification] request] identifier], "activated");
|
do_notification_callback([[[response notification] request] identifier], "activated");
|
||||||
} else if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
|
} else if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
|
||||||
// this never actually happens on macOS. Bloody Crapple.
|
// this never actually happens on macOS. Bloody Crapple.
|
||||||
do_notification_callback([[[response notification] request] identifier], "closed");
|
// do_notification_callback([[[response notification] request] identifier], "closed");
|
||||||
}
|
}
|
||||||
completionHandler();
|
completionHandler();
|
||||||
}
|
}
|
||||||
@@ -457,23 +457,6 @@ get_notification_center_safely(void) {
|
|||||||
return center;
|
return center;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
remove_delivered_notification(const char *identifier) {
|
|
||||||
UNUserNotificationCenter *center = get_notification_center_safely();
|
|
||||||
if (!center) return false;
|
|
||||||
[center removeDeliveredNotificationsWithIdentifiers:@[ @(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
|
static bool
|
||||||
ident_in_list_of_notifications(NSString *ident, NSArray<UNNotification*> *list) {
|
ident_in_list_of_notifications(NSString *ident, NSArray<UNNotification*> *list) {
|
||||||
for (UNNotification *n in list) {
|
for (UNNotification *n in list) {
|
||||||
@@ -482,82 +465,40 @@ ident_in_list_of_notifications(NSString *ident, NSArray<UNNotification*> *list)
|
|||||||
return false;
|
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
|
void
|
||||||
cocoa_report_closed_notification(const char* ident, bool untracked) {
|
cocoa_report_live_notifications(const char* ident) {
|
||||||
do_notification_callback(@(ident), untracked ? "untracked" : "closed");
|
do_notification_callback(@(ident), "live");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static bool
|
||||||
dispatch_closed_notifications(void) {
|
remove_delivered_notification(const char *identifier) {
|
||||||
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;
|
|
||||||
} else {
|
|
||||||
set_cocoa_pending_action(COCOA_NOTIFICATION_UNTRACKED, tracked_notifications.items[i].ident);
|
|
||||||
free(tracked_notifications.items[i].ident);
|
|
||||||
remove_i_from_array(tracked_notifications.items, i, tracked_notifications.count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||||
if (center) {
|
if (!center) return false;
|
||||||
[notifications_polling_lock lock];
|
char *ident = strdup(identifier);
|
||||||
bool has_existing = false;
|
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * notifications) {
|
||||||
for (size_t i = 0; i < tracked_notifications.count; i++) {
|
if (ident_in_list_of_notifications(@(ident), notifications)) {
|
||||||
if (strcmp(tracked_notifications.items[i].ident, ident) == 0) {
|
[center removeDeliveredNotificationsWithIdentifiers:@[ @(ident) ]];
|
||||||
tracked_notifications.items[i].creation_time = monotonic();
|
|
||||||
has_existing = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!has_existing) {
|
free(ident);
|
||||||
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()};
|
return true;
|
||||||
}
|
}
|
||||||
bool needs_poll = !polling_notifications;
|
|
||||||
[notifications_polling_lock unlock];
|
static bool
|
||||||
if (needs_poll) poll_for_closed_notifications();
|
live_delivered_notifications(void) {
|
||||||
}
|
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||||
|
if (!center) return false;
|
||||||
|
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * notifications) {
|
||||||
|
NSMutableString *buffer = [[NSMutableString stringWithCapacity:1024] autorelease];
|
||||||
|
for (UNNotification *n in notifications) [buffer appendFormat:@"%@,", [[n request] identifier]];
|
||||||
|
const char *val = [buffer UTF8String];
|
||||||
|
set_cocoa_pending_action(COCOA_NOTIFICATION_UNTRACKED, val ? val : "");
|
||||||
|
}];
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
schedule_notification(const char *identifier, const char *title, const char *body, bool track_closing, int urgency) {
|
schedule_notification(const char *identifier, const char *title, const char *body, int urgency) {
|
||||||
UNUserNotificationCenter *center = get_notification_center_safely();
|
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||||
if (!center) return;
|
if (!center) return;
|
||||||
// Configure the notification's payload.
|
// Configure the notification's payload.
|
||||||
@@ -591,9 +532,8 @@ schedule_notification(const char *identifier, const char *title, const char *bod
|
|||||||
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;
|
bool ok = error == nil;
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
do_notification_callback(@(duped_ident), ok ? "created" : "closed");
|
do_notification_callback(@(duped_ident), ok ? "created" : "creation_failed");
|
||||||
if (ok && track_closing) track_notification(duped_ident);
|
free(duped_ident);
|
||||||
else free(duped_ident);
|
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
[content release];
|
[content release];
|
||||||
@@ -602,7 +542,7 @@ schedule_notification(const char *identifier, const char *title, const char *bod
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *identifier, *title, *body;
|
char *identifier, *title, *body;
|
||||||
int urgency; bool track_closing;
|
int urgency;
|
||||||
} QueuedNotification;
|
} QueuedNotification;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -612,13 +552,13 @@ typedef struct {
|
|||||||
static NotificationQueue notification_queue = {0};
|
static NotificationQueue notification_queue = {0};
|
||||||
|
|
||||||
static void
|
static void
|
||||||
queue_notification(const char *identifier, const char *title, const char* body, bool track_closing, int urgency) {
|
queue_notification(const char *identifier, const char *title, const char* body, int urgency) {
|
||||||
ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
||||||
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
|
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
|
||||||
n->identifier = identifier ? strdup(identifier) : NULL;
|
n->identifier = identifier ? strdup(identifier) : NULL;
|
||||||
n->title = title ? strdup(title) : NULL;
|
n->title = title ? strdup(title) : NULL;
|
||||||
n->body = body ? strdup(body) : NULL;
|
n->body = body ? strdup(body) : NULL;
|
||||||
n->urgency = urgency; n->track_closing = track_closing;
|
n->urgency = urgency;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -626,12 +566,12 @@ drain_pending_notifications(BOOL granted) {
|
|||||||
if (granted) {
|
if (granted) {
|
||||||
for (size_t i = 0; i < notification_queue.count; i++) {
|
for (size_t i = 0; i < notification_queue.count; i++) {
|
||||||
QueuedNotification *n = notification_queue.notifications + i;
|
QueuedNotification *n = notification_queue.notifications + i;
|
||||||
schedule_notification(n->identifier, n->title, n->body, n->track_closing, n->urgency);
|
schedule_notification(n->identifier, n->title, n->body, n->urgency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while(notification_queue.count) {
|
while(notification_queue.count) {
|
||||||
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
|
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
|
||||||
if (!granted) do_notification_callback(@(n->identifier), "closed");
|
if (!granted) do_notification_callback(@(n->identifier), "creation_failed");
|
||||||
free(n->identifier); free(n->title); free(n->body);
|
free(n->identifier); free(n->title); free(n->body);
|
||||||
memset(n, 0, sizeof(QueuedNotification));
|
memset(n, 0, sizeof(QueuedNotification));
|
||||||
}
|
}
|
||||||
@@ -644,16 +584,23 @@ cocoa_remove_delivered_notification(PyObject *self UNUSED, PyObject *x) {
|
|||||||
Py_RETURN_FALSE;
|
Py_RETURN_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
cocoa_live_delivered_notifications(PyObject *self UNUSED, PyObject *x UNUSED) {
|
||||||
|
if (live_delivered_notifications()) { Py_RETURN_TRUE; }
|
||||||
|
Py_RETURN_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||||
char *identifier = NULL, *title = NULL, *body = NULL; int urgency = 1;
|
char *identifier = NULL, *title = NULL, *body = NULL; int urgency = 1;
|
||||||
int track_closing;
|
if (!PyArg_ParseTuple(args, "sss|i", &identifier, &title, &body, &urgency)) return NULL;
|
||||||
if (!PyArg_ParseTuple(args, "sssp|i", &identifier, &title, &body, &track_closing, &urgency)) return NULL;
|
|
||||||
|
|
||||||
UNUserNotificationCenter *center = get_notification_center_safely();
|
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||||
if (!center) Py_RETURN_NONE;
|
if (!center) Py_RETURN_NONE;
|
||||||
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
||||||
queue_notification(identifier, title, body, track_closing, urgency);
|
queue_notification(identifier, title, body, urgency);
|
||||||
|
|
||||||
// The badge permission needs to be requested as well, even though it is not used,
|
// The badge permission needs to be requested as well, even though it is not used,
|
||||||
// otherwise macOS refuses to show the preference checkbox for enable/disable notification sound.
|
// otherwise macOS refuses to show the preference checkbox for enable/disable notification sound.
|
||||||
@@ -1123,8 +1070,6 @@ cleanup(void) {
|
|||||||
dockMenu = nil;
|
dockMenu = nil;
|
||||||
if (beep_sound) [beep_sound release];
|
if (beep_sound) [beep_sound release];
|
||||||
beep_sound = nil;
|
beep_sound = nil;
|
||||||
if (notifications_polling_lock) [notifications_polling_lock release];
|
|
||||||
notifications_polling_lock = nil;
|
|
||||||
|
|
||||||
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
||||||
drain_pending_notifications(NO);
|
drain_pending_notifications(NO);
|
||||||
@@ -1164,6 +1109,7 @@ static PyMethodDef module_methods[] = {
|
|||||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||||
{"cocoa_remove_delivered_notification", (PyCFunction)cocoa_remove_delivered_notification, METH_O, ""},
|
{"cocoa_remove_delivered_notification", (PyCFunction)cocoa_remove_delivered_notification, METH_O, ""},
|
||||||
|
{"cocoa_live_delivered_notifications", (PyCFunction)cocoa_live_delivered_notifications, METH_NOARGS, ""},
|
||||||
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||||
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
||||||
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
|
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
|
||||||
@@ -1175,7 +1121,6 @@ bool
|
|||||||
init_cocoa(PyObject *module) {
|
init_cocoa(PyObject *module) {
|
||||||
cocoa_clear_global_shortcuts();
|
cocoa_clear_global_shortcuts();
|
||||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||||
notifications_polling_lock = [NSLock new];
|
|
||||||
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
|
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -568,12 +568,12 @@ def cocoa_send_notification(
|
|||||||
identifier: str,
|
identifier: str,
|
||||||
title: str,
|
title: str,
|
||||||
body: str,
|
body: str,
|
||||||
track_closing: bool,
|
|
||||||
urgency: int = 1,
|
urgency: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def cocoa_remove_delivered_notification(identifier: str) -> bool: ...
|
def cocoa_remove_delivered_notification(identifier: str) -> bool: ...
|
||||||
|
def cocoa_live_delivered_notifications() -> bool: ...
|
||||||
|
|
||||||
def create_os_window(
|
def create_os_window(
|
||||||
get_window_size: Callable[[int, int, int, int, float, float], Tuple[int,
|
get_window_size: Callable[[int, int, int, int, float, float], Tuple[int,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import Any, Callable, Dict, FrozenSet, Iterator, List, NamedTuple, Optional, Set, Tuple, Union
|
from typing import Any, Callable, Dict, FrozenSet, Iterator, List, NamedTuple, Optional, Sequence, Set, Tuple, Union
|
||||||
from weakref import ReferenceType, ref
|
from weakref import ReferenceType, ref
|
||||||
|
|
||||||
from .constants import cache_dir, config_dir, is_macos, logo_png_file
|
from .constants import cache_dir, config_dir, is_macos, logo_png_file
|
||||||
@@ -111,6 +111,7 @@ class PayloadType(Enum):
|
|||||||
query = '?'
|
query = '?'
|
||||||
close = 'close'
|
close = 'close'
|
||||||
icon = 'icon'
|
icon = 'icon'
|
||||||
|
alive = 'alive'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_text(self) -> bool:
|
def is_text(self) -> bool:
|
||||||
@@ -203,7 +204,7 @@ class NotificationCommand:
|
|||||||
|
|
||||||
# event callbacks
|
# event callbacks
|
||||||
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
||||||
on_close: Optional[Callable[['NotificationCommand', bool], None]] = None
|
on_close: Optional[Callable[['NotificationCommand'], None]] = None
|
||||||
on_update: Optional[Callable[['NotificationCommand', 'NotificationCommand'], None]] = None
|
on_update: Optional[Callable[['NotificationCommand', 'NotificationCommand'], None]] = None
|
||||||
|
|
||||||
# metadata
|
# metadata
|
||||||
@@ -410,6 +411,9 @@ class DesktopIntegration:
|
|||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def query_live_notifications(self, channel_id: int, identifier: str) -> None:
|
||||||
|
raise NotImplementedError('Implement me in subclass')
|
||||||
|
|
||||||
def close_notification(self, desktop_notification_id: int) -> bool:
|
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||||
raise NotImplementedError('Implement me in subclass')
|
raise NotImplementedError('Implement me in subclass')
|
||||||
|
|
||||||
@@ -432,11 +436,21 @@ class DesktopIntegration:
|
|||||||
|
|
||||||
class MacOSIntegration(DesktopIntegration):
|
class MacOSIntegration(DesktopIntegration):
|
||||||
|
|
||||||
|
supports_close_events: bool = False
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
from .fast_data_types import cocoa_set_notification_activated_callback
|
from .fast_data_types import cocoa_set_notification_activated_callback
|
||||||
self.id_counter = count(start=1)
|
self.id_counter = count(start=1)
|
||||||
|
self.live_notification_queries: List[Tuple[int, str]] = []
|
||||||
cocoa_set_notification_activated_callback(self.notification_activated)
|
cocoa_set_notification_activated_callback(self.notification_activated)
|
||||||
|
|
||||||
|
def query_live_notifications(self, channel_id: int, identifier: str) -> None:
|
||||||
|
from .fast_data_types import cocoa_live_delivered_notifications
|
||||||
|
if not cocoa_live_delivered_notifications():
|
||||||
|
self.notification_manager.send_live_response(channel_id, identifier, ())
|
||||||
|
else:
|
||||||
|
self.live_notification_queries.append((channel_id, identifier))
|
||||||
|
|
||||||
def close_notification(self, desktop_notification_id: int) -> bool:
|
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||||
from .fast_data_types import cocoa_remove_delivered_notification
|
from .fast_data_types import cocoa_remove_delivered_notification
|
||||||
close_succeeded = cocoa_remove_delivered_notification(str(desktop_notification_id))
|
close_succeeded = cocoa_remove_delivered_notification(str(desktop_notification_id))
|
||||||
@@ -455,12 +469,19 @@ class MacOSIntegration(DesktopIntegration):
|
|||||||
# for %% escaping.
|
# for %% escaping.
|
||||||
body = (nc.body or ' ')
|
body = (nc.body or ' ')
|
||||||
assert nc.urgency is not None
|
assert nc.urgency is not None
|
||||||
cocoa_send_notification(str(desktop_notification_id), nc.title, body, bool(nc.close_response_requested), nc.urgency.value)
|
cocoa_send_notification(str(desktop_notification_id), nc.title, body, nc.urgency.value)
|
||||||
return desktop_notification_id
|
return desktop_notification_id
|
||||||
|
|
||||||
def notification_activated(self, event: str, ident: str) -> None:
|
def notification_activated(self, event: str, ident: str) -> None:
|
||||||
if debug_desktop_integration:
|
if debug_desktop_integration:
|
||||||
log_error(f'Notification {ident} {event=}')
|
log_error(f'Notification {ident} {event=}')
|
||||||
|
if event == 'live':
|
||||||
|
live_ids = tuple(int(x) for x in ident.split(',') if x)
|
||||||
|
self.notification_manager.purge_dead_notifications(live_ids)
|
||||||
|
self.live_notification_queries, queries = [], self.live_notification_queries
|
||||||
|
for channel_id, req_id in queries:
|
||||||
|
self.notification_manager.send_live_response(channel_id, req_id, live_ids)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
desktop_notification_id = int(ident)
|
desktop_notification_id = int(ident)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -468,12 +489,12 @@ class MacOSIntegration(DesktopIntegration):
|
|||||||
return
|
return
|
||||||
if event == "created":
|
if event == "created":
|
||||||
self.notification_manager.notification_created(desktop_notification_id)
|
self.notification_manager.notification_created(desktop_notification_id)
|
||||||
|
from .fast_data_types import cocoa_live_delivered_notifications
|
||||||
|
cocoa_live_delivered_notifications() # so that we purge dead notifications
|
||||||
elif event == "activated":
|
elif event == "activated":
|
||||||
self.notification_manager.notification_activated(desktop_notification_id)
|
self.notification_manager.notification_activated(desktop_notification_id)
|
||||||
elif event == "closed":
|
elif event == "creation_failed":
|
||||||
self.notification_manager.notification_closed(desktop_notification_id)
|
self.notification_manager.notification_closed(desktop_notification_id)
|
||||||
elif event == "untracked":
|
|
||||||
self.notification_manager.notification_closed(desktop_notification_id, True)
|
|
||||||
|
|
||||||
|
|
||||||
class FreeDesktopIntegration(DesktopIntegration):
|
class FreeDesktopIntegration(DesktopIntegration):
|
||||||
@@ -486,6 +507,9 @@ class FreeDesktopIntegration(DesktopIntegration):
|
|||||||
self.dbus_to_desktop: 'OrderedDict[int, int]' = OrderedDict()
|
self.dbus_to_desktop: 'OrderedDict[int, int]' = OrderedDict()
|
||||||
self.desktop_to_dbus: Dict[int, int] = {}
|
self.desktop_to_dbus: Dict[int, int] = {}
|
||||||
|
|
||||||
|
def query_live_notifications(self, channel_id: int, identifier: str) -> None:
|
||||||
|
self.notification_manager.send_live_response(channel_id, identifier, tuple(self.desktop_to_dbus))
|
||||||
|
|
||||||
def close_notification(self, desktop_notification_id: int) -> bool:
|
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||||
from .fast_data_types import dbus_close_notification
|
from .fast_data_types import dbus_close_notification
|
||||||
close_succeeded = False
|
close_succeeded = False
|
||||||
@@ -672,14 +696,14 @@ class NotificationManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log('Notification on_update handler failed with error:', e)
|
self.log('Notification on_update handler failed with error:', e)
|
||||||
|
|
||||||
def notification_closed(self, desktop_notification_id: int, untracked: bool = False) -> None:
|
def notification_closed(self, desktop_notification_id: int) -> None:
|
||||||
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
||||||
self.purge_notification(n)
|
self.purge_notification(n)
|
||||||
if n.close_response_requested:
|
if n.close_response_requested:
|
||||||
self.send_closed_response(n.channel_id, n.identifier, untracked)
|
self.send_closed_response(n.channel_id, n.identifier)
|
||||||
if n.on_close is not None:
|
if n.on_close is not None:
|
||||||
try:
|
try:
|
||||||
n.on_close(n, untracked)
|
n.on_close(n)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log('Notification on_close handler failed with error:', e)
|
self.log('Notification on_close handler failed with error:', e)
|
||||||
|
|
||||||
@@ -740,6 +764,8 @@ class NotificationManager:
|
|||||||
self.register_in_progress_notification(cmd, desktop_notification_id)
|
self.register_in_progress_notification(cmd, desktop_notification_id)
|
||||||
if existing_cmd:
|
if existing_cmd:
|
||||||
self.notification_replaced(existing_cmd, cmd)
|
self.notification_replaced(existing_cmd, cmd)
|
||||||
|
if not self.desktop_integration.supports_close_events and cmd.close_response_requested:
|
||||||
|
self.send_closed_response(channel_id, cmd.identifier, untracked=True)
|
||||||
return desktop_notification_id
|
return desktop_notification_id
|
||||||
|
|
||||||
def register_in_progress_notification(self, cmd: NotificationCommand, desktop_notification_id: int) -> None:
|
def register_in_progress_notification(self, cmd: NotificationCommand, desktop_notification_id: int) -> None:
|
||||||
@@ -764,6 +790,10 @@ class NotificationManager:
|
|||||||
if payload_type is PayloadType.query:
|
if payload_type is PayloadType.query:
|
||||||
self.channel.send(channel_id, self.desktop_integration.query_response(cmd.identifier))
|
self.channel.send(channel_id, self.desktop_integration.query_response(cmd.identifier))
|
||||||
return None
|
return None
|
||||||
|
if payload_type is PayloadType.alive:
|
||||||
|
if cmd.identifier:
|
||||||
|
self.desktop_integration.query_live_notifications(channel_id, cmd.identifier)
|
||||||
|
return None
|
||||||
if payload_type is PayloadType.close:
|
if payload_type is PayloadType.close:
|
||||||
if cmd.identifier:
|
if cmd.identifier:
|
||||||
to_close = self.in_progress_notification_commands_by_client_id.get(cmd.identifier)
|
to_close = self.in_progress_notification_commands_by_client_id.get(cmd.identifier)
|
||||||
@@ -783,7 +813,19 @@ class NotificationManager:
|
|||||||
|
|
||||||
def send_closed_response(self, channel_id: int, client_id: str, untracked: bool = False) -> None:
|
def send_closed_response(self, channel_id: int, client_id: str, untracked: bool = False) -> None:
|
||||||
payload = 'untracked' if untracked else ''
|
payload = 'untracked' if untracked else ''
|
||||||
self.channel.send(channel_id, f'99;i={client_id}:p=close;{payload}')
|
self.channel.send(channel_id, f'99;i={client_id}:p={PayloadType.close.value};{payload}')
|
||||||
|
|
||||||
|
def send_live_response(self, channel_id: int, client_id: str, live_desktop_ids: Sequence[int]) -> None:
|
||||||
|
ids = []
|
||||||
|
for desktop_notification_id in live_desktop_ids:
|
||||||
|
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
||||||
|
if n.identifier and n.channel_id == channel_id:
|
||||||
|
ids.append(n.identifier)
|
||||||
|
self.channel.send(channel_id, f'99;i={client_id}:p={PayloadType.alive.value};{",".join(ids)}')
|
||||||
|
|
||||||
|
def purge_dead_notifications(self, live_desktop_ids: Sequence[int]) -> None:
|
||||||
|
for d in set(self.in_progress_notification_commands) - set(live_desktop_ids):
|
||||||
|
self.purge_notification(self.in_progress_notification_commands[d])
|
||||||
|
|
||||||
def purge_notification(self, cmd: NotificationCommand) -> None:
|
def purge_notification(self, cmd: NotificationCommand) -> None:
|
||||||
self.in_progress_notification_commands_by_client_id.pop(cmd.identifier, None)
|
self.in_progress_notification_commands_by_client_id.pop(cmd.identifier, None)
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ def do_test(self: 'TestNotifications', tdir: str) -> None:
|
|||||||
# Test querying
|
# Test querying
|
||||||
h('i=xyz:p=?')
|
h('i=xyz:p=?')
|
||||||
self.assertFalse(di.notifications)
|
self.assertFalse(di.notifications)
|
||||||
qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon:c=1'
|
qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon,alive:c=1'
|
||||||
self.ae(ch.responses, [f'99;i=xyz:p=?;{qr}'])
|
self.ae(ch.responses, [f'99;i=xyz:p=?;{qr}'])
|
||||||
reset()
|
reset()
|
||||||
h('p=?')
|
h('p=?')
|
||||||
|
|||||||
Reference in New Issue
Block a user