Implement a polling mode for close events

This commit is contained in:
Kovid Goyal
2024-07-28 15:35:22 +05:30
parent 02bc104091
commit 083f158bbd
7 changed files with 115 additions and 133 deletions

View File

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

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 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;
} }

View File

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

View File

@@ -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((&notification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true); ensure_space_for((&notification_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;
} }

View File

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

View File

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

View File

@@ -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=?')